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 }