« pComp Final-Observation | Main | White Balance, Depth of Field & Manual Focus »

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:

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;
  }
}

TrackBack

TrackBack URL for this entry:
http://itp.nyu.edu/~km63/cgi-bin/mt/mt-tb.cgi/16

Post a comment

(If you haven't left a comment here before, you may need to be approved by the site owner before your comment will appear. Until then, it won't appear on the entry. Thanks for waiting.)