001 package com.github.sarxos.webcam;
002
003 import java.awt.image.BufferedImage;
004 import java.util.ArrayList;
005 import java.util.List;
006 import java.util.concurrent.ExecutorService;
007 import java.util.concurrent.Executors;
008 import java.util.concurrent.ThreadFactory;
009
010 import org.slf4j.Logger;
011 import org.slf4j.LoggerFactory;
012
013 import com.github.sarxos.webcam.util.jh.JHBlurFilter;
014 import com.github.sarxos.webcam.util.jh.JHGrayFilter;
015
016
017 /**
018 * Webcam motion detector.
019 *
020 * @author Bartosz Firyn (SarXos)
021 */
022 public class WebcamMotionDetector {
023
024 private static final Logger LOG = LoggerFactory.getLogger(WebcamMotionDetector.class);
025
026 public static final int DEFAULT_THREASHOLD = 25;
027
028 /**
029 * Create new threads for detector internals.
030 *
031 * @author Bartosz Firyn (SarXos)
032 */
033 private static final class DetectorThreadFactory implements ThreadFactory {
034
035 private static int number = 0;
036
037 @Override
038 public Thread newThread(Runnable runnable) {
039 Thread t = new Thread(runnable, "motion-detector-" + (++number));
040 t.setUncaughtExceptionHandler(WebcamExceptionHandler.getInstance());
041 t.setDaemon(true);
042 return t;
043 }
044
045 }
046
047 /**
048 * Run motion detector.
049 *
050 * @author Bartosz Firyn (SarXos)
051 */
052 private class Runner implements Runnable {
053
054 @Override
055 public void run() {
056 running = true;
057 while (running && webcam.isOpen()) {
058 detect();
059 try {
060 Thread.sleep(interval);
061 } catch (InterruptedException e) {
062 throw new RuntimeException(e);
063 }
064 }
065 }
066 }
067
068 /**
069 * Change motion to false after specified number of seconds.
070 *
071 * @author Bartosz Firyn (SarXos)
072 */
073 private class Changer implements Runnable {
074
075 @Override
076 public void run() {
077 int time = inertia == 0 ? interval + interval / 2 : inertia;
078 LOG.debug("Motion change has been sheduled in " + time + "ms");
079 try {
080 Thread.sleep(time);
081 } catch (InterruptedException e) {
082 throw new RuntimeException(e);
083 }
084 synchronized (mutex) {
085 motion = false;
086 }
087 }
088 }
089
090 private List<WebcamMotionListener> listeners = new ArrayList<WebcamMotionListener>();
091
092 private Object mutex = new Object();
093
094 private boolean running = false;
095
096 /**
097 * Is motion?
098 */
099 private boolean motion = false;
100
101 /**
102 * Previously captured image.
103 */
104 private BufferedImage previous = null;
105
106 /**
107 * Webcam to be used to detect motion.
108 */
109 private Webcam webcam = null;
110
111 /**
112 * Motion check interval (1000 ms by default).
113 */
114 private int interval = 1000;
115
116 /**
117 * Pixel intensity threshold (0 - 255).
118 */
119 private int threshold = 10;
120
121 /**
122 * How long motion is valid.
123 */
124 private int inertia = 10000;
125
126 /**
127 * Motion strength (0 = no motion).
128 */
129 private int strength = 0;
130
131 /**
132 * Blur filter instance.
133 */
134 private JHBlurFilter blur = new JHBlurFilter(3, 3, 1);
135
136 /**
137 * Grayscale filter instance.
138 */
139 private JHGrayFilter gray = new JHGrayFilter();
140
141 /**
142 * Thread factory.
143 */
144 private ThreadFactory threadFactory = new DetectorThreadFactory();
145
146 /**
147 * Executor.
148 */
149 private ExecutorService executor = Executors.newCachedThreadPool(threadFactory);
150
151 /**
152 * Create motion detector. Will open webcam if it is closed.
153 *
154 * @param webcam web camera instance
155 * @param threshold intensity threshold (0 - 255)
156 * @param inertia for how long motion is valid (seconds)
157 */
158 public WebcamMotionDetector(Webcam webcam, int threshold, int inertia) {
159 this.webcam = webcam;
160 this.threshold = threshold;
161 this.inertia = inertia;
162 }
163
164 /**
165 * Create motion detector with default parameter inertia = 0.
166 *
167 * @param webcam web camera instance
168 * @param threshold intensity threshold (0 - 255)
169 */
170 public WebcamMotionDetector(Webcam webcam, int threshold) {
171 this(webcam, threshold, 0);
172 }
173
174 /**
175 * Create motion detector with default parameters - threshold = 25, inertia
176 * = 0.
177 *
178 * @param webcam web camera instance
179 */
180 public WebcamMotionDetector(Webcam webcam) {
181 this(webcam, DEFAULT_THREASHOLD, 0);
182 }
183
184 public void start() {
185 if (!webcam.isOpen()) {
186 webcam.open();
187 }
188 LOG.debug("Starting motion detector");
189 executor.submit(new Runner());
190 }
191
192 public void stop() {
193 running = false;
194 if (webcam.isOpen()) {
195 webcam.close();
196 }
197 }
198
199 protected void detect() {
200
201 if (LOG.isDebugEnabled()) {
202 LOG.debug(WebcamMotionDetector.class.getSimpleName() + ".detect()");
203 }
204
205 if (motion) {
206 LOG.debug("Motion detector still in inertia state, no need to check");
207 return;
208 }
209
210 BufferedImage current = webcam.getImage();
211
212 current = blur.filter(current, null);
213 current = gray.filter(current, null);
214
215 if (previous != null) {
216
217 int w = current.getWidth();
218 int h = current.getHeight();
219
220 int strength = 0;
221
222 synchronized (mutex) {
223 for (int i = 0; i < w; i++) {
224 for (int j = 0; j < h; j++) {
225
226 int c = current.getRGB(i, j);
227 int p = previous.getRGB(i, j);
228
229 int rgb = combinePixels(c, p);
230
231 int cr = (rgb & 0x00ff0000) >> 16;
232 int cg = (rgb & 0x0000ff00) >> 8;
233 int cb = (rgb & 0x000000ff);
234
235 int max = Math.max(Math.max(cr, cg), cb);
236
237 if (max > threshold) {
238
239 if (!motion) {
240 executor.submit(new Changer());
241 motion = true;
242 }
243
244 strength++; // unit = 1 / px^2
245 }
246 }
247 }
248
249 this.strength = strength;
250
251 if (motion) {
252 notifyMotionListeners();
253 }
254 }
255 }
256
257 previous = current;
258 }
259
260 /**
261 * Will notify all attached motion listeners.
262 */
263 private void notifyMotionListeners() {
264 WebcamMotionEvent wme = new WebcamMotionEvent(this, strength);
265 for (WebcamMotionListener l : listeners) {
266 try {
267 l.motionDetected(wme);
268 } catch (Exception e) {
269 e.printStackTrace();
270 }
271 }
272 }
273
274 /**
275 * Add motion listener.
276 *
277 * @param l listener to add
278 * @return true if listeners list has been changed, false otherwise
279 */
280 public boolean addMotionListener(WebcamMotionListener l) {
281 return listeners.add(l);
282 }
283
284 /**
285 * @return All motion listeners as array
286 */
287 public WebcamMotionListener[] getMotionListeners() {
288 return listeners.toArray(new WebcamMotionListener[listeners.size()]);
289 }
290
291 /**
292 * Removes motion listener.
293 *
294 * @param l motion listener to remove
295 * @return true if listener was available on the list, false otherwise
296 */
297 public boolean removeMotionListener(WebcamMotionListener l) {
298 return listeners.remove(l);
299 }
300
301 /**
302 * @return Motion check interval in milliseconds
303 */
304 public int getInterval() {
305 return interval;
306 }
307
308 public void setInterval(int interval) {
309 this.interval = interval;
310 }
311
312 public Webcam getWebcam() {
313 return webcam;
314 }
315
316 public boolean isMotion() {
317 if (!running) {
318 LOG.warn("Motion cannot be detected when detector is not running!");
319 }
320 return motion;
321 }
322
323 public int getMotionStrength() {
324 return strength;
325 }
326
327 private static int combinePixels(int rgb1, int rgb2) {
328
329 int a1 = (rgb1 >> 24) & 0xff;
330 int r1 = (rgb1 >> 16) & 0xff;
331 int g1 = (rgb1 >> 8) & 0xff;
332 int b1 = rgb1 & 0xff;
333 int a2 = (rgb2 >> 24) & 0xff;
334 int r2 = (rgb2 >> 16) & 0xff;
335 int g2 = (rgb2 >> 8) & 0xff;
336 int b2 = rgb2 & 0xff;
337
338 r1 = clamp(Math.abs(r1 - r2));
339 g1 = clamp(Math.abs(g1 - g2));
340 b1 = clamp(Math.abs(b1 - b2));
341
342 if (a1 != 0xff) {
343 a1 = a1 * 0xff / 255;
344 int a3 = (255 - a1) * a2 / 255;
345 r1 = clamp((r1 * a1 + r2 * a3) / 255);
346 g1 = clamp((g1 * a1 + g2 * a3) / 255);
347 b1 = clamp((b1 * a1 + b2 * a3) / 255);
348 a1 = clamp(a1 + a3);
349 }
350
351 return (a1 << 24) | (r1 << 16) | (g1 << 8) | b1;
352 }
353
354 /**
355 * Clamp a value to the range 0..255
356 */
357 private static int clamp(int c) {
358 if (c < 0) {
359 return 0;
360 }
361 if (c > 255) {
362 return 255;
363 }
364 return c;
365 }
366 }