Posted by Armin on Sunday, January 02, 2011
How the program works
The software utilizes libraries provided by the Processing language (http://www.processing.org/), as well as the open-source computer vision framework OpenCV (http://ubaa.net/shared/processing/opencv/).
On my laptop, it runs between 15 and 20 frames/s, so it is not useful for triggering a digital SLR camera to capture fast action, for example. The relatively long delays (about 100ms) don't matter when using high-speed cameras, however. These cameras continuously capture video to an internal, circular buffer. When a trigger is received, it can be set to define the last frame of the recording. The saved video history is on the order of seconds (depending on the resolution and recording rate), so that the 100 ms trigger delay can be ignored.
Source Code
The left image is a link to a screenshot that shows how the various source code and libraries are organized in the project folder created in the Eclipse IDE (http://www.eclipse.org/).
The following external libraries are needed
- Core Processing library: http://processing.org/learning/eclipse/
- OpenCV: http://ubaa.net/shared/processing/opencv/
- Minim audio library: http://code.compartmental.net/tools/minim/
- Serial library: http://www.processing.org/reference/libraries/serial/
MeasureMotionController.java - creating the window
The Java code in the listing below creates the window for the main applet, which mainly uses code from the Processing.org library (http://processing.org/). To create the window, it uses Java's AWT ("advanced windowing manager") package.
package org.amphioxus.measuremotionv1; import java.applet.Applet; import java.awt.BorderLayout; import java.awt.Frame; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; private static final long serialVersionUID = 1L; private int w = 640, h = 480; public MeasureMotionController() { // call to superclass needs to come first in constructor super("MotionMeasure v.1"); // won't allow frame to be resized setResizable(false); // set up frame (which will hold applet) setSize(w, h); setLocationRelativeTo(null); // centers the frame on the screen // allow window and application to be closed // System.out.println("Bye!"); } }); cam1.init(); } MeasureMotionController c = new MeasureMotionController(); c.setVisible(true); } }
MeasureMotion.java - the main applet
The applet defined by MeasureMotion.java above provides the main functionality for the software. It uses the OpenCV framework, which needs to be installed first, to grab frames from the computer's default web camera. Framet captured at time t is first converted to grayscale. It is then compared to a thresholded and slightly blurred version of the frame captured one iteration earlier. By taking the absolute difference of each pixel between framet and framet-1, any differences in the two will appear as white pixels. The last step is to put the current image into memory for the comparison in the next iteration. (see http://ubaa.net/shared/processing/opencv/opencv_absdiff.html).
As defined in the keyPressed() function (around line 127), keys are used to adjust the motion threshold (Increase by 0.1: up arrow; Decrease by 0.1: down arrow). The threshold for the binary threshold operation can be changed in steps of 1 with the "," and "." keys, or in steps of 5 with the "m" and "/" keys.
After initial start-up, the alarm is turned off. It can be toggled on and off by pressing the "a" key. Once the alarm is set (as indicated by the text in the application's interface), a trigger action is elicited every time the motion threshold is crossed. The trigger action is carried out within a thread in which a byte is sent to the serial port. An Arduino attached to the computer's USB port can easily be programmed to react to this serial command with an appropriate action. In addition to the serial communication, a sound clip is played. The sound clip is loaded and played back using the Minim audio library (found at http://code.compartmental.net/tools/minim/).
Line 199 of this listing is where you would place a serial command for talking to an Arduino, for example.
package org.amphioxus.measuremotionv1; import hypermedia.video.OpenCV; import processing.core.PApplet; import processing.core.PFont; import processing.serial.Serial; import ddf.minim.*; @SuppressWarnings("serial") public class MeasureMotion extends PApplet { private int w = 640, h = 480; OpenCV video; private int rectw = 150; private int rectx = w / 2 - rectw / 2; private int recth = 150; private int recty = h / 2 - recth / 2; Minim minim; AudioSample alarmSound; boolean alarmFlag = false; int timer = 15; TargetRect targetRect = new TargetRect(rectx, recty, rectw, recth, this); // instance private float imagethreshold = 50; private float motionThresh = 4.f; // above brightness/pixel value for which // alarm goes off Serial myPort; // create serial port // Fonts for displaying on applet: PFont f1, f2; public MeasureMotion() { // TODO Auto-generated constructor stub } public void setup() { size(w,h); background(0); //frameRate(30); noStroke(); // set up fonts: f1 = createFont("Monospaced", 28, true); // create display fonts f2 = createFont("Monospaced", 16, true); // set up opencv video object: video = new OpenCV(this); video.capture(w, h); minim = new Minim(this); alarmSound = minim.loadSample("alarm.aif", 1411); if ( alarmSound == null ) println("Couldn't load alarmSound!"); // // set up serial communication: println(Serial.list()); // List all the available serial ports myPort = new Serial(this, Serial.list()[0], 115200); } public void draw() { background(0); video.read(); video.convert(OpenCV.GRAY); video.absDiff(); video.threshold(imagethreshold); // get binary image video.blur(OpenCV.BLUR, 3); video.remember(); set(0, 0, video.image()); targetRect.measureMotion(); // measure brightness under target rectangle targetRect.draw(); // draw the rectangle textFont(f1,28); textAlign(RIGHT); // ACTION WHEN MOTION > THRESHOLD: if (targetRect.brightnessTotal > motionThresh) { fill(200, 20, 20, 150); // change text color to red if ((alarmFlag == true) && (stillinactive() == false)) { // send serial command out in separate thread serialTx.start(); alarmSound.trigger(); // sound the alarm bell timer = 15; // reset timer } } else fill(120, 120, 120, 150); text("Motion: " + line1, width - 50, height - 50); fill(120, 120, 120, 150); text("FPS: " + line2, width - 50, height - 20); textFont(f2); textAlign(LEFT); text("motion threshold: " + line3, 20, height-40); text("binary threshold: " + line4, 20, height-20); if (alarmFlag == true) text("trigger alarm: ON", 20, height-60); else text("trigger alarm: OFF", 20, height-60); // make timer count down each frame: if (timer > 0) { timer -= 1; } } public void mousePressed() { rectx = mouseX; // remember x and y coords when mouse is pressed recty = mouseY; } public void mouseDragged() { int tempw = mouseX - rectx; // distance from x/y of mouse click to // current position int temph = mouseY - recty; targetRect.update(rectx, recty, tempw, temph); } public void mouseReleased() { rectw = mouseX - rectx; recth = mouseY - recty; targetRect.update(rectx, recty, rectw, recth); } public void keyPressed() { if( key==CODED ){ if( keyCode == UP ){ if (motionThresh < 5.9) { motionThresh += 0.1f; } } if( keyCode == DOWN ){ if (motionThresh >= 0.2) { motionThresh -= 0.1f; } } } // end if key == coded if (key == 'm') { if (imagethreshold > 0) { imagethreshold -= 5; // System.out.println("image binary threshold: " + imagethreshold); } } if (key == '/') { if (imagethreshold < 250) { imagethreshold += 5; // System.out.println("image binary threshold: " + imagethreshold); } } if (key == ',') { if (imagethreshold > 0) { imagethreshold -= 1; // System.out.println("image binary threshold: " + imagethreshold); } } if (key == '.') { if (imagethreshold < 250) { imagethreshold += 1; // System.out.println("image binary threshold: " + imagethreshold); } } if (key == 'a'){ if (alarmFlag == false) { alarmFlag = true; // println("Audio alarm ON"); } else { alarmFlag = false; // println("Audio alarm OFF"); } } } // check whether next action can be triggered: public boolean stillinactive() { if (timer <= 0.0) { return false; } else { return true; } } public void stop() { alarmSound.close(); minim.stop(); video.stop(); println("Program stopped"); } SerialTxThread() { } // This method is called when the thread runs public void run() { println("Thread started when alarm was triggered!!! "); // communicate with the Arduino out of this thread } } } // end of MeasureMotion applet class
TargetRect.java: target rectangle for defining ROI
This class defines a region of interest (ROI) under which image motion is measured for the threshold calculation. The ROI can be set by clicking and dragging to place the rectangle.
package org.amphioxus.measuremotionv1; import processing.core.PApplet; public class TargetRect { PApplet parent; int x, y, w, h; float brightnessTotal; TargetRect(int x, int y, int w, int h, PApplet p) { this.x = x; this.y = y; this.w = w; this.h = h; parent = p; } public void draw() { parent.stroke(200, 20, 20); parent.fill(255, 255, 255, 25); parent.rect(x, y, w, h); } public void update(int x, int y, int w, int h) { this.x = x; this.y = y; this.w = w; this.h = h; } public void measureMotion() { brightnessTotal = 0; parent.loadPixels(); // operate on the pixels for (int i = x; i < x + w; i++) { for (int k = y; k < y + h; k++) { int loc = i + k * parent.width; brightnessTotal += parent.brightness(parent.pixels[loc]); } } // average pixel brightness in target rectangle: brightnessTotal = brightnessTotal / (w * h); } }