This is a reimplementation of Johny Lee's Automatic Projector Calibration in collaboration with James A. Ferwerda. To make it easier for students to work on this project a sensor board with four IF-D92 was developed that connects to a regular Arduino. Code examples for the Arduino microcontroller and for Processing were also provided.

LevelBinary CalibrationGray Code Calibration
0
1
2
3
4
5
6
7

The calibration images for the y position are similar, but just flipped.

unsigned int binaryToGray(unsigned int number)
{
  return (number >> 1) ^ number;
}

unsigned int grayToBinary(unsigned int gray)
{
  gray ^= gray >> 8; // for 16 bit and more
  gray ^= gray >> 4; // for  8 bit and more
  gray ^= gray >> 2; // for  4 bit and more
  gray ^= gray >> 1; // for  2 bit and more

  return gray;
}
Bit/FrameBlockValue
0 Start White
1 Start Black
2 Start White
3 Start Black
4 Start White
5 Start Black
6 x 10
7 x 9
8 x 8
9 x 7
10 x 6
11 x 5
12 x 4
13 x 3
14 x 2
15 x 1
16 x 0
17 Separator White
18 y 10
19 y 9
20 y 8
21 y 7
22 y 6
23 y 5
24 y 4
25 y 3
26 y 2
27 y 1
28 y 0
29 End White

The Arduino code:

/**
 * Sample.ino
 *
 * James A. Ferwerda and Lars Schumann
 * More information at: http://www.larsi.org/projects/ProjectorCalibration/
 */

// Fast sampling with slightly lower precission
#define FASTADC 1

// defines for setting and clearing register bits
#ifndef cbi
#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
#endif
#ifndef sbi
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))
#endif

/** 3 frames a second = 333ms a frame */
const unsigned long SAMPLE_TIME = 333;

/** default thresholds for all 4 points - these get sampled with the 'l' command */
unsigned int threshold0 = 255;
unsigned int threshold1 = 255;
unsigned int threshold2 = 255;
unsigned int threshold3 = 255;

/** Converts a reflected binary Gray code number to a binary number */
unsigned int grayToBinary(unsigned int gray)
{
  gray ^= gray >> 8; // for 16 bit and more
  gray ^= gray >> 4; // for  8 bit and more
  gray ^= gray >> 2; // for  4 bit and more
  gray ^= gray >> 1; // for  2 bit and more

  return gray;
}

/**
 * This function should be called, whike a white image is displayed.
 * It samples the fibers for a while to find a good threshold value.
 */
void level()
{
  unsigned long sum0 = 0;
  unsigned long sum1 = 0;
  unsigned long sum2 = 0;
  unsigned long sum3 = 0;
  unsigned long cnt  = 0;

  unsigned long t = millis() + SAMPLE_TIME;
  while (millis() < t) {
    sum0 += analogRead(0);
    sum1 += analogRead(1);
    sum2 += analogRead(2);
    sum3 += analogRead(3);
    cnt++;
  }
  Serial.print(cnt);
  Serial.print(": ");

  cnt <<= 1; // double, so that the threshold is set to average / 2

  threshold0 = sum0 / cnt;
  threshold1 = sum1 / cnt;
  threshold2 = sum2 / cnt;
  threshold3 = sum3 / cnt;

  Serial.print(threshold0);
  Serial.print(",");
  Serial.print(threshold1);
  Serial.print(",");
  Serial.print(threshold2);
  Serial.print(",");
  Serial.print(threshold3);
  Serial.println();
}

/**
 * Starts sampling the calibration sequence.  Shifts in x and y and
 * sends the coordinated back.
 */
byte sample()
{
  unsigned int x0 = 0;
  unsigned int y0 = 0;
  unsigned int x1 = 0;
  unsigned int y1 = 0;
  unsigned int x2 = 0;
  unsigned int y2 = 0;
  unsigned int x3 = 0;
  unsigned int y3 = 0;

  unsigned long t = millis();
  for (byte index = 0; index < 30; index++) {
    unsigned int lows0  = 0;
    unsigned int lows1  = 0;
    unsigned int lows2  = 0;
    unsigned int lows3  = 0;
    unsigned int cnt    = 0;

    t += SAMPLE_TIME;
    while (millis() < t) {
      if (analogRead(0) < threshold0) { lows0++; }
      if (analogRead(1) < threshold1) { lows1++; }
      if (analogRead(2) < threshold2) { lows2++; }
      if (analogRead(3) < threshold3) { lows3++; }
      cnt++;
    }
    cnt >>= 1;

    byte current0 = (lows0 < cnt) ? 1 : 0;
    byte current1 = (lows1 < cnt) ? 1 : 0;
    byte current2 = (lows2 < cnt) ? 1 : 0;
    byte current3 = (lows3 < cnt) ? 1 : 0;
    byte all = (current3 << 3) | (current2 << 2) | (current1 << 1) | current0;
    
    if (index == 0 || index == 2 || index == 5 || index == 17 || index == 29) {
      // should be a white frame
      if (all != 0x0F) return index;
    } else if (index == 1 || index == 3 || index == 4) {
      // should be a black frame
      if (all != 0x00) return index;
    } else if (index > 5 && index < 17) {
      // shift in x
      x0 = (x0 << 1) | current0;
      x1 = (x1 << 1) | current1;
      x2 = (x2 << 1) | current2;
      x3 = (x3 << 1) | current3;
    } else if (index > 17 && index < 29) {
      // shift in y
      y0 = (y0 << 1) | current0;
      y1 = (y1 << 1) | current1;
      y2 = (y2 << 1) | current2;
      y3 = (y3 << 1) | current3;
    }
  }

  Serial.print(grayToBinary(x0));
  Serial.print(",");
  Serial.print(grayToBinary(y0));
  Serial.print(",");
  Serial.print(grayToBinary(x1));
  Serial.print(",");
  Serial.print(grayToBinary(y1));
  Serial.print(",");
  Serial.print(grayToBinary(x2));
  Serial.print(",");
  Serial.print(grayToBinary(y2));
  Serial.print(",");
  Serial.print(grayToBinary(x3));
  Serial.print(",");
  Serial.print(grayToBinary(y3));
  Serial.println();

  return 30;
}

