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    }