001package com.github.sarxos.webcam; 002 003import java.awt.Point; 004import java.awt.image.BufferedImage; 005import java.util.ArrayList; 006import java.util.List; 007import java.util.concurrent.ExecutorService; 008import java.util.concurrent.Executors; 009import java.util.concurrent.ThreadFactory; 010import java.util.concurrent.atomic.AtomicBoolean; 011import java.util.concurrent.atomic.AtomicInteger; 012 013import org.slf4j.Logger; 014import org.slf4j.LoggerFactory; 015 016import com.github.sarxos.webcam.util.jh.JHBlurFilter; 017import com.github.sarxos.webcam.util.jh.JHGrayFilter; 018 019 020/** 021 * Webcam motion detector. 022 * 023 * @author Bartosz Firyn (SarXos) 024 */ 025public class WebcamMotionDetector { 026 027 /** 028 * Logger. 029 */ 030 private static final Logger LOG = LoggerFactory.getLogger(WebcamMotionDetector.class); 031 032 /** 033 * Thread number in pool. 034 */ 035 private static final AtomicInteger NT = new AtomicInteger(0); 036 037 /** 038 * Thread factory. 039 */ 040 private static final ThreadFactory THREAD_FACTORY = new DetectorThreadFactory(); 041 042 /** 043 * Default pixel difference intensity threshold (set to 25). 044 */ 045 public static final int DEFAULT_PIXEL_THREASHOLD = 25; 046 047 /** 048 * Default check interval, in milliseconds, set to 500 ms. 049 */ 050 public static final int DEFAULT_INTERVAL = 500; 051 052 /** 053 * Default percentage image area fraction threshold (set to 0.2%). 054 */ 055 public static final double DEFAULT_AREA_THREASHOLD = 0.2; 056 057 /** 058 * Create new threads for detector internals. 059 * 060 * @author Bartosz Firyn (SarXos) 061 */ 062 private static final class DetectorThreadFactory implements ThreadFactory { 063 064 @Override 065 public Thread newThread(Runnable runnable) { 066 Thread t = new Thread(runnable, String.format("motion-detector-%d", NT.incrementAndGet())); 067 t.setUncaughtExceptionHandler(WebcamExceptionHandler.getInstance()); 068 t.setDaemon(true); 069 return t; 070 } 071 } 072 073 /** 074 * Run motion detector. 075 * 076 * @author Bartosz Firyn (SarXos) 077 */ 078 private class Runner implements Runnable { 079 080 @Override 081 public void run() { 082 083 running.set(true); 084 085 while (running.get() && webcam.isOpen()) { 086 try { 087 detect(); 088 Thread.sleep(interval); 089 } catch (InterruptedException e) { 090 break; 091 } catch (Exception e) { 092 WebcamExceptionHandler.handle(e); 093 } 094 } 095 096 running.set(false); 097 } 098 } 099 100 /** 101 * Change motion to false after specified number of seconds. 102 * 103 * @author Bartosz Firyn (SarXos) 104 */ 105 private class Inverter implements Runnable { 106 107 @Override 108 public void run() { 109 110 int delay = 0; 111 112 while (running.get()) { 113 114 try { 115 Thread.sleep(10); 116 } catch (InterruptedException e) { 117 break; 118 } 119 120 delay = inertia != -1 ? inertia : 2 * interval; 121 122 if (lastMotionTimestamp + delay < System.currentTimeMillis()) { 123 motion = false; 124 } 125 } 126 } 127 } 128 129 /** 130 * Executor. 131 */ 132 private final ExecutorService executor = Executors.newFixedThreadPool(2, THREAD_FACTORY); 133 134 /** 135 * Motion listeners. 136 */ 137 private final List<WebcamMotionListener> listeners = new ArrayList<WebcamMotionListener>(); 138 139 /** 140 * Is detector running? 141 */ 142 private final AtomicBoolean running = new AtomicBoolean(false); 143 144 /** 145 * Is motion? 146 */ 147 private volatile boolean motion = false; 148 149 /** 150 * Previously captured image. 151 */ 152 private BufferedImage previous = null; 153 154 /** 155 * Webcam to be used to detect motion. 156 */ 157 private Webcam webcam = null; 158 159 /** 160 * Motion check interval (1000 ms by default). 161 */ 162 private volatile int interval = DEFAULT_INTERVAL; 163 164 /** 165 * Pixel intensity threshold (0 - 255). 166 */ 167 private volatile int pixelThreshold = DEFAULT_PIXEL_THREASHOLD; 168 169 /** 170 * Pixel intensity threshold (0 - 100). 171 */ 172 private volatile double areaThreshold = DEFAULT_AREA_THREASHOLD; 173 174 /** 175 * How long motion is valid (in milliseconds). Default value is 2 seconds. 176 */ 177 private volatile int inertia = -1; 178 179 /** 180 * Motion strength (0 = no motion, 100 = full image covered by motion). 181 */ 182 private double area = 0; 183 184 /** 185 * Center of motion gravity. 186 */ 187 private Point cog = null; 188 189 /** 190 * Timestamp when motion has been observed last time. 191 */ 192 private volatile long lastMotionTimestamp = 0; 193 194 /** 195 * Blur filter instance. 196 */ 197 private final JHBlurFilter blur = new JHBlurFilter(6, 6, 1); 198 199 /** 200 * Gray filter instance. 201 */ 202 private final JHGrayFilter gray = new JHGrayFilter(); 203 204 /** 205 * Create motion detector. Will open webcam if it is closed. 206 * 207 * @param webcam web camera instance 208 * @param pixelThreshold intensity threshold (0 - 255) 209 * @param areaThreshold percentage threshold of image covered by motion 210 * @param interval the check interval 211 */ 212 public WebcamMotionDetector(Webcam webcam, int pixelThreshold, double areaThreshold, int interval) { 213 214 this.webcam = webcam; 215 216 setPixelThreshold(pixelThreshold); 217 setAreaThreshold(areaThreshold); 218 setInterval(interval); 219 220 int w = webcam.getViewSize().width; 221 int h = webcam.getViewSize().height; 222 223 cog = new Point(w / 2, h / 2); 224 } 225 226 /** 227 * Create motion detector with default parameter inertia = 0. 228 * 229 * @param webcam web camera instance 230 * @param pixelThreshold intensity threshold (0 - 255) 231 * @param areaThreshold percentage threshold of image covered by motion (0 - 100) 232 */ 233 public WebcamMotionDetector(Webcam webcam, int pixelThreshold, double areaThreshold) { 234 this(webcam, pixelThreshold, areaThreshold, DEFAULT_INTERVAL); 235 } 236 237 /** 238 * Create motion detector with default parameter inertia = 0. 239 * 240 * @param webcam web camera instance 241 * @param pixelThreshold intensity threshold (0 - 255) 242 */ 243 public WebcamMotionDetector(Webcam webcam, int pixelThreshold) { 244 this(webcam, pixelThreshold, DEFAULT_AREA_THREASHOLD); 245 } 246 247 /** 248 * Create motion detector with default parameters - threshold = 25, inertia = 0. 249 * 250 * @param webcam web camera instance 251 */ 252 public WebcamMotionDetector(Webcam webcam) { 253 this(webcam, DEFAULT_PIXEL_THREASHOLD); 254 } 255 256 public void start() { 257 if (running.compareAndSet(false, true)) { 258 webcam.open(); 259 executor.submit(new Runner()); 260 executor.submit(new Inverter()); 261 } 262 } 263 264 public void stop() { 265 if (running.compareAndSet(true, false)) { 266 webcam.close(); 267 executor.shutdownNow(); 268 } 269 } 270 271 protected void detect() { 272 273 if (!webcam.isOpen()) { 274 motion = false; 275 return; 276 } 277 278 BufferedImage current = webcam.getImage(); 279 280 if (current == null) { 281 motion = false; 282 return; 283 } 284 285 current = blur.filter(current, null); 286 current = gray.filter(current, null); 287 288 int p = 0; 289 290 int cogX = 0; 291 int cogY = 0; 292 293 int w = current.getWidth(); 294 int h = current.getHeight(); 295 296 if (previous != null) { 297 for (int x = 0; x < w; x++) { 298 for (int y = 0; y < h; y++) { 299 300 int cpx = current.getRGB(x, y); 301 int ppx = previous.getRGB(x, y); 302 int pid = combinePixels(cpx, ppx) & 0x000000ff; 303 304 if (pid >= pixelThreshold) { 305 cogX += x; 306 cogY += y; 307 p += 1; 308 } 309 } 310 } 311 } 312 313 area = p * 100d / (w * h); 314 315 if (area >= areaThreshold) { 316 317 cog = new Point(cogX / p, cogY / p); 318 319 motion = true; 320 lastMotionTimestamp = System.currentTimeMillis(); 321 322 notifyMotionListeners(); 323 324 } else { 325 cog = new Point(w / 2, h / 2); 326 } 327 328 previous = current; 329 } 330 331 /** 332 * Will notify all attached motion listeners. 333 */ 334 private void notifyMotionListeners() { 335 WebcamMotionEvent wme = new WebcamMotionEvent(this, area, cog); 336 for (WebcamMotionListener l : listeners) { 337 try { 338 l.motionDetected(wme); 339 } catch (Exception e) { 340 WebcamExceptionHandler.handle(e); 341 } 342 } 343 } 344 345 /** 346 * Add motion listener. 347 * 348 * @param l listener to add 349 * @return true if listeners list has been changed, false otherwise 350 */ 351 public boolean addMotionListener(WebcamMotionListener l) { 352 return listeners.add(l); 353 } 354 355 /** 356 * @return All motion listeners as array 357 */ 358 public WebcamMotionListener[] getMotionListeners() { 359 return listeners.toArray(new WebcamMotionListener[listeners.size()]); 360 } 361 362 /** 363 * Removes motion listener. 364 * 365 * @param l motion listener to remove 366 * @return true if listener was available on the list, false otherwise 367 */ 368 public boolean removeMotionListener(WebcamMotionListener l) { 369 return listeners.remove(l); 370 } 371 372 /** 373 * @return Motion check interval in milliseconds 374 */ 375 public int getInterval() { 376 return interval; 377 } 378 379 /** 380 * Motion check interval in milliseconds. After motion is detected, it's valid for time which is 381 * equal to value of 2 * interval. 382 * 383 * @param interval the new motion check interval (ms) 384 * @see #DEFAULT_INTERVAL 385 */ 386 public void setInterval(int interval) { 387 388 if (interval < 100) { 389 throw new IllegalArgumentException("Motion check interval cannot be less than 100 ms"); 390 } 391 392 this.interval = interval; 393 } 394 395 /** 396 * Set pixel intensity difference threshold above which pixel is classified as "moved". Minimum 397 * value is 0 and maximum is 255. Default value is 10. This value is equal for all RGB 398 * components difference. 399 * 400 * @param threshold the pixel intensity difference threshold 401 * @see #DEFAULT_PIXEL_THREASHOLD 402 */ 403 public void setPixelThreshold(int threshold) { 404 if (threshold < 0) { 405 throw new IllegalArgumentException("Pixel intensity threshold cannot be negative!"); 406 } 407 if (threshold > 255) { 408 throw new IllegalArgumentException("Pixel intensity threshold cannot be higher than 255!"); 409 } 410 this.pixelThreshold = threshold; 411 } 412 413 /** 414 * Set percentage fraction of detected motion area threshold above which it is classified as 415 * "moved". Minimum value for this is 0 and maximum is 100, which corresponds to full image 416 * covered by spontaneous motion. 417 * 418 * @param threshold the percentage fraction of image area 419 * @see #DEFAULT_AREA_THREASHOLD 420 */ 421 public void setAreaThreshold(double threshold) { 422 if (threshold < 0) { 423 throw new IllegalArgumentException("Area fraction threshold cannot be negative!"); 424 } 425 if (threshold > 100) { 426 throw new IllegalArgumentException("Area fraction threshold cannot be higher than 100!"); 427 } 428 this.areaThreshold = threshold; 429 } 430 431 /** 432 * Set motion inertia (time when motion is valid). If no value specified this is set to 2 * 433 * interval. To reset to default value, {@link #clearInertia()} method must be used. 434 * 435 * @param inertia the motion inertia time in milliseconds 436 * @see #clearInertia() 437 */ 438 public void setInertia(int inertia) { 439 if (inertia < 0) { 440 throw new IllegalArgumentException("Inertia time must not be negative!"); 441 } 442 this.inertia = inertia; 443 } 444 445 /** 446 * Reset inertia time to value calculated automatically on the base of interval. This value will 447 * be set to 2 * interval. 448 */ 449 public void clearInertia() { 450 this.inertia = -1; 451 } 452 453 /** 454 * Get attached webcam object. 455 * 456 * @return Attached webcam 457 */ 458 public Webcam getWebcam() { 459 return webcam; 460 } 461 462 public boolean isMotion() { 463 if (!running.get()) { 464 LOG.warn("Motion cannot be detected when detector is not running!"); 465 } 466 return motion; 467 } 468 469 /** 470 * Get percentage fraction of image covered by motion. 0 means no motion on image and 100 means 471 * full image covered by spontaneous motion. 472 * 473 * @return Return percentage image fraction covered by motion 474 */ 475 public double getMotionArea() { 476 return area; 477 } 478 479 /** 480 * Get motion center of gravity. When no motion is detected this value points to the image 481 * center. 482 * 483 * @return Center of gravity point 484 */ 485 public Point getMotionCog() { 486 return cog; 487 } 488 489 private static int combinePixels(int rgb1, int rgb2) { 490 491 // first ARGB 492 493 int a1 = (rgb1 >> 24) & 0xff; 494 int r1 = (rgb1 >> 16) & 0xff; 495 int g1 = (rgb1 >> 8) & 0xff; 496 int b1 = rgb1 & 0xff; 497 498 // second ARGB 499 500 int a2 = (rgb2 >> 24) & 0xff; 501 int r2 = (rgb2 >> 16) & 0xff; 502 int g2 = (rgb2 >> 8) & 0xff; 503 int b2 = rgb2 & 0xff; 504 505 r1 = clamp(Math.abs(r1 - r2)); 506 g1 = clamp(Math.abs(g1 - g2)); 507 b1 = clamp(Math.abs(b1 - b2)); 508 509 // in case if alpha is enabled (translucent image) 510 511 if (a1 != 0xff) { 512 a1 = a1 * 0xff / 255; 513 int a3 = (255 - a1) * a2 / 255; 514 r1 = clamp((r1 * a1 + r2 * a3) / 255); 515 g1 = clamp((g1 * a1 + g2 * a3) / 255); 516 b1 = clamp((b1 * a1 + b2 * a3) / 255); 517 a1 = clamp(a1 + a3); 518 } 519 520 return (a1 << 24) | (r1 << 16) | (g1 << 8) | b1; 521 } 522 523 /** 524 * Clamp a value to the range 0..255 525 */ 526 private static int clamp(int c) { 527 if (c < 0) { 528 return 0; 529 } 530 if (c > 255) { 531 return 255; 532 } 533 return c; 534 } 535 536}