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