/*
* @(#) TopCode.java
*
* Tangible Object Placement Codes (topcodes)
* Copyright (c) 2007 Michael S. Horn
*
* Michael S. Horn (michael.horn@tufts.edu)
* Tufts University Computer Science
* 161 College Ave.
* Medford, MA 02155
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License (version 2) as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
/*
* This is original as downloaded, plus annotations added in comments --RJ
*/
package topcodes;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.geom.Arc2D;
import java.awt.geom.Line2D;
import java.awt.geom.Ellipse2D;
/**
* TopCodes (Tangible Object Placement Codes) are black-and-white
* circular fiducials designed to be recognized quickly by
* low-resolution digital cameras with poor optics. The TopCode symbol
* format is based on the open SpotCode format:
*
* http://www.highenergymagic.com/spotcode/symbols.html
*
* Each TopCode encodes a 13-bit number in a single data ring on the
* outer edge of the symbol. Zero is represented by a black sector and
* one is represented by a white sector.
*
* @author Michael Horn
* @version $Revision: 1.2 $, $Date: 2007/01/27 01:58:17 $
*/
public class TopCode {
/** Number of sectors in the data ring */
protected static int SECTORS = 13;
/** Width of the code in units (ring widths) */
protected static int WIDTH = 8;
protected static float PI = (float)Math.PI;
/** Span of a data sector in radians */
protected static float ARC = (2 * PI / SECTORS);
/** The symbol's code, or -1 if invalid. */
protected int code;
/** The width of a single ring. */
protected float unit;
/** The angular orientation of the symbol (in radians) */
protected float orientation;
/** Horizontal center of a symbol */
protected float x;
/** Vertical center of a symbol */
protected float y;
/** Buffer used to decode sectors */
protected int [] core;
public TopCode() {
this.code = -1;
this.unit = 72.0f / WIDTH;
this.orientation = 0;
this.x = 0;
this.y = 0;
this.core = new int[WIDTH];
}
public TopCode(int code) {
this();
this.code = code;
}
//-----------------------------------------------------------------
// Returns the value for this symbol. Calling the decode()
// function will set this value automatically.
//-----------------------------------------------------------------
public int getCode() {
return this.code;
}
public void setCode(int code) {
this.code = code;
}
//-----------------------------------------------------------------
// Returns the orientation of this code in radians and accurate
// to about plus or minus one degree. This value gets set
// automatically by the decode() function.
//-----------------------------------------------------------------
public float getOrientation() {
return this.orientation;
}
public void setOrientation(float orientation) {
this.orientation = orientation;
}
//-----------------------------------------------------------------
// Returns the diameter of this code in pixels. This value
// will be set automatically by the decode() function.
//-----------------------------------------------------------------
public float getDiameter() {
return this.unit * WIDTH;
}
public void setDiameter(float diameter) {
this.unit = diameter / WIDTH;
}
//-----------------------------------------------------------------
// x and y-coordinate for the center point of the symbol. This
// value will be set automatically by the decode() function.
//-----------------------------------------------------------------
public float getCenterX() {
return this.x;
}
public float getCenterY() {
return this.y;
}
public void setLocation(float x, float y) {
this.x = x;
this.y = y;
}
//-----------------------------------------------------------------
// Returns true if this code was sucessfully decoded.
//-----------------------------------------------------------------
public boolean isValid() {
return this.code > 0;
}
//-----------------------------------------------------------------
// Returns true if the given point is inside the bulls-eye
//-----------------------------------------------------------------
public boolean inBullsEye(float px, float py) {
return (((x - px) * (x - px) + (y - py) * (y - py)) <=
(unit * unit));
}
//-----------------------------------------------------------------
// Decodes a symbol given any point (cx, cy) inside the center
// circle (bulls-eye) of the code.
//-----------------------------------------------------------------
public int decode(Scanner scanner, int cx, int cy) { //*1 Decode a symbol whose center contains cx,cy
int up = scanner.ydist(cx, cy, -1);
int down = scanner.ydist(cx, cy, 1);
int left = scanner.xdist(cx, cy, -1);
int right = scanner.xdist(cx, cy, 1);
this.x = cx;
this.y = cy;
this.x += (right - left) / 2.0f;
this.y += (down - up) / 2.0f;
this.unit = readUnit(scanner);
this.code = -1;
if (unit < 0) return -1;
float c = 0;
float maxc = 0;
float maxa = 0;
float maxu = 0;
//-----------------------------------------
// Try different unit and arc adjustments,
// save the one that produces a maximum
// confidence reading...
//-----------------------------------------
for (int u = -1; u <= 1; u++) { //*2 Try different unit (size) and arc (rotation) for max confidence
for (int a = 0; a < 10; a++) { //*2
c = readCode(scanner, //*2
unit + (unit * 0.05f * u), //*2
a * ARC * 0.1f); //*2
if (c > maxc) { //*2
maxc = c; //*2
maxa = a * ARC * 0.1f;
maxu = unit + (unit * 0.05f * u);
}
}
}
// One last call to readCode to reset orientation and code
if (maxc > 0) {
unit = maxu;
readCode(scanner, unit, maxa);
this.code = rotateLowest(code, maxa);
}
return this.code;
}
//-----------------------------------------------------------------
// Attempts to decode the binary pixels of an image into a code
// value.
//
// scanner - image scanner
// unit - width of a single ring (codes are 8 units wide)
// arca - Arc adjustment. Rotation correction delta value.
//-----------------------------------------------------------------
protected float readCode(Scanner scanner, //*3 Attempt to read a code from given pixels
float unit,
float arca) {
float dx, dy; // direction vector
float dist;
int c = 0;
int sx, sy;
int bit, bits = 0;
this.code = -1;
for (int sector = (SECTORS-1) * 2; sector >= 0; sector--) {
dx = (float)Math.cos(ARC * 0.5 * sector + arca);
dy = (float)Math.sin(ARC * 0.5 * sector + arca);
for (int i=0; i<WIDTH; i++) { //*4 Take 8 samples across the diameter of the symbol
dist = (i - 3.5f) * unit; //*4
sx = (int)Math.round(x + dx * dist);
sy = (int)Math.round(y + dy * dist);
core[i] = scanner.getSample3x3(sx, sy); //*4
}
if (core[1] <= 128 || core[3] <= 128 || //*5 white rings
core[4] <= 128 || core[6] <= 128) { //*5
return 0; //*5
}
if (core[2] > 128 || core[5] > 128) { //*6 black ring
return 0; //*6
}
// compute confidence in core sample
c += (core[1] + core[3] + core[4] + core[6] + // white rings
(0xff - core[2]) + (0xff - core[5])); // black ring
// data rings
c += Math.abs(core[7] * 2 - 0xff);
// opposite data ring
c += (0xff - Math.abs(core[0] * 2 - 0xff));
bit = (core[7] > 128)? 1 : 0; //*7 Figure out binary number code
if (sector % 2 == 0) { //*7
bits <<= 1; //*7
bits += bit; //*7
} else if (bit != (bits & 0x01)) {
return 0;
}
}
if (checksum(bits)) {
this.code = bits;
return (c / (WIDTH * SECTORS * 255));
} else {
return 0;
}
}
//-----------------------------------------------------------------
// rotateLowest() tries each of the possible rotations and returns
// the lowest.
//-----------------------------------------------------------------
protected int rotateLowest(int bits, float arca) {
int min = bits;
int mask = 0x1fff;
this.orientation = arca - (ARC * 0.5f);
for (int i=0; i<SECTORS; i++) {
bits = (((bits << 1) & mask) |
(bits >> (SECTORS - 1)));
if (bits < min) {
min = bits;
this.orientation = ((i+1) * -ARC) + arca - (ARC * 0.5f);
}
}
return min;
}
//-----------------------------------------------------------------
// Codes with a checksum of 5 are valid
//-----------------------------------------------------------------
protected boolean checksum(int bits) {
int sum = 0;
for (int i=0; i<SECTORS; i++) {
sum += (bits & 0x01);
bits = bits >> 1;
}
return (sum == 5);
}
//-----------------------------------------------------------------
// Determines the symbol's unit length by counting the number
// of pixels between the outer edges of the first black ring.
// North, south, east, and west readings are taken and the average
// is returned.
//-----------------------------------------------------------------
protected float readUnit(Scanner scanner) {
int sx = (int)Math.round(x);
int sy = (int)Math.round(y);
int iwidth = scanner.getImageWidth();
int iheight = scanner.getImageHeight();
boolean whiteL = true;
boolean whiteR = true;
boolean whiteU = true;
boolean whiteD = true;
int sample;
int distL = 0, distR = 0, distU = 0, distD = 0;
for (int i=1; true; i++) {
if (sx - i < 1 || sx + i >= iwidth - 1 ||
sy - i < 1 || sy + i >= iheight - 1 ||
i > 100) {
return -1;
}
// Left sample
sample = scanner.getBW3x3(sx - i, sy);
if (distL <= 0) {
if (whiteL && sample == 0) {
whiteL = false;
} else if (!whiteL && sample == 1) {
distL = i;
}
}
// Right sample
sample = scanner.getBW3x3(sx + i, sy);
if (distR <= 0) {
if (whiteR && sample == 0) {
whiteR = false;
} else if (!whiteR && sample == 1) {
distR = i;
}
}
// Up sample
sample = scanner.getBW3x3(sx, sy - i);
if (distU <= 0) {
if (whiteU && sample == 0) {
whiteU = false;
} else if (!whiteU && sample == 1) {
distU = i;
}
}
// Down sample
sample = scanner.getBW3x3(sx, sy + i);
if (distD <= 0) {
if (whiteD && sample == 0) {
whiteD = false;
} else if (!whiteD && sample == 1) {
distD = i;
}
}
if (distR > 0 && distL > 0 && distU > 0 && distD > 0) {
float u = (distR + distL + distU + distD) / 8.0f;
if (Math.abs(distR + distL - distU - distD) > u) {
return -1;
} else {
return u;
}
}
}
}
public void annotate(Graphics2D g, Scanner scanner) {
float dx, dy;
float dist;
int sx, sy;
int bits = 0;
for (int sector=SECTORS-1; sector>=0; sector--) {
dx = (float)Math.cos(ARC * sector + orientation);
dy = (float)Math.sin(ARC * sector + orientation);
// Take 8 samples across the diameter of the symbol
int sample = 0;
for (int i=3; i<WIDTH; i++) {
dist = (i - 3.5f) * unit;
sx = (int)Math.round(x + dx * dist);
sy = (int)Math.round(y + dy * dist);
sample = scanner.getSample3x3(sx, sy);
g.setColor(new Color(sample, sample, sample));
g.fillRect(sx - 1, sy - 1, 3, 3);
g.setColor(Color.RED);
g.drawRect(sx - 1, sy - 1, 3, 3);
}
bits <<= 1;
if (sample > 128) bits += 1;
}
bits = rotateLowest(bits, 0);
}
//-----------------------------------------------------------------
// Draws this spotcode
//-----------------------------------------------------------------
public void draw(Graphics2D g) {
int bits = this.code;
Arc2D arc = new Arc2D.Float(Arc2D.PIE);
float sweep = 360.0f / SECTORS;
float sweepa = (-orientation * 180 / PI);
float r = WIDTH * 0.5f * unit;
Ellipse2D circ = new Ellipse2D.Float(
x - r, y - r, r * 2, r * 2);
g.setColor(Color.white);
g.fill(circ);
for (int i=SECTORS-1; i>=0; i--) {
arc.setArc(x - r, y - r, r * 2, r * 2,
i * sweep + sweepa, sweep, Arc2D.PIE);
g.setColor(((bits & 0x1) > 0)? Color.white : Color.black);
g.fill(arc);
bits >>= 1;
}
r -= unit;
g.setColor(Color.white);
circ.setFrame(x - r, y - r, r * 2, r * 2);
g.fill(circ);
r -= unit;
g.setColor(Color.black);
circ.setFrame(x - r, y - r, r * 2, r * 2);
g.fill(circ);
r -= unit;
g.setColor(Color.white);
circ.setFrame(x - r, y - r, r * 2, r * 2);
g.fill(circ);
}
//-----------------------------------------------------------------
// Debug routine that prints the 44 least significant bits of a
// integer.
//-----------------------------------------------------------------
protected void printBits(int bits) {
for (int i=SECTORS-1; i>=0; i--) {
if (((bits>>i) & 0x01) == 1) {
System.out.print("1");
} else {
System.out.print("0");
}
if ((44 - i) % 4 == 0) {
System.out.print(" ");
}
}
System.out.println(" = " + bits);
}
//-----------------------------------------------------------------
// Generates a list of n codes of value greater than or equal to
// base.
//-----------------------------------------------------------------
public static TopCode [] generateCodes() {
int n = 99;
TopCode [] list = new TopCode[n];
TopCode code = new TopCode();
int bits;
int base = 0;
int count = 0;
while (count < n) {
bits = code.rotateLowest(base, 0);
// Found a valid code
if (bits == base && code.checksum(bits)) {
code.setCode(bits);
code.setOrientation(0);
list[count++] = code;
code = new TopCode();
}
// Try next value
base++;
}
return list;
}
public static void main(String args[]) throws Exception {
TopCode [] codes = generateCodes();
int code;
for (int i=0; i<codes.length; i++) {
code = codes[i].getCode();
//System.out.println(code);
codes[i].printBits(code);
}
System.out.println(codes.length + " codes.");
}
}