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 }