Photo-Silhouette Booth (PComp & ICM Final)
For my final project in Introduction to Computational Media and Physical Computing, I collaborated with Daniel Soltis, Scott Varland, and Jeff Sable to create an interactive reinvention of the photobooth. After researching and testing traditional photobooths, we developed the notion that the constrained, self-conscious experience of posing for photographs could be improved by providing an immersive, stylized environment to engage and surprise participants--all the while capturing playful, candid photographs of them.
We were also drawn to the notion of the photobooth as a narrative space. In our observations and personal experience, we noted a theme of people using the booths to capture moments in time with friends, and to generate mementos. Subverting this theme, we decided to use our booth to generate a ongoing narrative of a place in time--rather than walking away with physical evidence of your experience, our booth adds the images of your experience to a database, and you become a part of the "story" of that booth that is displayed to future visitors.
Although our group collaborated closely on the various elements of this project, my primary role was to code the applet that is displayed on the front wall of the photobooth. This applet uses live image capture through a DV camera to display a black and white silhouette of the participant, then presents them with various graphic elements with which to interact. The three sections entail (1) small, falling red particles that bounce off the player, (2) flies moving across the screen that turn into butterflies when they "touch" the player, and (3) bubbles that float down from the top of the screen and audibly pop when the player touches them. For each of these sequences, the applet scrolls through an array of the video data searching for black pixels (indicating the participant's body), and reacts accordingly. See the code below for further details. Our primary challenge for this element of the project, aside from some serious debugging, was to create an environment that provided the required contrast between participant (black) and background (white), and to determine the best timing to create a successful experience. Optimizing the booth construction and range of movement within, and creating the high-res photo slideshow that dynamically pulled new photos from a folder presented challenges of their own, which my group-mates tackled admirably. We also owe many thanks to our dozens of user-testers, who greatly helped us fine tune the project.
Participant Experience
Upon entering our photobooth (described below), participants are presented with a projection of a slideshow of stylized screen captures of previous users' experiences, with instructions at the bottom to press the large button at the front to begin their session. In addition, they are encouraged to move and play freely while their photo is taken three times. Once started, the screen displays the participant's live silhouette superimposed on various moving elements with which they can interact by moving their body. A high-resolution photograph of the player is captured near the end of each of the three sequences described above, along with a screen capture of their silhouette and the screen elements. While this play is underway, observers outside the booth have the entertaingly voyeuristic opportunity to watch the stylized display of the participant while they are playing--toying with the notion of private and public space.
When this experience (lasting 30 seconds) is complete, the participant is instructed to step outside the booth to view the photos that were taken, and the slideshow resumes until the next player begins a session. The slideshow now includes the three screenshots of the participant that were captured while they were playing. When the player exits the booth, they see their photographs displayed on a monitor adjacent to the booth, which are then added to a separate slideshow of all players' photographs, which also plays between sessions.


The various elements of this experience combine to create a uniquely fun experience that results in attractive, vital and unexpected photographs. Almost invariably, the player loses any self-consciousness they may have had about having their photograph taken, and are generally pleased with the flattering results. The dichotomy of the high-res and low-res images that are created also adds to the experience, juxtaposing the striking and iconic stylized imagery from inside the booth with the beautiful candid photographs outside.
Physical Structure
Members of our group constructed a wooden frame of approximately 7'x4'x9' with a peaked roof, draped the sides with white curtains, and stretched white cloth across the front and rear screens/walls. The structure is diffusely but brightly lit with clip lights at the top corners, and two light kits in the rear. The effect is of a glowing, inviting tent. On the interior, the front of the booth consists of the projection area, a large button to initiate the sequences (hooked into an Arduino microprocessor [code below]), and a bay above the screen holding a DV cam, an SLR camera, and the Arduino. The Arduino also initiates the photographs taken by the SLR camera. The DV cam, which captures the live image and low-res screenshots for the Processing applet displayed on the screen (code below) is attached to a laptop running the applet. The SLR camera is attached to a second laptop, which displays the high-res photographs and photo slideshow outside the booth.
For additional information and an alternate description of this project, please see the following sites:
- https://itp.nyu.edu/projects/beta/projectinfo.php?project_id=1012 http://www.surfaceofearth.org/photobooth/photobooth.html.
- http://itp.nyu.edu/~ds1935/ITPblog/2006/12/physical_computing_final_the_g.html.
Arduino Code
int serialByte = 65; // ascii value to send to processing
int focusPin = 2;
int shutterPin = 3;
int buttonPin = 4;
//int inByte = 0; // incoming serial byte
void setup()
{
// start serial port at 9600 bps:
Serial.begin(9600);
pinMode(focusPin, OUTPUT);
pinMode(shutterPin, OUTPUT);
pinMode(buttonPin, INPUT);
}
void loop(){
// get incoming byte:
// inByte = Serial.read();
if (digitalRead(4) == HIGH){ // if button pushed && processing is sending a byte
Serial.print(serialByte, BYTE);
// start the processing applet
delay(9000); // wait 7 seconds
delay(4000);
digitalWrite(2, HIGH); // begin first set of pictures
delay(1000);
digitalWrite(3, HIGH);
delay(500);
digitalWrite(2,LOW);
digitalWrite(3, LOW);
delay(9000); // begin second set of pictures
delay(5000);
digitalWrite(2, HIGH);
delay(1000);
digitalWrite(3, HIGH);
delay(500);
digitalWrite(2,LOW);
digitalWrite(3, LOW);
delay(9000); // begin third set of pictures
delay(3000);
digitalWrite(2, HIGH);
delay(1000);
digitalWrite(3, HIGH);
delay(500);
digitalWrite(2,LOW);
digitalWrite(3, LOW);
delay(500);
// int inByte = 0; // incoming serial byte
}
}
Processing Code
// Draws a silhouette of the person when they stand against a white background
import processing.video.*; //import the processing video library
import processing.serial.* ;
// Import the Sonia library
import pitaru.sonia_v2_9.*;
// Sample objects (for various popping sounds)
Sample popsound1;
Sample popsound2;
Sample popsound3;
Sample popsound4;
Sample popsound5;
int number; //variable to determine which sound to play
Serial port; // The serial port
int serialIn = 0; // Where we'll put what we receive
//boolean firstContact = false; // Whether we've heard from the microcontroller
boolean restart; // true when button is pressed
Capture video; //capture the camera
ArrayList particles; //define an array for the red particles for first particle-snow effect
int video_width = 640; // video width
int video_height = 480; // video height
int timer = 1; // counts time to start each sequence
long savedTime; //millis timer
long particlesEnd=150; //set the lenth of each sequence
long flysEnd=300;
long balloonsEnd=450;
float t=100; //variable to hold transparency value of particles for fade out
int num_pixels = (video_width * video_height); //variable for iterating through video pixels
PImage bw; //define an image to hold video data for comparison
Fly[] flys = new Fly[15]; //array to hold flys for second effect (flys into butterflys)
PImage f; //fly image
PImage b; //butterfly image
int tintVar=100; //variable for fly/butterfly fading at end of second effect
int tintVar2=100; //varible for balloon fading at end of third effect (before thank you screen)
ArrayList balloons; //define an array to hold the ballons for third effect
PImage balloon_img; //balloon image
PFont font; //initiate font for thank you screen at end
float yoff = 0.0; // for noise var to adjust fly flight
float xoff = 0.0; // for noise var to adjust balloon fall
int photoArray=3; // manually set the number of images (-1) in the photo array for between games
int imageNumber=5; //variable to name screenshot files sequentially -- update with the latest filenumber +1 to avoid overwriting
int screenshotNumber; //variable for screenshot display between games
PImage screenshot; //pimage to hold screenshots
void setup()
{
background(0);
frameRate(60);
size(640, 480, P3D); //set applet size to match video size
video = new Capture(this, video_width, video_height, 60); //setup the video using the default camera
//video.settings();
/*settings:
Adjustments > color > white balance: off
Adjustments > mechanics > focus: manual (get someone in middle and use default
Adjustments > image > Exposure: off
Adjustments > image > Gain: 0
all others at default
*/
bw = createImage(video_width,video_height,RGB); //initiate the black & white image
particles = new ArrayList(); //initiate particles array
balloons = new ArrayList(); //initiate balloons array
screenshot = loadImage("screenshot_1.tif"); //load the initial screenshot for the slideshow
//smooth(); //can't use because of P3D
f = loadImage("fly.png"); //load the fly image into f
b = loadImage("butterfly.png"); //load the butterfly image into b
balloon_img = loadImage("balloon.png"); //load the balloon image into balloon_img
for (int fly_i = 0; fly_i < flys.length; fly_i++) {
flys[fly_i] = new Fly(0,random(-10,video.height),fly_i+3*2.0); //initiate a bunch of flys into the fly array
}
font=loadFont("OptimaLTStd-DemiBold-30.vlw"); //define the font to use on the thank you screen
// Print a list of the serial ports, for debugging purposes:
//println(Serial.list());
// Open serial port (using number based on results of list above)
port = new Serial(this, Serial.list()[0], 9600);
Sonia.start(this); // Start Sonia engine.
// Create new sample objects
popsound1 = new Sample("balloonpop.aif");
popsound2 = new Sample("balloonpop2.aif");
popsound3 = new Sample("balloonpop3.aif");
popsound4 = new Sample("balloonpop4.aif");
popsound5 = new Sample("balloonpop5.aif");
savedTime = millis();
}
void captureEvent(Capture camera) //capture the camera input
{
camera.read();
}
void draw(){
long passedTime = (millis() - savedTime)/100; // determine how much time has passed
int curr_color; // Declare variables to store a pixel's color.
if (timer==0){
for (int i=0; iparticlesEnd-10 && passedTimeparticlesEnd){ //after we're done, remove the particles in the background
if (particles.size()>0){
particles.remove(i);
}
}
else if (passedTime=particlesEnd-5 && passedTimeflysEnd-3){
flys[fly_i].display_out(); //at the end of this sequence, fade the flys & butterflys
}
else {
flys[fly_i].display(); //otherwise, show the flys & butterflys at full transparency
}
flys[fly_i].move(); // move each fly
}
}
if (passedTime>=flysEnd && passedTime= 0; i--) {
// When something comes out of the arraylist via "get" we have to
// remind ourselves what type it is. In this case, it's an instance of "Balloon"
Balloon b = (Balloon) balloons.get(i);
if (passedTime>balloonsEnd-2){
b.display_out(); //at the end of this sequence, fade the balloons
}
else{
b.display(); //otherwise, show the balloons at full transparency
}
b.move(); // move each balloon
if (b.popped(b.x,b.y) && passedTime=balloonsEnd && passedTime=balloonsEnd+10){
timer=1;
}
if (passedTime==balloonsEnd+10){
screenshotNumber=imageNumber-2;
}
if (timer==1 && restart){ //wait for button push to restart
timer=0;
savedTime = millis(); //Save the current time to restart the timer
restart=false; //reset restart
}
/*if (timer==1 && keyPressed){ //REPLACE THIS WITH BUTTON INPUT
timer=0;
savedTime = millis(); //Save the current time to restart the timer
}*/
if (timer==1) { // run the screensaver slideshow between plays
background(0); //required to get rid of bounding rectangles
// If no serial data has beeen received, send again until we get some.
// (in case you tend to start Processing before you start your external device):
// if (firstContact == false) {
// port.write(65); // Send a capital A to start the microcontroller sending
//delay(300);
// }
noTint();
image(screenshot,0,0); //display the current screenshot
for (int i = 0; i < photoArray; i++) {
if (screenshotNumber<=photoArray){ //so long as there are images to show
screenshotNumber++; //increment the screenshot number
screenshot = loadImage("screenshot_"+screenshotNumber+".tif"); //and load the next image
//delay(200);
}
else { //if we're out of images
screenshotNumber=1; //reset the screenshot number to 1
screenshot = loadImage("screenshot_"+screenshotNumber+".tif"); //and restart the slideshow
//delay(200);
}
}
textAlign(CENTER); //create a border at the top of the slideshow with instructions on restarting the booth
noStroke();
fill(35,61,1);
rect(0,400,width,80);
textFont(font,18);
translate(width/2,height/2);
rotateY(radians(180));
translate(-width/2,-height/2);
fill(255);
text("Press the button once to begin the One Minute Photo Booth.",width/2,425);
text("Have fun while a photo is taken during each of the three sequences.",width/2,460);
}
}
// Close the sound engine
public void stop(){
Sonia.stop();
super.stop();
}
// Adaptation of Dropping Particles - Daniel Shiffman - www.shiffman.net - Nov 2006
// class for the snowing particles
class Particle {
float x;
float y;
float xspeed;
float yspeed;
float r;
Particle(float x_, float y_) { //construtor
x = x_;
y = y_;
r = 4;
float theta = random(PI,TWO_PI);
float r = random(.5,1);
xspeed = r*cos(theta);
yspeed = r*sin(theta);
}
void run() { //makes the particles to drop
x = x + xspeed;
y = y + yspeed;
}
void gravity() { //adds speed with time
yspeed += 2.5;
}
void doIstop() { //check to see whether the particles has encountered a black pixel
int xloc = int(x);
int yloc = int(y);
color c = bw.get(xloc,yloc);
if (brightness(c) == 0) { //if a black pixel is found, look for white pixels around it
int[] perimeterCheck = new int[8];
perimeterCheck[0] = bw.get(xloc,yloc-1);
perimeterCheck[1] = bw.get(xloc+1,yloc-1);
perimeterCheck[2] = bw.get(xloc+1,yloc);
perimeterCheck[3] = bw.get(xloc+1,yloc+1);
perimeterCheck[4] = bw.get(xloc,yloc+1);
perimeterCheck[5] = bw.get(xloc-1,yloc+1);
perimeterCheck[6] = bw.get(xloc-1,yloc);
perimeterCheck[7] = bw.get(xloc-1,yloc-1);
//if a white pixel is found and the pixel is moving at a decent speed, bounce in that direction
if (brightness(perimeterCheck[0]) == 0 && yspeed>15 && y<300) {
yspeed *= random(-.5,-.7);
xspeed *= random(-3,3);
y += yspeed;
x += xspeed;
}
else if (brightness(perimeterCheck[1]) == 0 && yspeed>15 && y<300) {
yspeed *= random(-.5,-.7);
xspeed *= random(-1,-2);
y += yspeed;
x += xspeed;
}
else if (brightness(perimeterCheck[2]) == 0 && yspeed>15 && y<300) {
yspeed *= random(-.5,-.7);
xspeed *= random(-.6,-.8);
y += yspeed;
x += xspeed;
}
else if (brightness(perimeterCheck[3]) == 0 && yspeed>15 && y<300) {
yspeed *= random(-.5,-.7);
xspeed *= random(-1,-2);
y += yspeed;
x += xspeed;
}
else if (brightness(perimeterCheck[4]) == 0 && yspeed>15 && y<300) {
yspeed *= random(-.5,-.7);
xspeed *= random(-.6,.8);
y += yspeed;
x += xspeed;
}
else if (brightness(perimeterCheck[5]) == 0 && yspeed>15 && y<300) {
yspeed *= random(-.5,-.7);
xspeed *= random(1,2);
y += yspeed;
x += xspeed;
}
else if (brightness(perimeterCheck[6]) == 0 && yspeed>15 && y<300) {
yspeed *= random(-.5,-.7);
xspeed *= random(.6,.8);
y += yspeed;
x += xspeed;
}
else if (brightness(perimeterCheck[7]) == 0 && yspeed>15 && y<300) {
yspeed *= random(-.5,-.7);
xspeed *= random(2,4);
y += yspeed;
x += xspeed;
}
//if the particle is in the bottom third of the screen and therefore less fun...
else if (y>=300){
//...do nothing & let the particle fall
}
else {
xspeed=0; //if the particle has nearly stopped or is surrounded by black, stop it completely
yspeed=0;
}
}
}
void render() { //define the look of the particle
fill(255,0,0);
noStroke();
ellipse(x,y,r,r);
}
void render_out() { //fade the particle at the end of the sequence
fill(255,0,0,t);
noStroke();
ellipse(x,y,r,r);
t-=.05;
}
boolean finished(float f_x, float f_y) { //removes particles when they pass the edge of the screen
if (f_x > video.width-5 || f_y > video.height-5) {
return true;
}
else {
return false;
}
}
}
class Fly //class for the fly & butterfly sequence
{
float xpos;
float ypos;
float xspeed;
Fly(float xpos_, float ypos_, float xspeed_) { // constructor
xpos = xpos_;
ypos = ypos_;
xspeed = xspeed_;
}
void display(){ //show a fly if the detected pixel is white, or a butterfly if it's black
int xloc = int(xpos);
int yloc = int(ypos);
color c = bw.get(xloc+20,yloc+14); //read color in center of image
if (brightness(c) == 0) {
image(b,xpos,ypos); //butterfly
}
else{
image(f,xpos,ypos); //fly
}
}
void display_out(){ //fade the flys and butterfly at the end of the sequence
int xloc = int(xpos);
int yloc = int(ypos);
color c = bw.get(xloc+20,yloc+14); //read color in center of image
if (brightness(c) == 0) {
tint(255,tintVar);
image(b,xpos,ypos);
}
else{
int tintVar=0;
tint(255,tintVar);
image(f,xpos,ypos);
}
if (tintVar>=0){
tintVar--;
}
else{
tintVar=0;
}
}
void move(){ //determine the fly's and butterfly's movement and flight behavior
int xloc = int(xpos);
int yloc = int(ypos);
color c = bw.get(xloc+20,yloc+14); //read color in center of image
yoff+= 0.03;
float n = noise(yoff)*30;
//if the detected pixel is black and the fly is not near the edge of the screen...
if (brightness(c) == 0 && xpos>150 && xpos video.width) {
xpos = -10;
}
}
}
class Balloon //class for the balloon sequence
{
float x;
float y;
float yspeed;
Balloon(float x_, float y_, float yspeed_) { // constructor
x = x_;
y = y_;
yspeed = yspeed_;
}
void display(){ //show each balloon
image(balloon_img,x,y);
}
void display_out(){ //fade the balloons at the end of the sequence
tint(255,tintVar2);
image(balloon_img,x,y);
if (tintVar2>=0){
tintVar2--;
}
else{
tintVar2=0;
}
}
void move(){ //float the balloons down and a bit towards the right using perlin noise
xoff+= 0.03;
float n = noise(xoff)*3;
y += yspeed;
x+=n;
}
boolean finished(float f_x, float f_y) { //removes the balloons when they pass the edge of the screen
if (f_x > video.width-105 || f_y > video.height-5) {
return true;
}
else {
return false;
}
}
boolean popped(float p_x,float p_y) { //removes the balloons when they encounter a black pixel
int px = int(p_x);
int py = int(p_y);
color c = bw.get(px,py+46);
if (brightness(c) == 0 && py>1) {
return true;
}
else {
return false;
}
}
}
void serialEvent(Serial port) {
// if this is the first byte received,
// take note of that fact:
// if (firstContact == false) {
// firstContact = true;
// }
// read the latest byte
serialIn = port.read();
//println(serialIn);
// If we have a byte:
if (serialIn > 0) {
restart=true;
//timer=0;
// firstContact=false;
}
}