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 }