001package com.github.sarxos.webcam;
002
003import java.awt.Point;
004import java.awt.image.BufferedImage;
005import java.util.ArrayList;
006import java.util.List;
007import java.util.concurrent.ExecutorService;
008import java.util.concurrent.Executors;
009import java.util.concurrent.ThreadFactory;
010import java.util.concurrent.atomic.AtomicBoolean;
011import java.util.concurrent.atomic.AtomicInteger;
012
013import org.slf4j.Logger;
014import org.slf4j.LoggerFactory;
015
016import com.github.sarxos.webcam.util.jh.JHBlurFilter;
017import com.github.sarxos.webcam.util.jh.JHGrayFilter;
018
019
020/**
021 * Webcam motion detector.
022 * 
023 * @author Bartosz Firyn (SarXos)
024 */
025public class WebcamMotionDetector {
026
027        /**
028         * Logger.
029         */
030        private static final Logger LOG = LoggerFactory.getLogger(WebcamMotionDetector.class);
031
032        /**
033         * Thread number in pool.
034         */
035        private static final AtomicInteger NT = new AtomicInteger(0);
036
037        /**
038         * Thread factory.
039         */
040        private static final ThreadFactory THREAD_FACTORY = new DetectorThreadFactory();
041
042        /**
043         * Default pixel difference intensity threshold (set to 25).
044         */
045        public static final int DEFAULT_PIXEL_THREASHOLD = 25;
046
047        /**
048         * Default check interval (in milliseconds, set to 1 second).
049         */
050        public static final int DEFAULT_INTERVAL = 1000;
051
052        /**
053         * Default percentage image area fraction threshold (set to 0.2%).
054         */
055        public static final double DEFAULT_AREA_THREASHOLD = 0.2;
056
057        /**
058         * Create new threads for detector internals.
059         * 
060         * @author Bartosz Firyn (SarXos)
061         */
062        private static final class DetectorThreadFactory implements ThreadFactory {
063
064                @Override
065                public Thread newThread(Runnable runnable) {
066                        Thread t = new Thread(runnable, String.format("motion-detector-%d", NT.incrementAndGet()));
067                        t.setUncaughtExceptionHandler(WebcamExceptionHandler.getInstance());
068                        t.setDaemon(true);
069                        return t;
070                }
071        }
072
073        /**
074         * Run motion detector.
075         * 
076         * @author Bartosz Firyn (SarXos)
077         */
078        private class Runner implements Runnable {
079
080                @Override
081                public void run() {
082
083                        running.set(true);
084
085                        while (running.get() && webcam.isOpen()) {
086                                try {
087                                        detect();
088                                        Thread.sleep(interval);
089                                } catch (InterruptedException e) {
090                                        break;
091                                } catch (Exception e) {
092                                        WebcamExceptionHandler.handle(e);
093                                }
094                        }
095
096                        running.set(false);
097                }
098        }
099
100        /**
101         * Change motion to false after specified number of seconds.
102         * 
103         * @author Bartosz Firyn (SarXos)
104         */
105        private class Inverter implements Runnable {
106
107                @Override
108                public void run() {
109
110                        int delay = 0;
111
112                        while (running.get()) {
113
114                                try {
115                                        Thread.sleep(10);
116                                } catch (InterruptedException e) {
117                                        break;
118                                }
119
120                                delay = inertia != -1 ? inertia : 2 * interval;
121
122                                if (lastMotionTimestamp + delay < System.currentTimeMillis()) {
123                                        motion.set(false);
124                                }
125                        }
126                }
127        }
128
129        /**
130         * Executor.
131         */
132        private final ExecutorService executor = Executors.newFixedThreadPool(2, THREAD_FACTORY);
133
134        /**
135         * Motion listeners.
136         */
137        private final List<WebcamMotionListener> listeners = new ArrayList<WebcamMotionListener>();
138
139        /**
140         * Is detector running?
141         */
142        private final AtomicBoolean running = new AtomicBoolean(false);
143
144        /**
145         * Is motion?
146         */
147        private final AtomicBoolean motion = new AtomicBoolean(false);
148
149        /**
150         * Previously captured image.
151         */
152        private BufferedImage previous = null;
153
154        /**
155         * Webcam to be used to detect motion.
156         */
157        private Webcam webcam = null;
158
159        /**
160         * Motion check interval (1000 ms by default).
161         */
162        private volatile int interval = DEFAULT_INTERVAL;
163
164        /**
165         * Pixel intensity threshold (0 - 255).
166         */
167        private volatile int pixelThreshold = DEFAULT_PIXEL_THREASHOLD;
168
169        /**
170         * Pixel intensity threshold (0 - 100).
171         */
172        private volatile double areaThreshold = DEFAULT_AREA_THREASHOLD;
173
174        /**
175         * How long motion is valid (in milliseconds). Default value is 2 seconds.
176         */
177        private volatile int inertia = -1;
178
179        /**
180         * Motion strength (0 = no motion, 100 = full image covered by motion).
181         */
182        private double area = 0;
183
184        /**
185         * Center of motion gravity.
186         */
187        private Point cog = null;
188
189        /**
190         * Timestamp when motion has been observed last time.
191         */
192        private volatile long lastMotionTimestamp = 0;
193
194        /**
195         * Blur filter instance.
196         */
197        private final JHBlurFilter blur = new JHBlurFilter(6, 6, 1);
198
199        /**
200         * Gray filter instance.
201         */
202        private final JHGrayFilter gray = new JHGrayFilter();
203
204        /**
205         * Create motion detector. Will open webcam if it is closed.
206         * 
207         * @param webcam web camera instance
208         * @param pixelThreshold intensity threshold (0 - 255)
209         * @param areaThreshold percentage threshold of image covered by motion
210         * @param inertia for how long motion is valid (seconds)
211         * @param interval the check interval
212         */
213        public WebcamMotionDetector(Webcam webcam, int pixelThreshold, double areaThreshold, int interval) {
214
215                this.webcam = webcam;
216
217                setPixelThreshold(pixelThreshold);
218                setAreaThreshold(areaThreshold);
219                setInterval(interval);
220
221                int w = webcam.getViewSize().width;
222                int h = webcam.getViewSize().height;
223
224                cog = new Point(w / 2, h / 2);
225        }
226
227        /**
228         * Create motion detector with default parameter inertia = 0.
229         * 
230         * @param webcam web camera instance
231         * @param pixelThreshold intensity threshold (0 - 255)
232         * @param areaThreshol percentage threshold of image covered by motion
233         */
234        public WebcamMotionDetector(Webcam webcam, int pixelThreshold, double areaThreshold) {
235                this(webcam, pixelThreshold, areaThreshold, DEFAULT_INTERVAL);
236        }
237
238        /**
239         * Create motion detector with default parameter inertia = 0.
240         * 
241         * @param webcam web camera instance
242         * @param pixelThreshold intensity threshold (0 - 255)
243         */
244        public WebcamMotionDetector(Webcam webcam, int pixelThreshold) {
245                this(webcam, pixelThreshold, DEFAULT_AREA_THREASHOLD);
246        }
247
248        /**
249         * Create motion detector with default parameters - threshold = 25, inertia
250         * = 0.
251         * 
252         * @param webcam web camera instance
253         */
254        public WebcamMotionDetector(Webcam webcam) {
255                this(webcam, DEFAULT_PIXEL_THREASHOLD);
256        }
257
258        public void start() {
259                if (running.compareAndSet(false, true)) {
260                        webcam.open();
261                        executor.submit(new Runner());
262                        executor.submit(new Inverter());
263                }
264        }
265
266        public void stop() {
267                if (running.compareAndSet(true, false)) {
268                        webcam.close();
269                        executor.shutdownNow();
270                }
271        }
272
273        protected void detect() {
274
275                BufferedImage current = webcam.getImage();
276
277                current = blur.filter(current, null);
278                current = gray.filter(current, null);
279
280                int p = 0;
281
282                int cogX = 0;
283                int cogY = 0;
284
285                int w = current.getWidth();
286                int h = current.getHeight();
287
288                if (previous != null) {
289                        for (int x = 0; x < w; x++) {
290                                for (int y = 0; y < h; y++) {
291
292                                        int cpx = current.getRGB(x, y);
293                                        int ppx = previous.getRGB(x, y);
294                                        int pid = combinePixels(cpx, ppx) & 0x000000ff;
295
296                                        if (pid >= pixelThreshold) {
297                                                cogX += x;
298                                                cogY += y;
299                                                p += 1;
300                                        }
301                                }
302                        }
303                }
304
305                area = p * 100d / (w * h);
306
307                if (area >= areaThreshold) {
308
309                        cog = new Point(cogX / p, cogY / p);
310
311                        if (motion.compareAndSet(false, true)) {
312                                lastMotionTimestamp = System.currentTimeMillis();
313                        }
314
315                        notifyMotionListeners();
316
317                } else {
318                        cog = new Point(w / 2, h / 2);
319                }
320
321                previous = current;
322        }
323
324        /**
325         * Will notify all attached motion listeners.
326         */
327        private void notifyMotionListeners() {
328                WebcamMotionEvent wme = new WebcamMotionEvent(this, area, cog);
329                for (WebcamMotionListener l : listeners) {
330                        try {
331                                l.motionDetected(wme);
332                        } catch (Exception e) {
333                                WebcamExceptionHandler.handle(e);
334                        }
335                }
336        }
337
338        /**
339         * Add motion listener.
340         * 
341         * @param l listener to add
342         * @return true if listeners list has been changed, false otherwise
343         */
344        public boolean addMotionListener(WebcamMotionListener l) {
345                return listeners.add(l);
346        }
347
348        /**
349         * @return All motion listeners as array
350         */
351        public WebcamMotionListener[] getMotionListeners() {
352                return listeners.toArray(new WebcamMotionListener[listeners.size()]);
353        }
354
355        /**
356         * Removes motion listener.
357         * 
358         * @param l motion listener to remove
359         * @return true if listener was available on the list, false otherwise
360         */
361        public boolean removeMotionListener(WebcamMotionListener l) {
362                return listeners.remove(l);
363        }
364
365        /**
366         * @return Motion check interval in milliseconds
367         */
368        public int getInterval() {
369                return interval;
370        }
371
372        /**
373         * Motion check interval in milliseconds. After motion is detected, it's
374         * valid for time which is equal to value of 2 * interval.
375         * 
376         * @param interval the new motion check interval (ms)
377         * @see #DEFAULT_INTERVAL
378         */
379        public void setInterval(int interval) {
380
381                if (interval < 100) {
382                        throw new IllegalArgumentException("Motion check interval cannot be less than 100 ms");
383                }
384
385                this.interval = interval;
386        }
387
388        /**
389         * Set pixel intensity difference threshold above which pixel is classified
390         * as "moved". Minimum value is 0 and maximum is 255. Default value is 10.
391         * This value is equal for all RGB components difference.
392         * 
393         * @param threshold the pixel intensity difference threshold
394         * @see #DEFAULT_PIXEL_THREASHOLD
395         */
396        public void setPixelThreshold(int threshold) {
397                if (threshold < 0) {
398                        throw new IllegalArgumentException("Pixel intensity threshold cannot be negative!");
399                }
400                if (threshold > 255) {
401                        throw new IllegalArgumentException("Pixel intensity threshold cannot be higher than 255!");
402                }
403                this.pixelThreshold = threshold;
404        }
405
406        /**
407         * Set percentage fraction of detected motion area threshold above which it
408         * is classified as "moved". Minimum value for this is 0 and maximum is 100,
409         * which corresponds to full image covered by spontaneous motion.
410         * 
411         * @param threshold the percentage fraction of image area
412         * @see #DEFAULT_AREA_THREASHOLD
413         */
414        public void setAreaThreshold(double threshold) {
415                if (threshold < 0) {
416                        throw new IllegalArgumentException("Area fraction threshold cannot be negative!");
417                }
418                if (threshold > 100) {
419                        throw new IllegalArgumentException("Area fraction threshold cannot be higher than 100!");
420                }
421                this.areaThreshold = threshold;
422        }
423
424        /**
425         * Set motion inertia (time when motion is valid). If no value specified
426         * this is set to 2 * interval. To reset to default value,
427         * {@link #clearInertia()} method must be used.
428         * 
429         * @param inertia the motion inertia time in milliseconds
430         * @see #clearInertia()
431         */
432        public void setInertia(int inertia) {
433                if (inertia < 0) {
434                        throw new IllegalArgumentException("Inertia time must not be negative!");
435                }
436                this.inertia = inertia;
437        }
438
439        /**
440         * Reset inertia time to value calculated automatically on the base of
441         * interval. This value will be set to 2 * interval.
442         */
443        public void clearInertia() {
444                this.inertia = -1;
445        }
446
447        /**
448         * Get attached webcam object.
449         * 
450         * @return Attached webcam
451         */
452        public Webcam getWebcam() {
453                return webcam;
454        }
455
456        public boolean isMotion() {
457                if (!running.get()) {
458                        LOG.warn("Motion cannot be detected when detector is not running!");
459                }
460                return motion.get();
461        }
462
463        /**
464         * Get percentage fraction of image covered by motion. 0 means no motion on
465         * image and 100 means full image covered by spontaneous motion.
466         * 
467         * @return Return percentage image fraction covered by motion
468         */
469        public double getMotionArea() {
470                return area;
471        }
472
473        /**
474         * Get motion center of gravity. When no motion is detected this value
475         * points to the image center.
476         * 
477         * @return Center of gravity point
478         */
479        public Point getMotionCog() {
480                return cog;
481        }
482
483        private static int combinePixels(int rgb1, int rgb2) {
484
485                // first ARGB
486
487                int a1 = (rgb1 >> 24) & 0xff;
488                int r1 = (rgb1 >> 16) & 0xff;
489                int g1 = (rgb1 >> 8) & 0xff;
490                int b1 = rgb1 & 0xff;
491
492                // second ARGB
493
494                int a2 = (rgb2 >> 24) & 0xff;
495                int r2 = (rgb2 >> 16) & 0xff;
496                int g2 = (rgb2 >> 8) & 0xff;
497                int b2 = rgb2 & 0xff;
498
499                r1 = clamp(Math.abs(r1 - r2));
500                g1 = clamp(Math.abs(g1 - g2));
501                b1 = clamp(Math.abs(b1 - b2));
502
503                // in case if alpha is enabled (translucent image)
504
505                if (a1 != 0xff) {
506                        a1 = a1 * 0xff / 255;
507                        int a3 = (255 - a1) * a2 / 255;
508                        r1 = clamp((r1 * a1 + r2 * a3) / 255);
509                        g1 = clamp((g1 * a1 + g2 * a3) / 255);
510                        b1 = clamp((b1 * a1 + b2 * a3) / 255);
511                        a1 = clamp(a1 + a3);
512                }
513
514                return (a1 << 24) | (r1 << 16) | (g1 << 8) | b1;
515        }
516
517        /**
518         * Clamp a value to the range 0..255
519         */
520        private static int clamp(int c) {
521                if (c < 0) {
522                        return 0;
523                }
524                if (c > 255) {
525                        return 255;
526                }
527                return c;
528        }
529
530}