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 }