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