Skip to content

Commit 646f426

Browse files
committed
Merge pull request #312 from krok32/master
Customizable WebcamMotionDetector
2 parents 0b96dd2 + fd1c944 commit 646f426

File tree

4 files changed

+307
-160
lines changed

4 files changed

+307
-160
lines changed

webcam-capture/src/main/java/com/github/sarxos/webcam/WebcamMotionDetector.java

Lines changed: 51 additions & 159 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,6 @@
1313
import org.slf4j.Logger;
1414
import org.slf4j.LoggerFactory;
1515

16-
import com.github.sarxos.webcam.util.jh.JHBlurFilter;
17-
import com.github.sarxos.webcam.util.jh.JHGrayFilter;
18-
1916

2017
/**
2118
* Webcam motion detector.
@@ -39,21 +36,11 @@ public class WebcamMotionDetector {
3936
*/
4037
private static final ThreadFactory THREAD_FACTORY = new DetectorThreadFactory();
4138

42-
/**
43-
* Default pixel difference intensity threshold (set to 25).
44-
*/
45-
public static final int DEFAULT_PIXEL_THREASHOLD = 25;
46-
4739
/**
4840
* Default check interval, in milliseconds, set to 500 ms.
4941
*/
5042
public static final int DEFAULT_INTERVAL = 500;
5143

52-
/**
53-
* Default percentage image area fraction threshold (set to 0.2%).
54-
*/
55-
public static final double DEFAULT_AREA_THREASHOLD = 0.2;
56-
5744
/**
5845
* Create new threads for detector internals.
5946
*
@@ -166,65 +153,50 @@ public void run() {
166153
*/
167154
private volatile int interval = DEFAULT_INTERVAL;
168155

169-
/**
170-
* Pixel intensity threshold (0 - 255).
171-
*/
172-
private volatile int pixelThreshold = DEFAULT_PIXEL_THREASHOLD;
173-
174-
/**
175-
* Pixel intensity threshold (0 - 100).
176-
*/
177-
private volatile double areaThreshold = DEFAULT_AREA_THREASHOLD;
178-
179156
/**
180157
* How long motion is valid (in milliseconds). Default value is 2 seconds.
181158
*/
182159
private volatile int inertia = -1;
183160

184-
/**
185-
* Motion strength (0 = no motion, 100 = full image covered by motion).
186-
*/
187-
private double area = 0;
188-
189-
/**
190-
* Center of motion gravity.
191-
*/
192-
private Point cog = new Point(0, 0);
193-
194161
/**
195162
* Timestamp when motion has been observed last time.
196163
*/
197164
private volatile long lastMotionTimestamp = 0;
198165

199166
/**
200-
* Blur filter instance.
167+
* Implementation of motion detection algorithm.
201168
*/
202-
private final JHBlurFilter blur = new JHBlurFilter(6, 6, 1);
169+
private final WebcamMotionDetectorAlgorithm detectorAlgorithm;
203170

204171
/**
205-
* Gray filter instance.
172+
* Create motion detector. Will open webcam if it is closed.
173+
*
174+
* @param webcam web camera instance
175+
* @param motion detector algorithm implementation
176+
* @param interval the check interval
206177
*/
207-
private final JHGrayFilter gray = new JHGrayFilter();
208-
178+
public WebcamMotionDetector(Webcam webcam, WebcamMotionDetectorAlgorithm detectorAlgorithm, int interval) {
179+
this.webcam = webcam;
180+
this.detectorAlgorithm = detectorAlgorithm;
181+
setInterval(interval);
182+
}
183+
209184
/**
210-
* Create motion detector. Will open webcam if it is closed.
185+
* Create motion detector. Will open webcam if it is closed.
186+
* Uses WebcamMotionDetectorDefaultAlgorithm for motion detection.
211187
*
212188
* @param webcam web camera instance
213189
* @param pixelThreshold intensity threshold (0 - 255)
214190
* @param areaThreshold percentage threshold of image covered by motion
215191
* @param interval the check interval
216192
*/
217193
public WebcamMotionDetector(Webcam webcam, int pixelThreshold, double areaThreshold, int interval) {
218-
219-
this.webcam = webcam;
220-
221-
setPixelThreshold(pixelThreshold);
222-
setAreaThreshold(areaThreshold);
223-
setInterval(interval);
194+
this(webcam, new WebcamMotionDetectorDefaultAlgorithm(pixelThreshold, areaThreshold), interval);
224195
}
225196

