Instantly share code, notes, and snippets.
Last active
August 29, 2015 14:22
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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