void setup()
{
  // serial port speed - must match computers settings
  Serial.begin(115200);

#if FASTADC
  // set prescale to 16
  sbi(ADCSRA,ADPS2);
  cbi(ADCSRA,ADPS1);
  cbi(ADCSRA,ADPS0);
#endif
}

void loop()
{
  if (Serial.available()) {
    char c = Serial.read();

    if (c == 'l') {
      level();
    } else if (c == 's') {
      byte cnt = sample();
      if (cnt < 30) {
        Serial.print("-1,");
        Serial.print((int)cnt);
        Serial.print(",0,0,0,0,0,0");
        Serial.println();
      }
    }
  }
}

Processing/Java example code:

/**
 * GrayCodeImage.java
 *
 * James A. Ferwerda and Lars Schumann
 * More information at: http://www.larsi.org/projects/ProjectorCalibration/
 */

package org.larsi.jim;

import org.larsi.codes.Binary2Gray;
import org.larsi.protocols.serial.Serial;

import processing.core.PApplet;
import processing.core.PImage;

@SuppressWarnings("serial")
public class GrayCodeImage extends PApplet
{
  /** serial port speed - must match Arduino settings */
  static int BAUD = 115200;

  /** true means gray code, false means binary code */
  static boolean doImage = false;

  /** true means gray code, false means binary code */
  static boolean grayCode = true;

  static int x0 = 0;
  static int y0 = 0;
  static int x1 = 0;
  static int y1 = 0;
  static int x2 = 0;
  static int y2 = 0;
  static int x3 = 0;
  static int y3 = 0;

  static Serial serial;

  static PImage img;

  public static String getLine()
  {
    String temp = null;

    while ((temp = serial.readStringUntil('\n') ) == null) {
    }

    return temp.replaceAll("[\n\r]", "");
  }

  /** Processing: setup() */
  @Override
  public void setup()
  {
    size(1024, 768);
    //size(1024, 768, P3D);
    frameRate(3);

    serial = new Serial(null, Serial.getSerialPort(), BAUD);
    try {
      Thread.sleep(200);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }


    img = loadImage("berlin-1.jpg");
  }

  /** Processing: draw() */
  @Override
  public void draw()
  {
    final int f = (frameCount - 1) % 50;

    // start sampling
    if (f == 0)
      serial.write('s');

    if (f == 0 || f == 2 || f == 5 || f == 17 || f == 29) {
      // white frame
      background(0xFFFFFFFF);
    } else if (f == 1 || f == 3 || f == 4 || f > 46) {
      // black frame
      background(0xFF000000);
    } else if (f > 5 && f < 17) {
      background(0xFF000000);
      // shifting x out
      int i = 16 - f;
      int mask = 1 << i;
      for (int x = 0; x < width; x++) {
        int g = grayCode ? Binary2Gray.binaryToGray(x) : x;
        stroke((g & mask) > 0 ? 0xFFFFFFFF : 0xFF000000);
        line(x, 0, x, height);
      }
    } else if (f > 17 && f < 29) {
      background(0xFF000000);
      // shifting y out
      int i = 28 - f;
      int mask = 1 << i;
      for (int y = 0; y < height; y++) {
        int g = grayCode ? Binary2Gray.binaryToGray(y) : y;
        stroke((g & mask) > 0 ? 0xFFFFFFFF : 0xFF000000);
        line(0, y, width, y);
      }
    } else if (f == 30) {
      // collecting results
      background(0xFF000000);
      String[] elements = getLine().split(",");
      if (elements.length != 8)
        System.out.println("???");

      x0 = Integer.parseInt(elements[0]);
      y0 = Integer.parseInt(elements[1]);
      x1 = Integer.parseInt(elements[2]);
      y1 = Integer.parseInt(elements[3]);
      x2 = Integer.parseInt(elements[4]);
      y2 = Integer.parseInt(elements[5]);
      x3 = Integer.parseInt(elements[6]);
      y3 = Integer.parseInt(elements[7]);

      System.out.println(x0 + "," + y0);
      System.out.println(x1 + "," + y1);
      System.out.println(x2 + "," + y2);
      System.out.println(x3 + "," + y3);
    } else {
      // show found corner points
      background(0xFF000000);
      stroke(0xFFFFFFFF);

      if (doImage) {
        beginShape();
        texture(img);
        vertex(x0, y0,         0,          0);
        vertex(x1, y1, img.width,          0);
        vertex(x3, y3, img.width, img.height);
        vertex(x2, y2,         0, img.height);
        endShape();
      }
      line(x0, y0, x1, y1);
      line(x1, y1, x3, y3);
      line(x3, y3, x2, y2);
      line(x2, y2, x0, y0);
    }

    // save calibration image
    //if (f < 30) save((grayCode ? "gray" : "binary") + "_" + f + ".png");
  }
}

Watch (running time 00:20)

This movie shows the first test, with the fiber directly on the screen.