diff --git a/webcam-capture/src/main/java/com/github/sarxos/webcam/WebcamMotionDetector.java b/webcam-capture/src/main/java/com/github/sarxos/webcam/WebcamMotionDetector.java index 1eebdf11..e991a8d6 100644 --- a/webcam-capture/src/main/java/com/github/sarxos/webcam/WebcamMotionDetector.java +++ b/webcam-capture/src/main/java/com/github/sarxos/webcam/WebcamMotionDetector.java @@ -13,9 +13,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.github.sarxos.webcam.util.jh.JHBlurFilter; -import com.github.sarxos.webcam.util.jh.JHGrayFilter; - /** * Webcam motion detector. @@ -39,21 +36,11 @@ public class WebcamMotionDetector { */ private static final ThreadFactory THREAD_FACTORY = new DetectorThreadFactory(); - /** - * Default pixel difference intensity threshold (set to 25). - */ - public static final int DEFAULT_PIXEL_THREASHOLD = 25; - /** * Default check interval, in milliseconds, set to 500 ms. */ public static final int DEFAULT_INTERVAL = 500; - /** - * Default percentage image area fraction threshold (set to 0.2%). - */ - public static final double DEFAULT_AREA_THREASHOLD = 0.2; - /** * Create new threads for detector internals. * @@ -166,48 +153,37 @@ public void run() { */ private volatile int interval = DEFAULT_INTERVAL; - /** - * Pixel intensity threshold (0 - 255). - */ - private volatile int pixelThreshold = DEFAULT_PIXEL_THREASHOLD; - - /** - * Pixel intensity threshold (0 - 100). - */ - private volatile double areaThreshold = DEFAULT_AREA_THREASHOLD; - /** * How long motion is valid (in milliseconds). Default value is 2 seconds. */ private volatile int inertia = -1; - /** - * Motion strength (0 = no motion, 100 = full image covered by motion). - */ - private double area = 0; - - /** - * Center of motion gravity. - */ - private Point cog = new Point(0, 0); - /** * Timestamp when motion has been observed last time. */ private volatile long lastMotionTimestamp = 0; /** - * Blur filter instance. + * Implementation of motion detection algorithm. */ - private final JHBlurFilter blur = new JHBlurFilter(6, 6, 1); + private final WebcamMotionDetectorAlgorithm detectorAlgorithm; /** - * Gray filter instance. + * Create motion detector. Will open webcam if it is closed. + * + * @param webcam web camera instance + * @param motion detector algorithm implementation + * @param interval the check interval */ - private final JHGrayFilter gray = new JHGrayFilter(); - + public WebcamMotionDetector(Webcam webcam, WebcamMotionDetectorAlgorithm detectorAlgorithm, int interval) { + this.webcam = webcam; + this.detectorAlgorithm = detectorAlgorithm; + setInterval(interval); + } + /** - * Create motion detector. Will open webcam if it is closed. + * Create motion detector. Will open webcam if it is closed. + * Uses WebcamMotionDetectorDefaultAlgorithm for motion detection. * * @param webcam web camera instance * @param pixelThreshold intensity threshold (0 - 255) @@ -215,16 +191,12 @@ public void run() { * @param interval the check interval */ public WebcamMotionDetector(Webcam webcam, int pixelThreshold, double areaThreshold, int interval) { - - this.webcam = webcam; - - setPixelThreshold(pixelThreshold); - setAreaThreshold(areaThreshold); - setInterval(interval); + this(webcam, new WebcamMotionDetectorDefaultAlgorithm(pixelThreshold, areaThreshold), interval); } /** * Create motion detector with default parameter inertia = 0. + * Uses WebcamMotionDetectorDefaultAlgorithm for motion detection. * * @param webcam web camera instance * @param pixelThreshold intensity threshold (0 - 255) @@ -237,12 +209,13 @@ public WebcamMotionDetector(Webcam webcam, int pixelThreshold, double areaThresh /** * Create motion detector with default parameter inertia = 0. + * Uses WebcamMotionDetectorDefaultAlgorithm for motion detection. * * @param webcam web camera instance * @param pixelThreshold intensity threshold (0 - 255) */ public WebcamMotionDetector(Webcam webcam, int pixelThreshold) { - this(webcam, pixelThreshold, DEFAULT_AREA_THREASHOLD); + this(webcam, pixelThreshold, WebcamMotionDetectorDefaultAlgorithm.DEFAULT_AREA_THREASHOLD); } /** @@ -252,19 +225,12 @@ public WebcamMotionDetector(Webcam webcam, int pixelThreshold) { * @param webcam web camera instance */ public WebcamMotionDetector(Webcam webcam) { - this(webcam, DEFAULT_PIXEL_THREASHOLD); + this(webcam, WebcamMotionDetectorDefaultAlgorithm.DEFAULT_PIXEL_THREASHOLD); } public void start() { if (running.compareAndSet(false, true)) { - webcam.open(); - - int w = webcam.getViewSize().width; - int h = webcam.getViewSize().height; - - cog = new Point(w / 2, h / 2); - executor.submit(new Runner()); executor.submit(new Inverter()); } @@ -291,49 +257,16 @@ protected void detect() { return; } - BufferedImage currentModified = blur.filter(currentOriginal, null); - currentModified = gray.filter(currentModified, null); - - int p = 0; - - int cogX = 0; - int cogY = 0; - - int w = currentModified.getWidth(); - int h = currentModified.getHeight(); - - if (previousModified != null) { - for (int x = 0; x < w; x++) { - for (int y = 0; y < h; y++) { - - int cpx = currentModified.getRGB(x, y); - int ppx = previousModified.getRGB(x, y); - int pid = combinePixels(cpx, ppx) & 0x000000ff; - - if (pid >= pixelThreshold) { - cogX += x; - cogY += y; - p += 1; - } - } - } - } - - area = p * 100d / (w * h); - - if (area >= areaThreshold) { - - cog = new Point(cogX / p, cogY / p); + BufferedImage currentModified = detectorAlgorithm.prepareImage(currentOriginal); + + boolean movementDetected = detectorAlgorithm.detect(previousModified, currentModified); + if (movementDetected) { motion = true; lastMotionTimestamp = System.currentTimeMillis(); - notifyMotionListeners(currentOriginal); - - } else { - cog = new Point(w / 2, h / 2); } - + previousOriginal = currentOriginal; previousModified = currentModified; } @@ -343,7 +276,7 @@ protected void detect() { * @param image with the motion detected */ private void notifyMotionListeners(BufferedImage currentOriginal) { - WebcamMotionEvent wme = new WebcamMotionEvent(this, previousOriginal, currentOriginal, area, cog); + WebcamMotionEvent wme = new WebcamMotionEvent(this, previousOriginal, currentOriginal, detectorAlgorithm.getArea(), detectorAlgorithm.getCog()); for (WebcamMotionListener l : listeners) { try { l.motionDetected(wme); @@ -404,39 +337,31 @@ public void setInterval(int interval) { } /** - * Set pixel intensity difference threshold above which pixel is classified - * as "moved". Minimum value is 0 and maximum is 255. Default value is 10. - * This value is equal for all RGB components difference. - * + * Sets pixelThreshold to the underlying detector algorithm, but only if the + * algorithm is (or extends) WebcamMotionDetectorDefaultAlgorithm + * + * @see WebcamMotionDetectorDefaultAlgorithm#setPixelThreshold(int) + * * @param threshold the pixel intensity difference threshold - * @see #DEFAULT_PIXEL_THREASHOLD */ public void setPixelThreshold(int threshold) { - if (threshold < 0) { - throw new IllegalArgumentException("Pixel intensity threshold cannot be negative!"); + if (detectorAlgorithm instanceof WebcamMotionDetectorDefaultAlgorithm) { + ((WebcamMotionDetectorDefaultAlgorithm)detectorAlgorithm).setPixelThreshold(threshold); } - if (threshold > 255) { - throw new IllegalArgumentException("Pixel intensity threshold cannot be higher than 255!"); - } - this.pixelThreshold = threshold; } /** - * Set percentage fraction of detected motion area threshold above which it - * is classified as "moved". Minimum value for this is 0 and maximum is 100, - * which corresponds to full image covered by spontaneous motion. - * + * Sets areaThreshold to the underlying detector algorithm, but only if the + * algorithm is (or extends) WebcamMotionDetectorDefaultAlgorithm + * + * @see WebcamMotionDetectorDefaultAlgorithm#setAreaThreshold(double) + * * @param threshold the percentage fraction of image area - * @see #DEFAULT_AREA_THREASHOLD */ public void setAreaThreshold(double threshold) { - if (threshold < 0) { - throw new IllegalArgumentException("Area fraction threshold cannot be negative!"); + if (detectorAlgorithm instanceof WebcamMotionDetectorDefaultAlgorithm) { + ((WebcamMotionDetectorDefaultAlgorithm)detectorAlgorithm).setAreaThreshold(threshold); } - if (threshold > 100) { - throw new IllegalArgumentException("Area fraction threshold cannot be higher than 100!"); - } - this.areaThreshold = threshold; } /** @@ -485,7 +410,7 @@ public boolean isMotion() { * @return Return percentage image fraction covered by motion */ public double getMotionArea() { - return area; + return detectorAlgorithm.getArea(); } /** @@ -495,54 +420,21 @@ public double getMotionArea() { * @return Center of gravity point */ public Point getMotionCog() { - return cog; - } - - private static int combinePixels(int rgb1, int rgb2) { - - // first ARGB - - int a1 = (rgb1 >> 24) & 0xff; - int r1 = (rgb1 >> 16) & 0xff; - int g1 = (rgb1 >> 8) & 0xff; - int b1 = rgb1 & 0xff; - - // second ARGB - - int a2 = (rgb2 >> 24) & 0xff; - int r2 = (rgb2 >> 16) & 0xff; - int g2 = (rgb2 >> 8) & 0xff; - int b2 = rgb2 & 0xff; - - r1 = clamp(Math.abs(r1 - r2)); - g1 = clamp(Math.abs(g1 - g2)); - b1 = clamp(Math.abs(b1 - b2)); - - // in case if alpha is enabled (translucent image) - - if (a1 != 0xff) { - a1 = a1 * 0xff / 255; - int a3 = (255 - a1) * a2 / 255; - r1 = clamp((r1 * a1 + r2 * a3) / 255); - g1 = clamp((g1 * a1 + g2 * a3) / 255); - b1 = clamp((b1 * a1 + b2 * a3) / 255); - a1 = clamp(a1 + a3); + Point cog = detectorAlgorithm.getCog(); + if (cog == null) { + // detectorAlgorithm hasn't been called so far - get image center + int w = webcam.getViewSize().width; + int h = webcam.getViewSize().height; + cog = new Point(w / 2, h / 2); } - - return (a1 << 24) | (r1 << 16) | (g1 << 8) | b1; + return cog; } /** - * Clamp a value to the range 0..255 + * @return the detectorAlgorithm */ - private static int clamp(int c) { - if (c < 0) { - return 0; - } - if (c > 255) { - return 255; - } - return c; + public WebcamMotionDetectorAlgorithm getDetectorAlgorithm() { + return detectorAlgorithm; } } diff --git a/webcam-capture/src/main/java/com/github/sarxos/webcam/WebcamMotionDetectorAlgorithm.java b/webcam-capture/src/main/java/com/github/sarxos/webcam/WebcamMotionDetectorAlgorithm.java new file mode 100644 index 00000000..110d9998 --- /dev/null +++ b/webcam-capture/src/main/java/com/github/sarxos/webcam/WebcamMotionDetectorAlgorithm.java @@ -0,0 +1,53 @@ +package com.github.sarxos.webcam; + +import java.awt.Point; +import java.awt.image.BufferedImage; + + +/** + * Implementation of this interface is responsible for decision whether the + * difference between two images represents movement or not. Instance may + * specified as parameter of WebcamMotionDetector constructor, otherwise + * WebcamMotionDetectorDefaultAlgorithm is used. + */ +public interface WebcamMotionDetectorAlgorithm { + + /** + * WebcamMotionDetector calls this method for each image used as parameter + * of the method {@link #detect(BufferedImage, BufferedImage)}. + * Implementation may transform the original image and prepare it for + * comparison of two images. + * May return the same instance if no there is no need to transform. + * + * @param original image + * @return modified image + */ + BufferedImage prepareImage(BufferedImage original); + + /** + * Detects motion by comparison of the two specified images content. + * {@link #prepareImage(BufferedImage)} method was called for both specified images. + * + * @param previousModified + * @param currentModified + * @return If the motion was detected returns true, otherwise returns false + */ + boolean detect(BufferedImage previousModified, BufferedImage currentModified); + + /** + * Get motion center of gravity. When no motion is detected this value + * points to the image center. + * May return null before the first movement check. + * + * @return Center of gravity point + */ + Point getCog(); + + /** + * Get percentage fraction of image covered by motion. 0 means no motion on + * image and 100 means full image covered by spontaneous motion. + * + * @return Return percentage image fraction covered by motion + */ + double getArea(); +} diff --git a/webcam-capture/src/main/java/com/github/sarxos/webcam/WebcamMotionDetectorDefaultAlgorithm.java b/webcam-capture/src/main/java/com/github/sarxos/webcam/WebcamMotionDetectorDefaultAlgorithm.java new file mode 100644 index 00000000..5a9a8a27 --- /dev/null +++ b/webcam-capture/src/main/java/com/github/sarxos/webcam/WebcamMotionDetectorDefaultAlgorithm.java @@ -0,0 +1,202 @@ +package com.github.sarxos.webcam; + +import java.awt.Point; +import java.awt.image.BufferedImage; + +import com.github.sarxos.webcam.util.jh.JHBlurFilter; +import com.github.sarxos.webcam.util.jh.JHGrayFilter; + +/** + * Default motion detector algorithm. + */ +public class WebcamMotionDetectorDefaultAlgorithm implements WebcamMotionDetectorAlgorithm { + + /** + * Default pixel difference intensity threshold (set to 25). + */ + public static final int DEFAULT_PIXEL_THREASHOLD = 25; + + /** + * Default percentage image area fraction threshold (set to 0.2%). + */ + public static final double DEFAULT_AREA_THREASHOLD = 0.2; + + /** + * Pixel intensity threshold (0 - 255). + */ + private volatile int pixelThreshold = DEFAULT_PIXEL_THREASHOLD; + + /** + * Pixel intensity threshold (0 - 100). + */ + private volatile double areaThreshold = DEFAULT_AREA_THREASHOLD; + + /** + * Motion strength (0 = no motion, 100 = full image covered by motion). + */ + private double area = 0; + + /** + * Center of motion gravity. + */ + private Point cog = null; + + /** + * Blur filter instance. + */ + private final JHBlurFilter blur = new JHBlurFilter(6, 6, 1); + + /** + * Gray filter instance. + */ + private final JHGrayFilter gray = new JHGrayFilter(); + + /** + * Creates default motion detector algorithm. + * + * @param pixelThreshold intensity threshold (0 - 255) + * @param areaThreshold percentage threshold of image covered by motion + */ + public WebcamMotionDetectorDefaultAlgorithm(int pixelThreshold, double areaThreshold) { + setPixelThreshold(pixelThreshold); + setAreaThreshold(areaThreshold); + } + + @Override + public BufferedImage prepareImage(BufferedImage original) { + BufferedImage modified = blur.filter(original, null); + modified = gray.filter(modified, null); + return modified; + } + + @Override + public boolean detect(BufferedImage previousModified, BufferedImage currentModified) { + int p = 0; + + int cogX = 0; + int cogY = 0; + + int w = currentModified.getWidth(); + int h = currentModified.getHeight(); + + if (previousModified != null) { + for (int x = 0; x < w; x++) { + for (int y = 0; y < h; y++) { + + int cpx = currentModified.getRGB(x, y); + int ppx = previousModified.getRGB(x, y); + int pid = combinePixels(cpx, ppx) & 0x000000ff; + + if (pid >= pixelThreshold) { + cogX += x; + cogY += y; + p += 1; + } + } + } + } + + area = p * 100d / (w * h); + + if (area >= areaThreshold) { + cog = new Point(cogX / p, cogY / p); + return true; + } else { + cog = new Point(w / 2, h / 2); + return false; + } + } + + @Override + public Point getCog() { + return this.cog; + } + + @Override + public double getArea() { + return this.area; + } + + /** + * Set pixel intensity difference threshold above which pixel is classified + * as "moved". Minimum value is 0 and maximum is 255. Default value is 10. + * This value is equal for all RGB components difference. + * + * @param threshold the pixel intensity difference threshold + * @see #DEFAULT_PIXEL_THREASHOLD + */ + public void setPixelThreshold(int threshold) { + if (threshold < 0) { + throw new IllegalArgumentException("Pixel intensity threshold cannot be negative!"); + } + if (threshold > 255) { + throw new IllegalArgumentException("Pixel intensity threshold cannot be higher than 255!"); + } + this.pixelThreshold = threshold; + } + + /** + * Set percentage fraction of detected motion area threshold above which it + * is classified as "moved". Minimum value for this is 0 and maximum is 100, + * which corresponds to full image covered by spontaneous motion. + * + * @param threshold the percentage fraction of image area + * @see #DEFAULT_AREA_THREASHOLD + */ + public void setAreaThreshold(double threshold) { + if (threshold < 0) { + throw new IllegalArgumentException("Area fraction threshold cannot be negative!"); + } + if (threshold > 100) { + throw new IllegalArgumentException("Area fraction threshold cannot be higher than 100!"); + } + this.areaThreshold = threshold; + } + + private static int combinePixels(int rgb1, int rgb2) { + + // first ARGB + + int a1 = (rgb1 >> 24) & 0xff; + int r1 = (rgb1 >> 16) & 0xff; + int g1 = (rgb1 >> 8) & 0xff; + int b1 = rgb1 & 0xff; + + // second ARGB + + int a2 = (rgb2 >> 24) & 0xff; + int r2 = (rgb2 >> 16) & 0xff; + int g2 = (rgb2 >> 8) & 0xff; + int b2 = rgb2 & 0xff; + + r1 = clamp(Math.abs(r1 - r2)); + g1 = clamp(Math.abs(g1 - g2)); + b1 = clamp(Math.abs(b1 - b2)); + + // in case if alpha is enabled (translucent image) + + if (a1 != 0xff) { + a1 = a1 * 0xff / 255; + int a3 = (255 - a1) * a2 / 255; + r1 = clamp((r1 * a1 + r2 * a3) / 255); + g1 = clamp((g1 * a1 + g2 * a3) / 255); + b1 = clamp((b1 * a1 + b2 * a3) / 255); + a1 = clamp(a1 + a3); + } + + return (a1 << 24) | (r1 << 16) | (g1 << 8) | b1; + } + + /** + * Clamp a value to the range 0..255 + */ + private static int clamp(int c) { + if (c < 0) { + return 0; + } + if (c > 255) { + return 255; + } + return c; + } +} diff --git a/webcam-capture/src/main/java/com/github/sarxos/webcam/WebcamUpdater.java b/webcam-capture/src/main/java/com/github/sarxos/webcam/WebcamUpdater.java index a573cff0..9607bc37 100644 --- a/webcam-capture/src/main/java/com/github/sarxos/webcam/WebcamUpdater.java +++ b/webcam-capture/src/main/java/com/github/sarxos/webcam/WebcamUpdater.java @@ -30,7 +30,7 @@ public class WebcamUpdater implements Runnable { * between 2 image fetching, when the non-blocking (asynchronous) access to the * webcam is enabled. */ - public interface DelayCalculator { + public static interface DelayCalculator { /** * Calculates delay before the next image will be fetched from the