Point Clouds
The Kinect captures the depth image as a set of 3D points.
Simple OpenNI can return that set of points.
When you access the set of 3D points, you are dealing with vectors instead of integers.
Vectors are a way of storing points with multiple coordinates in a single variable.
It is a collection of values that describe relative position in space, and the difference between two points. You can think of vectors as something
traveling in a direction.
Using vectors will simplify your code and the vector class provides a set of functions for common mathematical operations that happen over and over and over again. You'll be able to store a vector and access it's x, y and z component. This is especially handy when you deal with large sets of 3D data.
Processing provides a class to handle vectors. It is called
PVector:
PVector p=new PVector(1,2,3);
println("x="+p.x+" y="+p.y+" z="+p.z);
Here is how you could use Processing to draw a point cloud:
/*
http://shop.oreilly.com/product/0636920020684.do
Making Things See by Greg Borenstein
*/
import processing.opengl.*;
import SimpleOpenNI.*;
SimpleOpenNI kinect;
void setup() {
size(1024, 768, OPENGL);
kinect = new SimpleOpenNI(this);
kinect.enableDepth();
}
void draw() {
background(0);
kinect.update();
// prepare to draw centered in x-y
// pull it 1000 pixels closer on z
translate(width/2, height/2, -1000);
// flip y-axis from "realWorld"
//otherwise your point cloud will be upside down
//remember that in Processing the top is 0
rotateX(radians(180));
//points are colored using stroke()
stroke(255); //
// get the depth data as 3D points
PVector[] depthPoints = kinect.depthMapRealWorld(); //
for (int i = 0; i < depthPoints.length; i++) {
// get the current point from the point array
PVector currentPoint = depthPoints[i];
// draw the current point
point(currentPoint.x, currentPoint.y, currentPoint.z); //
}
}
Because you are now working in 3D space, the image does not fit perfectly into a rectangle. This is because at the edge of the rectangle, everything gets cut off regardless of whether that object is in the foreground or the background. Anything cut off in the image, doesn't get represented in the point cloud.
You can also notice that you can see through objects when represented as a point cloud. The density of the point cloud is limited by the images resolution. While you have many points, you do not have an infinite number of them.
Rotating and Translating
Because rotating and translating when applied to the world are additive, you'll need to use
pushMatrix() and
popMatrix() to isolate a set of transformations so that they don't affect anything outside of them.
To make your point cloud interactive, you'll need to use the
draw() method.
/*
http://shop.oreilly.com/product/0636920020684.do
Making Things See by Greg Borenstein
*/
import processing.opengl.*;
import SimpleOpenNI.*;
SimpleOpenNI kinect;
// variable to hold current rotation
// represented in degrees
float rotation = 0;
void setup() {
//3D space
size(1024, 768, OPENGL);
kinect = new SimpleOpenNI(this);
kinect.enableDepth();
}
void draw() {
background(0);
kinect.update();
// prepare to draw centered in x-y
// pull it 1000 pixels closer on z
translate(width/2, height/2, -1000);
// flip the point cloud vertically:
rotateX(radians(180));
// move center of rotation
// to inside the point cloud
translate(0, 0, 1000);
//rotate with the mouse
//this function keeps the named axis fixed
rotateY(map(mouseX, 0, width, -PI/2, PI/2));
stroke(255);
PVector[] depthPoints = kinect.depthMapRealWorld();
// notice: "i+=10"
// only draw every 10th point to make things faster
for (int i = 0; i < depthPoints.length; i+=10) {
PVector currentPoint = depthPoints[i];
point(currentPoint.x, currentPoint.y, currentPoint.z);
}
}
Play with the sketch. The kinect only sees the surface of the objects. The point cloud is a distribution of points on the surface, so you won't have a 3D model.
Hot Spots
The following sketch creates a red cube:
import processing.opengl.*;
import SimpleOpenNI.*;
SimpleOpenNI kinect;
float rotation = 0;
// set the box size
int boxSize = 150;
// a vector holding the center of the box
PVector boxCenter = new PVector(0, 0, 600);
void setup() {
size(1024, 768, OPENGL);
kinect = new SimpleOpenNI(this);
kinect.enableDepth();
}
void draw() {
background(0);
kinect.update();
translate(width/2, height/2, -1000); //
rotateX(radians(180));
translate(0, 0, 1000);
rotateY(map(mouseX, 0, width, -PI/2, PI/2));
stroke(255);
PVector[] depthPoints = kinect.depthMapRealWorld();
for (int i = 0; i < depthPoints.length; i+=10) {
PVector currentPoint = depthPoints[i];
point(currentPoint.x, currentPoint.y, currentPoint.z);
}
// draw the cube
// move to the box center
translate(boxCenter.x, boxCenter.y, boxCenter.z); //
// set line color to red
stroke(255, 0, 0);
// leave the box unfilled so we can see through it
noFill();
// draw the box
box(boxSize); //
}
To test whether the depth cloud is within the hot spot, you need to declare a new variable
depthPointsInBoxin the draw() method and then use nested if then statements to test if a point is within the cube:
if (currentPoint.x > boxCenter.x - boxSize/2 && currentPoint.x < boxCenter.x + boxSize/2){
if (currentPoint.y > boxCenter.y - boxSize/2 && currentPoint.y < boxCenter.y + boxSize/2){
if (currentPoint.z > boxCenter.z - boxSize/2 && currentPoint.z < boxCenter.z + boxSize/2){
depthPointsInBox++;
}
}
}
If the point is within the box, then increment the variable
depthPointsInBox.
You can now use this variable as a variable resistor. Maybe change some property based on how much or how little points are within the cube.
For instance, you could increase the opacity of the cube based on the points:
float pointOpacity=map(depthPointsInBox, 0, 1000,0,255);
fill(255,0,0,pointOpacity);
stroke(255,0,0);
box(boxSize);
Besides color, what else could you control?
What about sound?
Minim
Processing does not provide much functionality for working with audio. To use sound in Processing, you need to use a library, and while the Processing community has developed several libraries for working with sound, the
Minim library, developed by Damien DiFede, is the best known and most complete. It also comes with Processing so you don't need to download it or install it.
The core of the
Minim library is a class called
Minim. Every time you use the
Minim library, you need to instantiate a
Minim object and call its constructor
with the
this keyword. You can call four tasks with the Minim object:
- play an audio file that was loaded into your application
- play a file that was created in your program
- monitor audio and get data about it
- record audio to disk
this needs to be included in your sketch:
//import the library
import ddf.minim.*;
//create an object
Minim minim;
//instantiate the instance
minim=new Minim(this);
This sketch plays a file. The wave file must be in the same folder as your sketch:
import ddf.minim.*;
Minim minim;
AudioPlayer song;
void setup(){
size(200, 200);
// always start Minim first!
minim=new Minim(this);
// this loads mysong.wav from the data folder
song = minim.loadFile("mysong.wav");
background(255);
}
void draw(){
background(255);
if (mousePressed){
song.unmute();
song.play();
}
}
void stop(){
song.close();
super.stop();
}
void mouseReleased(){
song.mute();
song.rewind();
cursor(HAND);
}
The following program instantiates an audio library that can hold an audio file, control the playback, read the data from the audio file as it plays to create graphics.
import ddf.minim.*;
Minim minim;
AudioPlayer song;
boolean isPlaying=false;
// always start Minim first!
void setup(){
size(200, 200);
minim=new Minim(this);
// this loads mysong.wav from the data folder
song = minim.loadFile("mySong.mp3");
}
void draw(){
fill(0x000000,30);
rect(0,0,width,height);
stroke(255);
noFill();
for(int i=0;i<song.bufferSize()-1;i++){
ellipse(i*4,100+song.left.get(i)*100,5,5);
ellipse(i*4,250+song.right.get(i)*100,5,5);
}
}
void mousePressed(){
if(isPlaying){
song.pause();
isPlaying=false;
}else{
song.play();
isPlaying=true;
}
}
void mouseReleased(){
if(isPlaying){
song.pause();
isPlaying=false;
}else{
song.play();
isPlaying=true;
}
}
void stop(){
minim.stop();
super.stop();
}
Generating Sound
Minim also defines methods to generate new sounds from equations. Four fundamental kinds of wav can generate sound:
- triangle
- square
- sine
- sawtooth
Each of these creates a slight variation of the familiar single tone that can be the base of a more complex tone.
Minim simplifies generating tones for you by providing several convenience classes that generate tones and let you manipulate their frequency and amplitude in your application.
Generating a sine or sawtooth wave is a process of feeding floating-point values to the sound card so that it can convert them into analog signals. The
AudioOutput class is used to store information about the data being sent to the sound card for manipulation while the application is running. Though the
AudioOutput class has several dozen methods, only a few are used frequently:
- addSignal(AudioSignal signal)
This adds a signal to the chain of signals that will be played
- removesSignal(AudioSignal signal)
This removes a signal from the chain of signals that will be played
- left
This is an array containing all the samples for the left channel of the sound being sent to the sound card
- right
This is an array containing all the samples for the right channel of the sound being sent to the sound card
- mix
This is an array of data containing the mix of the left and right channels
To get the audio data generated by SineWave or SquareWave classes you need to use getLineOut:
AudioOutput out;
out=minim.getLineOut(Minim.STEREO);
Here is code for generating a square and sine wave:
import ddf.minim.*;
import ddf.minim.signals.*;
AudioOutput out;
SquareWave square;
SawWave saw;
Minim minim;
void setup(){
size(400,400);
minim=new Minim(this);
out=minim.getLineOut(Minim.STEREO, 512);
//freq of 440Hz, amplitude of 1 smples of 44100
square=new SquareWave(440,1,44100);
//freq of 600Hz, amplitude of 1 smples of 44100
saw=new SawWave(600,1,44100);
out.addSignal(square);
out.addSignal(saw);
}
void draw(){
saw.setFreq(mouseX);
square.setFreq(mouseY);
}
void stop(){
minim.stop();
super.stop();
}
AudioPlayer class
The easiest way to check whether some functionality is supported is to call the
hasControl() method of the
AudioPlayer class:
AudioPlayer player=Minim.loadFile("myfile.mp3");
boolean hasVolume=player.hasControl(Controller.VOLUME);
This code snippet loads up two files and then allows the user to fade between them using the movement of of the mouse:
import ddf.minim.*;
AudioPlayer player;
AudioPlayer player2;
Minim minim;
boolean hasVolume;
void setup(){
size(400,400);
minim=new Minim(this);
// this loads mysong.wav from the data folder
player= minim.loadFile("04.mp3",1024);
player2= minim.loadFile("03.mp3");
player.play();
player.printControls();
player2.play();
hasVolume=player.hasControl(Controller.VOLUME);
}
void draw(){
background(0);
stroke(0,255,0);
float gain1=0;
float gain2=0;
/*if the setip on the user's computer can alter the volume, then set that using the mouse position, otherwise, set the gain using the mouse position.
*/
if(hasVolume){
player.setVolume(mouseX/width);
gain1=map(player.getVolume(),0,1,0,50);
}
else{
player.setGain(map(mouseX,0,width, -20,1));
gain1=map(player.getGain(),-20,1,0,50);
}
for(int i=0;i<player.left.size()-1;i++){
line(i,50+player.left.get(i)*gain1,i+1,50+player.left.get(i+1)*gain1);
line(i,150+player.right.get(i)*gain1,i+1,150+player.right.get(i+1)*gain1);
}
stroke(255,0,0);
if(hasVolume){
player2.setVolume(width-mouseX/width);
gain2=map(player2.getVolume(),0.0,1,0,50);
}
else{
player2.setGain(map(width-mouseX,0,width,-20,1));
gain2=map(player2.getGain(),-20,1,0,50);
}
for(int i=0;i<player2.left.size()-1;i++){
line(i,50+player2.left.get(i)*gain2,i+1,50+player.left.get(i+1)*gain2);
line(i,150+player2.right.get(i)*gain2,i+1,150+player.right.get(i+1)*gain2);
}
}
void stop(){
player.close();
player2.close();
minim.stop();
super.stop();
}
Filtering
Filtering a sound can involve removing a narrow band of its sound frequency, removing the highest or lowest frequency part of a complex sound, changing its pitch, or removing popping sounds from the audio to smooth it out.Being able to create and tune filters in Minim is simple.
This code applies a lowPass filter to a sine wave:
import ddf.minim.*;
import ddf.minim.signals.*;
import ddf.minim.effects.*;
AudioOutput out;
SquareWave square;
LowPassSP lowpass;
Minim minim;
void setup(){
size(400,400);
minim=new Minim(this);
// this loads mysong.wav from the data folder
out=minim.getLineOut(Minim.STEREO, 512);
square=new SquareWave(440,1,44100);
//filter has cutoff at 200Hz
lowpass=new LowPassSP(200,44100);
out.addSignal(square);
out.addEffect(lowpass);
}
void draw(){
try{
if(out.hasSignal(square)){
out.removeEffect(lowpass);
}
lowpass.setFreq(mouseY);
out.addEffect(lowpass);
}catch(Exception e){
}
}
void stop(){
out.close();
minim.stop();
super.stop();
}
Assignment
Create a sketch that uses a point cloud to control sound.
Here is the HotPoint class that might help you:
/*
http://shop.oreilly.com/product/0636920020684.do
Making Things See by Greg Borenstein
*/
class HotPoint{
PVector center;
color fillColor;
color strokeColor;
int size;
int pointsIncluded;
int maxPoints;
boolean wasJustHit;
int threshold;
HotPoint(float centerX, float centerY, float centerZ,int boxSize){
center=new PVector(centerX, centerY, centerZ)'
size=boxSize;
pointIncluded=0;
maxPoints=1000;
threshold=0;
fillColor=strokeColor=color(random(255), random(255), random(255));
}
void setThreshold(int newThreshold){
threshold=newThreshold;
}
void setMaxPoints(int newMaxPoints){
maxPoints=newMaxPoints;
}
void setColor(float red, float green, float blue){
fillColor=strokeColor=color(red, green, blue);
}
boolean check(PVector point){
boolean result=false;
if(point.x>center.x-size/2 && point.x<center.x+size/2){
if(point.y>center.y-size/2 && point.y<center.y+size/2){
if(point.z>center.z-size/2 && point.z<center.z+size/2){
result=true;
pointsIncluded++;
}
}
}
return result;
}
void draw(){
pushMatrix();
translate(center.x, center.y, center.z);
fill(red(fillColor), green(fillColor), blue(fillColor), 255*percentIncluded());
stroke(red(strokeColor), green(strokeColor), blue(strokeColor), 255);
box(size);
popMatrix();
}
float percentIncluded(){
return map(pointsIncluded, o, maxPoints, 0,1);
}
boolean currentlyHit(){
return (pointsIncluded>threshold);
}
boolean isHit(){
return currentlyHit() && !wasJustHit;
}
void clear(){
wasJustHit=currentlyHit();
pointsIncluded=0;
}
}
Inside your main sketch:
Create the instance variable:
in the
setup()
// HotPoint(float centerX, float centerY, float centerZ,int boxSize){
myHotPoint=new HotPoint(200, 0, 600, 150);
in the
draw()
myHotPoint.check(currentPoint);
also
draw()
if(myHotPoint.isHit()){
//do something
}
myHotPoint.draw();
myHotPoint.clear();