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    }