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 500 ms.
049         */
050        public static final int DEFAULT_INTERVAL = 500;
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 = 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 volatile boolean motion = 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 interval the check interval
211         */
212        public WebcamMotionDetector(Webcam webcam, int pixelThreshold, double areaThreshold, int interval) {
213
214                this.webcam = webcam;
215
216                setPixelThreshold(pixelThreshold);
217                setAreaThreshold(areaThreshold);
218                setInterval(interval);
219
220                int w = webcam.getViewSize().width;
221                int h = webcam.getViewSize().height;
222
223                cog = new Point(w / 2, h / 2);
224        }
225
226        /**
227         * Create motion detector with default parameter inertia = 0.
228         *
229         * @param webcam web camera instance
230         * @param pixelThreshold intensity threshold (0 - 255)
231         * @param areaThreshold percentage threshold of image covered by motion (0 - 100)
232         */
233        public WebcamMotionDetector(Webcam webcam, int pixelThreshold, double areaThreshold) {
234                this(webcam, pixelThreshold, areaThreshold, DEFAULT_INTERVAL);
235        }
236
237        /**
238         * Create motion detector with default parameter inertia = 0.
239         *
240         * @param webcam web camera instance
241         * @param pixelThreshold intensity threshold (0 - 255)
242         */
243        public WebcamMotionDetector(Webcam webcam, int pixelThreshold) {
244                this(webcam, pixelThreshold, DEFAULT_AREA_THREASHOLD);
245        }
246
247        /**
248         * Create motion detector with default parameters - threshold = 25, inertia = 0.
249         *
250         * @param webcam web camera instance
251         */
252        public WebcamMotionDetector(Webcam webcam) {
253                this(webcam, DEFAULT_PIXEL_THREASHOLD);
254        }
255
256        public void start() {
257                if (running.compareAndSet(false, true)) {
258                        webcam.open();
259                        executor.submit(new Runner());
260                        executor.submit(new Inverter());
261                }
262        }
263
264        public void stop() {
265                if (running.compareAndSet(true, false)) {
266                        webcam.close();
267                        executor.shutdownNow();
268                }
269        }
270
271        protected void detect() {
272
273                if (!webcam.isOpen()) {
274                        motion = false;
275                        return;
276                }
277
278                BufferedImage current = webcam.getImage();
279
280                if (current == null) {
281                        motion = false;
282                        return;
283                }
284
285                current = blur.filter(current, null);
286                current = gray.filter(current, null);
287
288                int p = 0;
289
290                int cogX = 0;
291                int cogY = 0;
292
293                int w = current.getWidth();
294                int h = current.getHeight();
295
296                if (previous != null) {
297                        for (int x = 0; x < w; x++) {
298                                for (int y = 0; y < h; y++) {
299
300                                        int cpx = current.getRGB(x, y);
301                                        int ppx = previous.getRGB(x, y);
302                                        int pid = combinePixels(cpx, ppx) & 0x000000ff;
303
304                                        if (pid >= pixelThreshold) {
305                                                cogX += x;
306                                                cogY += y;
307                                                p += 1;
308                                        }
309                                }
310                        }
311                }
312
313                area = p * 100d / (w * h);
314
315                if (area >= areaThreshold) {
316
317                        cog = new Point(cogX / p, cogY / p);
318
319                        motion = true;
320                        lastMotionTimestamp = System.currentTimeMillis();
321
322                        notifyMotionListeners();
323
324                } else {
325                        cog = new Point(w / 2, h / 2);
326                }
327
328                previous = current;
329        }
330
331        /**
332         * Will notify all attached motion listeners.
333         */
334        private void notifyMotionListeners() {
335                WebcamMotionEvent wme = new WebcamMotionEvent(this, area, cog);
336                for (WebcamMotionListener l : listeners) {
337                        try {
338                                l.motionDetected(wme);
339                        } catch (Exception e) {
340                                WebcamExceptionHandler.handle(e);
341                        }
342                }
343        }
344
345        /**
346         * Add motion listener.
347         *
348         * @param l listener to add
349         * @return true if listeners list has been changed, false otherwise
350         */
351        public boolean addMotionListener(WebcamMotionListener l) {
352                return listeners.add(l);
353        }
354
355        /**
356         * @return All motion listeners as array
357         */
358        public WebcamMotionListener[] getMotionListeners() {
359                return listeners.toArray(new WebcamMotionListener[listeners.size()]);
360        }
361
362        /**
363         * Removes motion listener.
364         *
365         * @param l motion listener to remove
366         * @return true if listener was available on the list, false otherwise
367         */
368        public boolean removeMotionListener(WebcamMotionListener l) {
369                return listeners.remove(l);
370        }
371
372        /**
373         * @return Motion check interval in milliseconds
374         */
375        public int getInterval() {
376                return interval;
377        }
378
379        /**
380         * Motion check interval in milliseconds. After motion is detected, it's valid for time which is
381         * equal to value of 2 * interval.
382         *
383         * @param interval the new motion check interval (ms)
384         * @see #DEFAULT_INTERVAL
385         */
386        public void setInterval(int interval) {
387
388                if (interval < 100) {
389                        throw new IllegalArgumentException("Motion check interval cannot be less than 100 ms");
390                }
391
392                this.interval = interval;
393        }
394
395        /**
396         * Set pixel intensity difference threshold above which pixel is classified as "moved". Minimum
397         * value is 0 and maximum is 255. Default value is 10. This value is equal for all RGB
398         * components difference.
399         *
400         * @param threshold the pixel intensity difference threshold
401         * @see #DEFAULT_PIXEL_THREASHOLD
402         */
403        public void setPixelThreshold(int threshold) {
404                if (threshold < 0) {
405                        throw new IllegalArgumentException("Pixel intensity threshold cannot be negative!");
406                }
407                if (threshold > 255) {
408                        throw new IllegalArgumentException("Pixel intensity threshold cannot be higher than 255!");
409                }
410                this.pixelThreshold = threshold;
411        }
412
413        /**
414         * Set percentage fraction of detected motion area threshold above which it is classified as
415         * "moved". Minimum value for this is 0 and maximum is 100, which corresponds to full image
416         * covered by spontaneous motion.
417         *
418         * @param threshold the percentage fraction of image area
419         * @see #DEFAULT_AREA_THREASHOLD
420         */
421        public void setAreaThreshold(double threshold) {
422                if (threshold < 0) {
423                        throw new IllegalArgumentException("Area fraction threshold cannot be negative!");
424                }
425                if (threshold > 100) {
426                        throw new IllegalArgumentException("Area fraction threshold cannot be higher than 100!");
427                }
428                this.areaThreshold = threshold;
429        }
430
431        /**
432         * Set motion inertia (time when motion is valid). If no value specified this is set to 2 *
433         * interval. To reset to default value, {@link #clearInertia()} method must be used.
434         *
435         * @param inertia the motion inertia time in milliseconds
436         * @see #clearInertia()
437         */
438        public void setInertia(int inertia) {
439                if (inertia < 0) {
440                        throw new IllegalArgumentException("Inertia time must not be negative!");
441                }
442                this.inertia = inertia;
443        }
444
445        /**
446         * Reset inertia time to value calculated automatically on the base of interval. This value will
447         * be set to 2 * interval.
448         */
449        public void clearInertia() {
450                this.inertia = -1;
451        }
452
453        /**
454         * Get attached webcam object.
455         *
456         * @return Attached webcam
457         */
458        public Webcam getWebcam() {
459                return webcam;
460        }
461
462        public boolean isMotion() {
463                if (!running.get()) {
464                        LOG.warn("Motion cannot be detected when detector is not running!");
465                }
466                return motion;
467        }
468
469        /**
470         * Get percentage fraction of image covered by motion. 0 means no motion on image and 100 means
471         * full image covered by spontaneous motion.
472         *
473         * @return Return percentage image fraction covered by motion
474         */
475        public double getMotionArea() {
476                return area;
477        }
478
479        /**
480         * Get motion center of gravity. When no motion is detected this value points to the image
481         * center.
482         *
483         * @return Center of gravity point
484         */
485        public Point getMotionCog() {
486                return cog;
487        }
488
489        private static int combinePixels(int rgb1, int rgb2) {
490
491                // first ARGB
492
493                int a1 = (rgb1 >> 24) & 0xff;
494                int r1 = (rgb1 >> 16) & 0xff;
495                int g1 = (rgb1 >> 8) & 0xff;
496                int b1 = rgb1 & 0xff;
497
498                // second ARGB
499
500                int a2 = (rgb2 >> 24) & 0xff;
501                int r2 = (rgb2 >> 16) & 0xff;
502                int g2 = (rgb2 >> 8) & 0xff;
503                int b2 = rgb2 & 0xff;
504
505                r1 = clamp(Math.abs(r1 - r2));
506                g1 = clamp(Math.abs(g1 - g2));
507                b1 = clamp(Math.abs(b1 - b2));
508
509                // in case if alpha is enabled (translucent image)
510
511                if (a1 != 0xff) {
512                        a1 = a1 * 0xff / 255;
513                        int a3 = (255 - a1) * a2 / 255;
514                        r1 = clamp((r1 * a1 + r2 * a3) / 255);
515                        g1 = clamp((g1 * a1 + g2 * a3) / 255);
516                        b1 = clamp((b1 * a1 + b2 * a3) / 255);
517                        a1 = clamp(a1 + a3);
518                }
519
520                return (a1 << 24) | (r1 << 16) | (g1 << 8) | b1;
521        }
522
523        /**
524         * Clamp a value to the range 0..255
525         */
526        private static int clamp(int c) {
527                if (c < 0) {
528                        return 0;
529                }
530                if (c > 255) {
531                        return 255;
532                }
533                return c;
534        }
535
536}