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 1 second). 049 */ 050 public static final int DEFAULT_INTERVAL = 1000; 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.set(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 final AtomicBoolean motion = new AtomicBoolean(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 inertia for how long motion is valid (seconds) 211 * @param interval the check interval 212 */ 213 public WebcamMotionDetector(Webcam webcam, int pixelThreshold, double areaThreshold, int interval) { 214 215 this.webcam = webcam; 216 217 setPixelThreshold(pixelThreshold); 218 setAreaThreshold(areaThreshold); 219 setInterval(interval); 220 221 int w = webcam.getViewSize().width; 222 int h = webcam.getViewSize().height; 223 224 cog = new Point(w / 2, h / 2); 225 } 226 227 /** 228 * Create motion detector with default parameter inertia = 0. 229 * 230 * @param webcam web camera instance 231 * @param pixelThreshold intensity threshold (0 - 255) 232 * @param areaThreshol percentage threshold of image covered by motion 233 */ 234 public WebcamMotionDetector(Webcam webcam, int pixelThreshold, double areaThreshold) { 235 this(webcam, pixelThreshold, areaThreshold, DEFAULT_INTERVAL); 236 } 237 238 /** 239 * Create motion detector with default parameter inertia = 0. 240 * 241 * @param webcam web camera instance 242 * @param pixelThreshold intensity threshold (0 - 255) 243 */ 244 public WebcamMotionDetector(Webcam webcam, int pixelThreshold) { 245 this(webcam, pixelThreshold, DEFAULT_AREA_THREASHOLD); 246 } 247 248 /** 249 * Create motion detector with default parameters - threshold = 25, inertia 250 * = 0. 251 * 252 * @param webcam web camera instance 253 */ 254 public WebcamMotionDetector(Webcam webcam) { 255 this(webcam, DEFAULT_PIXEL_THREASHOLD); 256 } 257 258 public void start() { 259 if (running.compareAndSet(false, true)) { 260 webcam.open(); 261 executor.submit(new Runner()); 262 executor.submit(new Inverter()); 263 } 264 } 265 266 public void stop() { 267 if (running.compareAndSet(true, false)) { 268 webcam.close(); 269 executor.shutdownNow(); 270 } 271 } 272 273 protected void detect() { 274 275 BufferedImage current = webcam.getImage(); 276 277 current = blur.filter(current, null); 278 current = gray.filter(current, null); 279 280 int p = 0; 281 282 int cogX = 0; 283 int cogY = 0; 284 285 int w = current.getWidth(); 286 int h = current.getHeight(); 287 288 if (previous != null) { 289 for (int x = 0; x < w; x++) { 290 for (int y = 0; y < h; y++) { 291 292 int cpx = current.getRGB(x, y); 293 int ppx = previous.getRGB(x, y); 294 int pid = combinePixels(cpx, ppx) & 0x000000ff; 295 296 if (pid >= pixelThreshold) { 297 cogX += x; 298 cogY += y; 299 p += 1; 300 } 301 } 302 } 303 } 304 305 area = p * 100d / (w * h); 306 307 if (area >= areaThreshold) { 308 309 cog = new Point(cogX / p, cogY / p); 310 311 if (motion.compareAndSet(false, true)) { 312 lastMotionTimestamp = System.currentTimeMillis(); 313 } 314 315 notifyMotionListeners(); 316 317 } else { 318 cog = new Point(w / 2, h / 2); 319 } 320 321 previous = current; 322 } 323 324 /** 325 * Will notify all attached motion listeners. 326 */ 327 private void notifyMotionListeners() { 328 WebcamMotionEvent wme = new WebcamMotionEvent(this, area, cog); 329 for (WebcamMotionListener l : listeners) { 330 try { 331 l.motionDetected(wme); 332 } catch (Exception e) { 333 WebcamExceptionHandler.handle(e); 334 } 335 } 336 } 337 338 /** 339 * Add motion listener. 340 * 341 * @param l listener to add 342 * @return true if listeners list has been changed, false otherwise 343 */ 344 public boolean addMotionListener(WebcamMotionListener l) { 345 return listeners.add(l); 346 } 347 348 /** 349 * @return All motion listeners as array 350 */ 351 public WebcamMotionListener[] getMotionListeners() { 352 return listeners.toArray(new WebcamMotionListener[listeners.size()]); 353 } 354 355 /** 356 * Removes motion listener. 357 * 358 * @param l motion listener to remove 359 * @return true if listener was available on the list, false otherwise 360 */ 361 public boolean removeMotionListener(WebcamMotionListener l) { 362 return listeners.remove(l); 363 } 364 365 /** 366 * @return Motion check interval in milliseconds 367 */ 368 public int getInterval() { 369 return interval; 370 } 371 372 /** 373 * Motion check interval in milliseconds. After motion is detected, it's 374 * valid for time which is equal to value of 2 * interval. 375 * 376 * @param interval the new motion check interval (ms) 377 * @see #DEFAULT_INTERVAL 378 */ 379 public void setInterval(int interval) { 380 381 if (interval < 100) { 382 throw new IllegalArgumentException("Motion check interval cannot be less than 100 ms"); 383 } 384 385 this.interval = interval; 386 } 387 388 /** 389 * Set pixel intensity difference threshold above which pixel is classified 390 * as "moved". Minimum value is 0 and maximum is 255. Default value is 10. 391 * This value is equal for all RGB components difference. 392 * 393 * @param threshold the pixel intensity difference threshold 394 * @see #DEFAULT_PIXEL_THREASHOLD 395 */ 396 public void setPixelThreshold(int threshold) { 397 if (threshold < 0) { 398 throw new IllegalArgumentException("Pixel intensity threshold cannot be negative!"); 399 } 400 if (threshold > 255) { 401 throw new IllegalArgumentException("Pixel intensity threshold cannot be higher than 255!"); 402 } 403 this.pixelThreshold = threshold; 404 } 405 406 /** 407 * Set percentage fraction of detected motion area threshold above which it 408 * is classified as "moved". Minimum value for this is 0 and maximum is 100, 409 * which corresponds to full image covered by spontaneous motion. 410 * 411 * @param threshold the percentage fraction of image area 412 * @see #DEFAULT_AREA_THREASHOLD 413 */ 414 public void setAreaThreshold(double threshold) { 415 if (threshold < 0) { 416 throw new IllegalArgumentException("Area fraction threshold cannot be negative!"); 417 } 418 if (threshold > 100) { 419 throw new IllegalArgumentException("Area fraction threshold cannot be higher than 100!"); 420 } 421 this.areaThreshold = threshold; 422 } 423 424 /** 425 * Set motion inertia (time when motion is valid). If no value specified 426 * this is set to 2 * interval. To reset to default value, 427 * {@link #clearInertia()} method must be used. 428 * 429 * @param inertia the motion inertia time in milliseconds 430 * @see #clearInertia() 431 */ 432 public void setInertia(int inertia) { 433 if (inertia < 0) { 434 throw new IllegalArgumentException("Inertia time must not be negative!"); 435 } 436 this.inertia = inertia; 437 } 438 439 /** 440 * Reset inertia time to value calculated automatically on the base of 441 * interval. This value will be set to 2 * interval. 442 */ 443 public void clearInertia() { 444 this.inertia = -1; 445 } 446 447 /** 448 * Get attached webcam object. 449 * 450 * @return Attached webcam 451 */ 452 public Webcam getWebcam() { 453 return webcam; 454 } 455 456 public boolean isMotion() { 457 if (!running.get()) { 458 LOG.warn("Motion cannot be detected when detector is not running!"); 459 } 460 return motion.get(); 461 } 462 463 /** 464 * Get percentage fraction of image covered by motion. 0 means no motion on 465 * image and 100 means full image covered by spontaneous motion. 466 * 467 * @return Return percentage image fraction covered by motion 468 */ 469 public double getMotionArea() { 470 return area; 471 } 472 473 /** 474 * Get motion center of gravity. When no motion is detected this value 475 * points to the image center. 476 * 477 * @return Center of gravity point 478 */ 479 public Point getMotionCog() { 480 return cog; 481 } 482 483 private static int combinePixels(int rgb1, int rgb2) { 484 485 // first ARGB 486 487 int a1 = (rgb1 >> 24) & 0xff; 488 int r1 = (rgb1 >> 16) & 0xff; 489 int g1 = (rgb1 >> 8) & 0xff; 490 int b1 = rgb1 & 0xff; 491 492 // second ARGB 493 494 int a2 = (rgb2 >> 24) & 0xff; 495 int r2 = (rgb2 >> 16) & 0xff; 496 int g2 = (rgb2 >> 8) & 0xff; 497 int b2 = rgb2 & 0xff; 498 499 r1 = clamp(Math.abs(r1 - r2)); 500 g1 = clamp(Math.abs(g1 - g2)); 501 b1 = clamp(Math.abs(b1 - b2)); 502 503 // in case if alpha is enabled (translucent image) 504 505 if (a1 != 0xff) { 506 a1 = a1 * 0xff / 255; 507 int a3 = (255 - a1) * a2 / 255; 508 r1 = clamp((r1 * a1 + r2 * a3) / 255); 509 g1 = clamp((g1 * a1 + g2 * a3) / 255); 510 b1 = clamp((b1 * a1 + b2 * a3) / 255); 511 a1 = clamp(a1 + a3); 512 } 513 514 return (a1 << 24) | (r1 << 16) | (g1 << 8) | b1; 515 } 516 517 /** 518 * Clamp a value to the range 0..255 519 */ 520 private static int clamp(int c) { 521 if (c < 0) { 522 return 0; 523 } 524 if (c > 255) { 525 return 255; 526 } 527 return c; 528 } 529 530}