226197
/**
227198
* Create motion detector with default parameter inertia = 0.
199+
* Uses WebcamMotionDetectorDefaultAlgorithm for motion detection.
228200
*
229201
* @param webcam web camera instance
230202
* @param pixelThreshold intensity threshold (0 - 255)
@@ -237,12 +209,13 @@ public WebcamMotionDetector(Webcam webcam, int pixelThreshold, double areaThresh
237209

238210
/**
239211
* Create motion detector with default parameter inertia = 0.
212+
* Uses WebcamMotionDetectorDefaultAlgorithm for motion detection.
240213
*
241214
* @param webcam web camera instance
242215
* @param pixelThreshold intensity threshold (0 - 255)
243216
*/
244217
public WebcamMotionDetector(Webcam webcam, int pixelThreshold) {
245-
this(webcam, pixelThreshold, DEFAULT_AREA_THREASHOLD);
218+
this(webcam, pixelThreshold, WebcamMotionDetectorDefaultAlgorithm.DEFAULT_AREA_THREASHOLD);
246219
}
247220

248221
/**
@@ -252,19 +225,12 @@ public WebcamMotionDetector(Webcam webcam, int pixelThreshold) {
252225
* @param webcam web camera instance
253226
*/
254227
public WebcamMotionDetector(Webcam webcam) {
255-
this(webcam, DEFAULT_PIXEL_THREASHOLD);
228+
this(webcam, WebcamMotionDetectorDefaultAlgorithm.DEFAULT_PIXEL_THREASHOLD);
256229
}
257230

