Skip to content

Instantly share code, notes, and snippets.

@kasperkamperman
Last active August 29, 2015 14:22
Show Gist options
  • Save kasperkamperman/43f76dfa89a8fc50e583 to your computer and use it in GitHub Desktop.
Save kasperkamperman/43f76dfa89a8fc50e583 to your computer and use it in GitHub Desktop.
Buttons that can be triggered by Motion. Uses the OpenCV library for Processing.
/*
Frame Differencing example:
- including mirror effect
- you can place motion buttons on the screen
- in the setup we place the buttons in a grid.
- you probably need to tweak some variables in the MotionButton class.
Install the OpenCV for Processing library (Sketch > Import library):
https://github.com/atduskgreg/opencv-processing
Check also the reference:
http://atduskgreg.github.io/opencv-processing/reference/
One OpenCV object is used to flip the image (so webcam acts like a mirror).
We use OpenCV Mat objects for framedifferencing. See this issue if you'd like to know why
https://github.com/atduskgreg/opencv-processing/issues/79
kasperkamperman.com - 24-06-2015
*/
import gab.opencv.*;
import processing.video.*;
import org.opencv.core.Mat;
Capture video;
OpenCV cvFlip;
OpenCV cv;
Mat cvLastFrameMat;
Mat cvThisFrameMat;
PImage scaledImg;
PImage differenceImg;
// size of the webcam capture
// depends your camera
// also modifify the cvDivider if you change this
int captureWidth = 320;
int captureHeight = 240;
int cvDivider = 4; // the higher the smaller the cv resolution
int cvWidth;
int cvHeight;
int rows = 4; // rows
int cols = 4; // cols
// array for the motionbutton objects
MotionButton [] buttons = new MotionButton[rows * cols];
void setup() {
// P3D uses OpenGL, probably it runs more fluent
size(1280,800, P3D);
// frame rate more then 25fps doesn't make much sense
frameRate(25);
// capture video from the webcam
// check Processing examples for more options on capturing (selecting a camera for example)
video = new Capture(this, captureWidth, captureHeight, 25);
video.start();
// opencv object that is purely used to flip the video image
cvFlip = new OpenCV( this, video.width, video.height);
cvFlip.useColor();
// we will progress changed motion on a smaller image, this makes
// calculations faster (and your computer running smoother).
cvWidth = video.width/cvDivider;
cvHeight = video.height/cvDivider;
// opencv object for frame differencing
cv = new OpenCV( this, cvWidth, cvHeight);
// image to store the resized video frame
scaledImg = createImage(cvWidth, cvHeight, RGB);
// image to store the motion difference between two frames
differenceImg = createImage(cvWidth, cvHeight, RGB);
// opencv Matrix (used to check difference between frames)
cvLastFrameMat = cv.getGray(); // init the cvLastFrameMatrix
// places the buttons on the screen
// we use a grid in this example with a for-loop
// however you make separate buttons and place them where you want of course
for(int i = 0; i<buttons.length; i++) {
// give a size
int buttonSize = 80;
// calculate space between the buttons to spread them equally over the screen
int xSpacing = (width - (cols*buttonSize)) / (cols+1);
int ySpacing = (height - (rows*buttonSize)) / (rows+1);
// calculate the x and y position of each button
int x = xSpacing + buttonSize/2 + ((i%cols) * (xSpacing+buttonSize));
int y = ySpacing + buttonSize/2 + ((i/cols) * (ySpacing+buttonSize));
// create a button on position x, y, size, a number
// we also pass the size of the differenceImage
buttons[i] = new MotionButton(x, y, buttonSize, i, differenceImg.width, differenceImg.height);
// when debugMode is true the number of the button (i) is shown
buttons[i].debugMode = true;
// give the button a color
buttons[i].setColor(color(255,0,196));
}
}
void draw() {
background(0);
// only process a video frame when there is a new one
// this is because the draw() loop is mostly not in sync with webcam framerates
if (video.available()) {
// read the frame
video.read();
// store the frame in the cvFlip object
cvFlip.loadImage(video);
// flip the frame horizontal so it behaves as a mirror
// (vertical doesn't make much sense)
cvFlip.flip(OpenCV.HORIZONTAL);
// in makeDifferenceImage function we check the difference between this video frame
// and the previous video frame
makeDifferenceImage();
// pass the differenceImg to each button to detect if the motion happened
// on the location of the button
for(int i = 0; i<buttons.length; i++) {
buttons[i].detectMotion(differenceImg);
}
}
// show the flipped video frame and scale it to the whole screen
// comment this to only see the difference in motion
image( cvFlip.getOutput(), 0, 0, width, height);
// show the motion on top, you can turn this off of course
// blend(src, sx, sy, sw, sh, dx, dy, dw, dh, mode)
blend(differenceImg, 0, 0, cvWidth, cvHeight, 0, 0, width, height, ADD);
// loop through all the buttons and display them
// we also check if a button is pressed
for(int i = 0; i<buttons.length; i++) {
buttons[i].display();
// see if a button is triggered
if(buttons[i].getPressedTrigger()) {
// print something to the console
// of course you can trigger sound and other cool stuff here
// println("button "+i+" pressed");
}
}
}
void makeDifferenceImage() {
// copy and scale the cvFlip image to scaledImg for opencv
scaledImg.copy(cvFlip.getOutput(), 0, 0, cvFlip.width, cvFlip.height, 0, 0, cv.width, cv.height);
// load the scaled img in the cv object for frame differencing
cv.loadImage(scaledImg);
// convert the image to an OpenCV matrix
cvThisFrameMat = cv.getGray();
// difference with last matrix (previous video frame) and this matrix (current video frame)
// the result is stored in the cv object
OpenCV.diff(cvThisFrameMat, cvLastFrameMat);
// use blur to emphasize differences
cv.blur(3);
// use threshold to make it a black and white image
cv.threshold(20);
// cv output in a differenceImg (this is what we use to check motion under the buttons)
differenceImg = cv.getOutput();
// now we store this frame because that will be used a the previous frame to
// compare the differences
// we load to scaledImg in the cv object and convert it to an OpenCV matrix
cv.loadImage(scaledImg);
cvLastFrameMat = cv.getGray();
}
/* =====================================================================================================
It might be smart to save the MotionButton class in a different tab in the Processing IDE.
With Gist one file is more convenient.
=====================================================================================================
*/
/*
In the function detectMotion we get a picture with pixels that changed (because something
was moving in front of the camera).
We count the white pixels in the rectangle (surface) below the circle.
With a threshold we can decide when we think there is enough motion. For example
10% of the pixels in the button area need to be white.
We don't what to trigger something directly, because maybe someone was just walking by. That's
we would like to see see motion over several frames.
When we see motion we add for example 0.25 to the 'progressRawValue' variable.
When there was no motion we substract for example a lower number from the 'progressRawValue' variable.
So if there is enough motion the 'progressValue' will reach 1.0 or higher (depending on the charge factor)
When that happens ('progressRawValue>=1.0') we see it as a press (isPressed == true).
The 'progressValue' variable is visualized in the progressbar and the alpha of the button.
*/
class MotionButton {
// show the number of the MotionButton when it's true
public boolean debugMode = false;
// progress bar weight
public int progressWeight = 8;
// how much percent of the area need to have motion
// to see it as a trigger (for the slider)
// value between 0.0 - 1.0 (towards 0 is more sensitive for motion)
public float detectionSurfacePercent = 0.10;
// the higher the number the more time we need to see motion in the button area.
public int progressAddFactor = 4;
// the higher the number the more we can charge the button so it stays pressed even if there is no motion
public int progressChargeFactor = 4;
// button appearance
private int x;
private int y;
private int size;
private int number;
private color buttonColor = color(255,0,0);
// variables for motion detection
private int differenceImgW;
private int differenceImgH;
private int detectionAreaX1;
private int detectionAreaY1;
private int detectionAreaX2;
private int detectionAreaY2;
private int detectionAreaWidth;
private int detectionAreaHeight;
// variables for counting pixels and values for the progress bar
private int amountOfPixels;
private int detectionPixelThreshold = 0;
private int detectionPixelCounter;
// progress bar variables
public float progressValue;
private float progressAddValue;
private float progressSubstractValue;
private float progressChargeLimit;
private float progressRawValue;
public boolean isPressed = false;
// for state change
private boolean lastPressed = false;
private boolean isPressedTrigger = false;
private boolean isReleasedTrigger = false;
MotionButton(int _x, int _y, int _s, int _n, int _diffW, int _diffH) {
this.x = _x;
this.y = _y;
this.size = _s;
this.number = _n;
this.differenceImgW = _diffW;
this.differenceImgH = _diffH;
// we draw a rectangle around our ellipse and scale it according to the
// difference image size
// calculate the scaling of the differenceImg compared with the screen
// for example image width is 640 pixels, screen is 1280 pixels, factor is 0.5
float scaleFactorW = differenceImgW/(float)width;
float scaleFactorH = differenceImgH/(float)height;
detectionAreaWidth = (int) (size * scaleFactorW);
detectionAreaHeight = (int) (size * scaleFactorH);
// the four corner points from the area covered by this button
// x-(size/2): because the x is the center of the ellipse. So size/2 gives us the edge
detectionAreaX1 = (int) ( (x-(size/2)) * scaleFactorW );
detectionAreaY1 = (int) ( (y-(size/2)) * scaleFactorH );
detectionAreaX2 = detectionAreaX1 + detectionAreaWidth;
detectionAreaY2 = detectionAreaY1 + detectionAreaHeight;
// amount of pixels in this area
amountOfPixels = detectionAreaWidth * detectionAreaHeight;
// calculate the amount of pixels that have to be white before we see it as a motion
// for example of the size is 100 pixels then 10 pixels have to be white
detectionPixelThreshold = (int) (amountOfPixels * detectionSurfacePercent);
// calculate how big the values are that need to be added or substracted when there is motion
// make the addFactor (see above) lower if you want a faster reaction on motion
progressAddValue = 1.0/progressAddFactor;
progressSubstractValue = progressAddValue/4.0;
// our threshold limit for a trigger/press and full progressbar is 1.0.
// however we can make the value higher, so when there is no motion for a few frames
// the button still seems pressed.
progressChargeLimit = 1.0 + (progressSubstractValue*progressChargeFactor);
}
// set the color of the button
void setColor(color c) {
buttonColor = c;
}
// call this to detect if there was motion below the button
// if you don't call to button doesn't work...
void detectMotion(PImage differenceImg) {
// reset the pixel counter
detectionPixelCounter = 0;
// walk through the area below the button
for( int y = detectionAreaY1; y < detectionAreaY2; y++ ){
for( int x = detectionAreaX1; x < detectionAreaX2; x++ ){
// safety check if a button is half off the screen, there isn't any
// image data
if ( x < differenceImg.width && x > 0 && y < differenceImg.height && y > 0 ) {
// If the brightness in the black and white image is above 127 (in this case, if it is white)
// -8421505 is equal to color(127)
if (differenceImg.pixels[x + (y * differenceImg.width)] > -8421505) {
// Add 1 to the movementAmount variable.
detectionPixelCounter++;
}
}
else {
// if the button is partly outside of the image, we need to lower the threshold.
// because there are no pixels that can be white (or black) at that point
detectionPixelThreshold = detectionPixelThreshold--;
}
}
}
// if there is motion, we make 'value' higher, otherwise lower.
if(detectionPixelCounter>detectionPixelThreshold) {
progressRawValue = progressRawValue + progressAddValue;
}
else {
progressRawValue = progressRawValue - progressSubstractValue;
}
// the progressRawValue can be higher then 1.0 (used to 'charge' the button)
// constrain the Raw Value between 0.0 and the charge limit
progressRawValue = constrain(progressRawValue, 0.0, progressChargeLimit);
// our progressValue (for the progress bar) should be between 0.0 and 1.0
if(progressRawValue > 1.0) progressValue = 1.0;
else progressValue = progressRawValue;
// if the value is high enough the button is pressed otherwise not.
if(progressRawValue >= 1.0) {
isPressed = true;
}
else {
isPressed = false;
}
// check if the button changed state (pressed true/false)
// more about state change check this tutorial:
// http://www.kasperkamperman.com/blog/arduino/arduino-programming-state-change/
if(lastPressed != isPressed) {
// when the is not pressed it's released
if(isPressed) isPressedTrigger = true;
else isReleasedTrigger = true;
}
else {
isPressedTrigger = false;
isReleasedTrigger = false;
}
// remember this button state for the next check
lastPressed = isPressed;
}
// get the amount of motion within the button
// a value between 0.0 and 1.0. 1.0 all pixels changed, 0.0 no movement at all
float getMotionAmount() {
return detectionPixelCounter/float(amountOfPixels);
}
// get a trigger (just one time true) when the button is pressed
boolean getPressedTrigger() {
boolean tempTrigger = isPressedTrigger;
// make it false after we call it, otherwise we can receive true the next frame as well
isPressedTrigger = false;
return tempTrigger;
}
// get a trigger (just one time true) when the button is released
boolean getReleasedTrigger() {
boolean tempTrigger = isReleasedTrigger;
isReleasedTrigger = false;
return tempTrigger;
}
// call this if you'd like to display the button.
// this is not necessary if you just want to have spots to detect motion
void display() {
// button fill with alpha change based on value
noStroke();
fill(buttonColor, progressValue * 255);
ellipse(x, y, size-(progressWeight*2), size-(progressWeight*2));
// black circle outline for progressbar
noFill();
strokeWeight(progressWeight);
stroke(0);
ellipse(x, y, size, size);
// circular progressbar to show the value
stroke(buttonColor);
arc(x, y, size, size, -HALF_PI, map(progressValue, 0.0, 1.0, -HALF_PI, TWO_PI-HALF_PI));
// show the number of the button in debugmode
// the number gets bigger when isPressed is true
if(debugMode) {
if(isPressed) textSize(44);
else textSize(32);
textAlign(CENTER, CENTER);
fill(255);
text(number,x,y);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment