Skip to content
Snippets Groups Projects
mapper2d.pde 5.72 KiB
Newer Older
/*
 * Using a camera, approximate the 2-D location of the area illuminated by each LED.
 * Instead of illuminating each LED in sequence, this uses a binary pattern to quickly
 * identify large numbers of LEDs with few test patterns.
 *
 * This is still really experimental! Currently it's just a minimal proof-of-concept :)
 *
 * Micah Elizabeth Scott, 2013
 */

import processing.video.*;

//int MAX_PIXELS = 0xFFFF / 3;    // Maximum number of pixels
int MAX_PIXELS = 16;
int BRIGHTNESS = 130;           // How bright should our test pattern be?
float MSE_THRESHOLD = 30;       // Mean squared error below which frames are "identical"

int S_WAITING    = 0;
int S_START_BIT  = 1;
int S_SETTLE_BIT = 2;
int S_FINALIZE = 3;

int state;
int bit;
int numBitsInUse;
OPCLowLevel opc;
Capture video;
int[][] bitImages;

void setup()
{
  size(640, 480);
  video = new Capture(this, width, height);
  video.start();

  bitImages = new int[16][width * height];

  opc = new OPCLowLevel("127.0.0.1", 7890, MAX_PIXELS);
  opc.firmwareConfig = 0x07;  // Disable dithering, interpolation, and status LED
}

void allPixelsOff()
{
  for (int i = 0; i < MAX_PIXELS; i++) {
    opc.setPixel(i, 0, 0, 0);
  }
  opc.sendPixels();
}  

void sendBitPattern()
{
  // Send an LED pattern in which red/blue correspond to 1/0 for the indicated bit position.
  for (int i = 0; i < MAX_PIXELS; i++) {
    boolean b = 0 != ((i >> bit) & 1);
    opc.setPixel(i, b ? BRIGHTNESS : 0, 0, b ? 0 : BRIGHTNESS);
  }
  opc.sendPixels();
}

void keyPressed()
{
  if (key == ' ' && state == S_WAITING) {
    // Start!
    bit = 0;
    state = S_START_BIT;
  }
  if (keyCode == DELETE || keyCode == BACKSPACE) {
    // Start over
    state = S_WAITING;
  }
}

void sText(String s, int x, int y)
{
  // Text with a shadow
  fill(0);
  text(s, x+2, y+2);
  fill(255);
  text(s, x, y);
}

float frameDifference(int[] a, int[] b)
{
  // Mean squared distance between frames
  float total = 0;
  for (int i = 0; i < a.length; i++) {
    int aPixel = a[i];
    int bPixel = b[i];
    int diffR = ((aPixel >> 16) & 0xFF) - ((bPixel >> 16) & 0xFF);
    int diffG = ((aPixel >> 8) & 0xFF) - ((bPixel >> 8) & 0xFF);
    int diffB = (aPixel & 0xFF) - (bPixel & 0xFF);
    total += diffR*diffR + diffG*diffG + diffB*diffB;
  }
  return total / (width * height);
}

PVector bitSimilarityImage(int[] output, int pattern)
{
  /*
   * Generate an image which maps how similar each pixel is, across all bits, to
   * the indicated bit pattern. One way to think of this: we're calculating a
   * probability that each pixel in question was in fact pointed at the pattern
   * we're looking for. Each bit gives us a single probability, and those probabilities
   * are multiplied together to give a pattern probability.
   *
   * Returns the weighted centroid of the image.
   */

  float xTotal = 0, xWeight = 0;
  float yTotal = 0, yWeight = 0;

  for (int i = 0; i < output.length; i++) {
    int x = i % width;
    int y = i / width;
    float probability = 1.0;
    
    for (int b = 0; b < numBitsInUse; b++) {
      int pixel = bitImages[b][i];
      boolean patternBit = 1 == ((pattern >> b) & 1);
      
      // We might want to do something smarter here, like histogram backprojection
      // based on actual colors sampled from the device. This is pretty stupid.
      
      int diffR = (patternBit ? 0xFF : 0x00) - ((pixel >> 16) & 0xFF);
      int diffB = (patternBit ? 0x00 : 0xFF) - (pixel & 0xFF);  
      probability /= 1 + (diffR*diffR + diffB*diffB) * 1e-4;
    }

    // Update weighted centroid
    float w = probability * probability;
    xTotal += x * w;
    xWeight += w;
    yTotal += y * w;
    yWeight += w;

    int gray = constrain(int(probability * 1e3), 0, 255);
    output[i] = gray | (gray << 8) | (gray << 16);
  }
  
  return new PVector(xTotal / xWeight, yTotal / yWeight);
}

void showFrameSequence()
{
  // Show our captured frames in sequence
  int i = (millis() / 500) % numBitsInUse;
  loadPixels();
  arrayCopy(bitImages[i], pixels);
  updatePixels();
  textSize(25);
  sText("Bit " + i, 10, 30);
}

void showPatternSequence()
{
  // Show pattern similarity measurements in sequence
  int i = (millis() / 500) % MAX_PIXELS;
  loadPixels();
  PVector peak = bitSimilarityImage(pixels, i);
  updatePixels();
  textSize(25);
  sText("Similarity to pattern " + i, 10, 30);

  // Draw crosshairs at the peak probability location
  stroke(128, 255, 128);
  int size = 100;
  line(peak.x, peak.y - size, peak.x, peak.y + size);
  line(peak.x - size, peak.y, peak.x + size, peak.y);
}

void draw()
{
  if (state == S_FINALIZE) {
    if (mousePressed)
      showFrameSequence();
    else
      showPatternSequence();
    return;
  }

  if (!video.available())
    return;
  video.read();
  image(video, 0, 0, width, height);
  video.loadPixels();

  textSize(25);
  if (state == S_WAITING) {
    sText("Press [SPACE] to start mapping", 10, 30);
    allPixelsOff();
  } else {
    sText("Mapping bit " + bit + "...", 10, 30);
  }
  textSize(16);

  if (state == S_START_BIT) {
    // Update the LEDs, and discard this video frame.
    sendBitPattern();
    state = S_SETTLE_BIT;
  } else if (state == S_SETTLE_BIT) {
    // Keep capturing frames until the frames are similar enough.
    // This gives the camera's brightness control time to settle.
    float d = frameDifference(video.pixels, bitImages[bit]);
    sText("Settling... MSE: " + d, 10, 60);
    arrayCopy(video.pixels, bitImages[bit]);

    if (d < MSE_THRESHOLD) {
      // Settled enough! Ready to move on.

      if (((MAX_PIXELS - 1) >> (bit + 1) == 0)) {
        // No more pixels possible beyond this bit. We're done.
        numBitsInUse = bit + 1;
        state = S_FINALIZE;

      } else {
        // More bits
        bit++;
        state = S_START_BIT;
      }
    }     
  }
}