258231
public void start() {
259232
if (running.compareAndSet(false, true)) {
260-
261233
webcam.open();
262-
263-
int w = webcam.getViewSize().width;
264-
int h = webcam.getViewSize().height;
265-
266-
cog = new Point(w / 2, h / 2);
267-
268234
executor.submit(new Runner());
269235
executor.submit(new Inverter());
270236
}
@@ -291,49 +257,16 @@ protected void detect() {
291257
return;
292258
}
293259

294-
BufferedImage currentModified = blur.filter(currentOriginal, null);
295-
currentModified = gray.filter(currentModified, null);
296-
297-
int p = 0;
298-
299-
int cogX = 0;
300-
int cogY = 0;
301-
302-
int w = currentModified.getWidth();
303-
int h = currentModified.getHeight();
304-
305-
if (previousModified != null) {
306-
for (int x = 0; x < w; x++) {
307-
for (int y = 0; y < h; y++) {
308-
309-
int cpx = currentModified.getRGB(x, y);
310-
int ppx = previousModified.getRGB(x, y);
311-
int pid = combinePixels(cpx, ppx) & 0x000000ff;
312-
313-
if (pid >= pixelThreshold) {
314-
cogX += x;
315-
cogY += y;
316-
p += 1;
317-
}
318-
}
319-
}
320-
}
321-
322-
area = p * 100d / (w * h);
323-
324-
if (area >= areaThreshold) {
325-
326-
cog = new Point(cogX / p, cogY / p);
260+
BufferedImage currentModified = detectorAlgorithm.prepareImage(currentOriginal);
261+
262+
boolean movementDetected = detectorAlgorithm.detect(previousModified, currentModified);
327263

264+
if (movementDetected) {
328265
motion = true;
329266
lastMotionTimestamp = System.currentTimeMillis();
330-
331267
notifyMotionListeners(currentOriginal);
332-
333-
} else {
334-
cog = new Point(w / 2, h / 2);
335268
}
336-
269+
337270
previousOriginal = currentOriginal;
338271
previousModified = currentModified;
339272
}
@@ -343,7 +276,7 @@ protected void detect() {
343276
* @param image with the motion detected
344277
*/
345278
private void notifyMotionListeners(BufferedImage currentOriginal) {
346-
WebcamMotionEvent wme = new WebcamMotionEvent(this, previousOriginal, currentOriginal, area, cog);
279+
WebcamMotionEvent wme = new WebcamMotionEvent(this, previousOriginal, currentOriginal, detectorAlgorithm.getArea(), detectorAlgorithm.getCog());
347280
for (WebcamMotionListener l : listeners) {
348281
try {
349282
l.motionDetected(wme);
@@ -404,39 +337,31 @@ public void setInterval(int interval) {
404337
}
405338

406339
/**
407-
* Set pixel intensity difference threshold above which pixel is classified
408-
* as "moved". Minimum value is 0 and maximum is 255. Default value is 10.
409-
* This value is equal for all RGB components difference.
410-
*
340+
* Sets pixelThreshold to the underlying detector algorithm, but only if the
341+
* algorithm is (or extends) WebcamMotionDetectorDefaultAlgorithm
342+
*
343+
* @see WebcamMotionDetectorDefaultAlgorithm#setPixelThreshold(int)
344+
*
411345
* @param threshold the pixel intensity difference threshold
412-
* @see #DEFAULT_PIXEL_THREASHOLD
413346
*/
414347
public void setPixelThreshold(int threshold) {
415-
if (threshold < 0) {
416-
throw new IllegalArgumentException("Pixel intensity threshold cannot be negative!");
348+
if (detectorAlgorithm instanceof WebcamMotionDetectorDefaultAlgorithm) {
349+
((WebcamMotionDetectorDefaultAlgorithm)detectorAlgorithm).setPixelThreshold(threshold);
417350
}
418-
if (threshold > 255) {
419-
throw new IllegalArgumentException("Pixel intensity threshold cannot be higher than 255!");
420-
}
421-
this.pixelThreshold = threshold;
422351
}
423352

424353
/**
425-
* Set percentage fraction of detected motion area threshold above which it
426-
* is classified as "moved". Minimum value for this is 0 and maximum is 100,
427-
* which corresponds to full image covered by spontaneous motion.
428-
*
354+
* Sets areaThreshold to the underlying detector algorithm, but only if the
355+
* algorithm is (or extends) WebcamMotionDetectorDefaultAlgorithm
356+
*
357+
* @see WebcamMotionDetectorDefaultAlgorithm#setAreaThreshold(double)
358+
*
429359
* @param threshold the percentage fraction of image area
430-
* @see #DEFAULT_AREA_THREASHOLD
431360
*/
432361
public void setAreaThreshold(double threshold) {
433-
if (threshold < 0) {
434-
throw new IllegalArgumentException("Area fraction threshold cannot be negative!");
362+
if (detectorAlgorithm instanceof WebcamMotionDetectorDefaultAlgorithm) {
363+
((WebcamMotionDetectorDefaultAlgorithm)detectorAlgorithm).setAreaThreshold(threshold);
435364
}
436-
if (threshold > 100) {
437-
throw new IllegalArgumentException("Area fraction threshold cannot be higher than 100!");
438-
}
439-
this.areaThreshold = threshold;
440365
}
441366

442367
/**
@@ -485,7 +410,7 @@ public boolean isMotion() {
485410
* @return Return percentage image fraction covered by motion
486411
*/
487412
public double getMotionArea() {
488-
return area;
413+
return detectorAlgorithm.getArea();
489414
}
490415

491416
/**
@@ -495,54 +420,21 @@ public double getMotionArea() {
495420
* @return Center of gravity point
496421
*/
497422
public Point getMotionCog() {
498-
return cog;
499-
}
500-
501-
private static int combinePixels(int rgb1, int rgb2) {
502-
503-
// first ARGB
504-
505-
int a1 = (rgb1 >> 24) & 0xff;
506-
int r1 = (rgb1 >> 16) & 0xff;
507-
int g1 = (rgb1 >> 8) & 0xff;
508-
int b1 = rgb1 & 0xff;
509-
510-
// second ARGB
511-
512-
int a2 = (rgb2 >> 24) & 0xff;
513-
int r2 = (rgb2 >> 16) & 0xff;
514-
int g2 = (rgb2 >> 8) & 0xff;
515-
int b2 = rgb2 & 0xff;
516-
517-
r1 = clamp(Math.abs(r1 - r2));
518-
g1 = clamp(Math.abs(g1 - g2));
519-
b1 = clamp(Math.abs(b1 - b2));
520-
521-
// in case if alpha is enabled (translucent image)
522-
523-
if (a1 != 0xff) {
524-
a1 = a1 * 0xff / 255;
525-
int a3 = (255 - a1) * a2 / 255;
526-
r1 = clamp((r1 * a1 + r2 * a3) / 255);
527-
g1 = clamp((g1 * a1 + g2 * a3) / 255);
528-
b1 = clamp((b1 * a1 + b2 * a3) / 255);
529-
a1 = clamp(a1 + a3);
423+
Point cog = detectorAlgorithm.getCog();
424+
if (cog == null) {
425+
// detectorAlgorithm hasn't been called so far - get image center
426+
int w = webcam.getViewSize().width;
427+
int h = webcam.getViewSize().height;
428+
cog = new Point(w / 2, h / 2);
530429
}
531-
532-
return (a1 << 24) | (r1 << 16) | (g1 << 8) | b1;
430+
return cog;
533431
}
534432

535433
/**
536-
* Clamp a value to the range 0..255
434+
* @return the detectorAlgorithm
537435
*/
538-
private static int clamp(int c) {
539-
if (c < 0) {
540-
return 0;
541-
}
542-
if (c > 255) {
543-
return 255;
544-
}
545-
return c;
436+
public WebcamMotionDetectorAlgorithm getDetectorAlgorithm() {
437+
return detectorAlgorithm;
546438
}
547439

548440
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package com.github.sarxos.webcam;
2+
3+
import java.awt.Point;
4+
import java.awt.image.BufferedImage;
5+
6+
7+
/**
8+
* Implementation of this interface is responsible for decision whether the
9+
* difference between two images represents movement or not. Instance may
10+
* specified as parameter of WebcamMotionDetector constructor, otherwise
11+
* WebcamMotionDetectorDefaultAlgorithm is used.
12+
*/
13+
public interface WebcamMotionDetectorAlgorithm {
14+
15+
/**
16+
* WebcamMotionDetector calls this method for each image used as parameter
17+
* of the method {@link #detect(BufferedImage, BufferedImage)}.
18+
* Implementation may transform the original image and prepare it for
19+
* comparison of two images.
20+
* May return the same instance if no there is no need to transform.
21+
*
22+
* @param original image
23+
* @return modified image
24+
*/
25+
BufferedImage prepareImage(BufferedImage original);
26+
27+
/**
28+
* Detects motion by comparison of the two specified images content.
29+
* {@link #prepareImage(BufferedImage)} method was called for both specified images.
30+
*
31+
* @param previousModified
32+
* @param currentModified
33+
* @return If the motion was detected returns true, otherwise returns false
34+
*/
35+
boolean detect(BufferedImage previousModified, BufferedImage currentModified);
36+
37+
/**
38+
* Get motion center of gravity. When no motion is detected this value
39+
* points to the image center.
40+
* May return null before the first movement check.
41+
*
42+
* @return Center of gravity point
43+
*/
44+
Point getCog();
45+
46+
/**
47+
* Get percentage fraction of image covered by motion. 0 means no motion on
48+
* image and 100 means full image covered by spontaneous motion.
49+
*
50+
* @return Return percentage image fraction covered by motion
51+
*/
52+
double getArea();
53+
}

0 commit comments

Comments
 (0)