/*
* @(#) Scanner.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.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.FileInputStream;
import com.sun.image.codec.jpeg.*;
import java.util.List;
/**
* Loads and scans images for TopCodes. The algorithm does a
* single sweep of an image (scanning one horizontal line at a time)
* looking for a pattern pattern: WHITE BLACK WHITE BLACK WHITE. If
* the pattern matches and the black and white regions meet certain
* ratio constraints, then the pixel is tested as the center of a
* candidate TopCode.
*
* To extract a list of TopCodes from an image follow these steps:
*
* 1) Create a new Scanner object (this may be reused)
*
* 2) Call the getImage() or setImage() function to load an image
*
* 3) Call the doThreshold() method to convert the image to black and
* white.
*
* 4) Call the findCodes() method to get a list of TopCodes objects
* extracted from the image.
*
* @author Michael Horn
* @version $Revision: 1.3 $, $Date: 2007/01/31 19:50:45 $
*/
public class Scanner {
/** Original JPG image */
protected BufferedImage image;
/** Total width of image */
protected int w;
/** Total height of image */
protected int h;
/** Holds binary image data */
protected int[] data;
/** Binary view of the image */
protected BufferedImage preview;
//-----------------------------------------------------------------
// Default constructor
//-----------------------------------------------------------------
public Scanner() {
clear();
}
public void clear() {
this.image = null;
this.w = 0;
this.h = 0;
this.data = null;
this.preview = null;
}
//-----------------------------------------------------------------
// Original (unaltered) image
//-----------------------------------------------------------------
public BufferedImage getImage() {
return this.image;
}
//-----------------------------------------------------------------
// Load pixel data from JPG file.
//-----------------------------------------------------------------
public void setImage(BufferedImage image) {
this.image = image;
this.w = image.getWidth();
this.h = image.getHeight();
this.data = image.getRGB(0, 0, w, h, null, 0, w);
}
//-----------------------------------------------------------------
// Load pixel data from a JPG file.
//-----------------------------------------------------------------
public void loadImage(String file) throws IOException {
FileInputStream in = new FileInputStream(file);
JPEGImageDecoder decoder = JPEGCodec.createJPEGDecoder(in);
setImage(decoder.decodeAsBufferedImage());
in.close();
}
//-----------------------------------------------------------------
// Perform Wellner adaptive thresholding to produce binary pixel
// data. Also mark candidate spotcode locations.
//
// "Adaptive Thresholding for the DigitalDesk"
// EuroPARC Technical Report EPC-93-110
//-----------------------------------------------------------------
public void doThreshold() {
int pixel, r, g, b, a;
int threshold, sum = 128;
int s = 30;
int k;
int b1, w1, b2, level, dk;
for (int j=0; j<h; j++) { //*1 Process horizontal rows
level = b1 = b2 = w1 = 0;
//----------------------------------------
// Process rows back and forth (alternating
// left-to-right, right-to-left)
//----------------------------------------
k = (j % 2 == 0) ? 0 : w-1; //*1
k += (j * w); //*1
for (int i=0; i<w; i++) { //*1
//----------------------------------------
// Calculate pixel intensity (0-255)
//----------------------------------------
pixel = data[k]; //*2 Convert RGB to grey scale, then threshold to black or white
r = (pixel >> 16) & 0xff; //*2
g = (pixel >> 8) & 0xff; //*2
b = pixel & 0xff; //*2
a = (r + g + b) / 3; //*2
//a = r;
//----------------------------------------
// Calculate sum as an approximate sum
// of the last s pixels
//----------------------------------------
sum += a - (sum / s);
//----------------------------------------
// Factor in sum from the previous row
//----------------------------------------
if (k >= w) {
threshold = (sum + (data[k-w] & 0xffffff)) / (2*s);
} else {
threshold = sum / s;
}
//----------------------------------------
// Compare the average sum to current pixel
// to decide black or white
//----------------------------------------
double f = 0.85;
f = 0.975;
a = (a < threshold * f)? 0 : 1; //*2
//----------------------------------------
// Repack pixel data with binary data in
// the alpha channel, and the running sum
// for this pixel in the RGB channels
//----------------------------------------
data[k] = (a << 24) + (sum & 0xffffff);
switch (level) {
case 0: //*3 Scanning: On a white region. No black pixels yet
if (a == 0) { // First black encountered //*3
level = 1; //*3
b1 = 1; //*3
w1 = 0; //*3
b2 = 0; //*3
}
break;
case 1: //*4 Scanning: On first black region
if (a == 0) { //*4
b1++; //*4
} else {
level = 2;
w1 = 1;
}
break;
case 2: //*5 Scanning: On second white region (bulls-eye of a code?)
if (a == 0) { //*5
level = 3; //*5
b2 = 1;
} else {
w1++;
}
break;
case 3: //*6 Scanning: On second black region
if (a == 0) { //*6
b2++; //*6
}
else { //*7 This could be a top code
int mask; //*7
if (Math.abs(b1 + b2 - w1) <= b1 &&
Math.abs(b1 + b2 - w1) <= b2 &&
Math.abs(b1 + b2 - w1) <= w1 &&
Math.abs(b1 - b2) < b1/2.0 &&
Math.abs(b1 - b2) < b2/2.0) {
mask = 0x2000000; //*7
dk = 1 + b2 + w1/2;
if (j % 2 == 0) {
dk = k - dk;
} else {
dk = k + dk;
}
data[dk - 1] |= mask; //*7
data[dk] |= mask; //*7
data[dk + 1] |= mask; //*7
}
b1 = b2; //*7
w1 = 1; //*7
b2 = 0; //*7
level = 2; //*7
}
break;
}
k += (j % 2 == 0) ? 1 : -1;
}
}
}
//-----------------------------------------------------------------
// Scan the image line by line looking for TopCodes
//-----------------------------------------------------------------
public List findCodes() {
List spots = new java.util.ArrayList();
TopCode spot = new TopCode();
int k = w * 2;
for (int j=2; j<h-2; j++) {
for (int i=0; i<w; i++) {
if ((data[k] & 0x2000000) > 0 &&
(data[k-1] & 0x2000000) > 0 &&
(data[k+1] & 0x2000000) > 0 &&
(data[k-w] & 0x2000000) > 0 &&
(data[k+w] & 0x2000000) > 0) {
if (!overlaps(spots, i, j)) {
spot.decode(this, i, j);
if (spot.isValid()) {
spots.add(spot);
spot = new TopCode();
}
}
}
k++;
}
}
return spots;
}
//-----------------------------------------------------------------
// Returns the result of the adaptive thresholding algorithm as a
// black and white image.
//-----------------------------------------------------------------
public BufferedImage getPreview() {
if (this.preview != null) return preview;
this.preview =
new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
int pixel = 0;
int k = 0;
for (int j=0; j<h; j++) {
for (int i=0; i<w; i++) {
pixel = (data[k++] >> 24);
if (pixel == 0) {
pixel = 0xFF000000;
} else if (pixel == 1) {
pixel = 0xFFFFFFFF;
} else if (pixel == 3) {
pixel = 0xFF00FF00;
} else if (pixel == 7) {
pixel = 0xFFFF0000;
}
this.preview.setRGB(i, j, pixel);
}
}
return preview;
}
public int getImageWidth() {
return this.w;
}
public int getImageHeight() {
return this.h;
}
//-----------------------------------------------------------------
// Binary (thresholded black/white) value for pixel (x,y)
//-----------------------------------------------------------------
protected int getBW(int x, int y) {
int pixel = data[y * w + x];
return (pixel >> 24) & 0x01;
}
//-----------------------------------------------------------------
// Average of thresholded pixels in a 3x3 region around (x,y).
// Returned value is between 0 (black) and 255 (white).
//-----------------------------------------------------------------
protected int getSample3x3(int x, int y) {
if (x < 1 || x > w-2 || y < 1 || y >= h-2) return 0;
int pixel, sum = 0;
for (int j=y-1; j<=y+1; j++) {
for (int i=x-1; i<=x+1; i++) {
pixel = data[j * w + i];
if ((pixel & 0x01000000) > 0) {
sum += 0xff;
}
}
}
//return (sum >= 5) ? 1 : 0;
return (sum / 9);
}
//-----------------------------------------------------------------
// Average of thresholded pixels in a 3x3 region around (x,y).
// Returned value is either 0 (black) or 1 (white).
//-----------------------------------------------------------------
protected int getBW3x3(int x, int y) {
if (x < 1 || x > w-2 || y < 1 || y >= h-2) return 0;
int pixel, sum = 0;
for (int j=y-1; j<=y+1; j++) {
for (int i=x-1; i<=x+1; i++) {
pixel = data[j * w + i];
sum += ((pixel >> 24) & 0x01);
}
}
return (sum >= 5) ? 1 : 0;
}
//-----------------------------------------------------------------
// Returns true if point (x,y) is in an existing TopCode bullseye
//-----------------------------------------------------------------
protected boolean overlaps(List spots, int x, int y) {
TopCode spot;
for (int i=0; i<spots.size(); i++) {
spot = (TopCode)spots.get(i);
if (spot.inBullsEye(x, y)) return true;
}
return false;
}
//-----------------------------------------------------------------
// Counts the number of vertical pixels from (x,y) until a color
// change is perceived.
//-----------------------------------------------------------------
protected int ydist(int x, int y, int d) {
int sample;
int start = getBW3x3(x, y);
for (int j=y+d; j>1 && j<h-1; j+=d) {
sample = getBW3x3(x, j);
if (start + sample == 1) {
return (d > 0) ? j - y : y - j;
}
}
return -1;
}
//-----------------------------------------------------------------
// Counts the number of horizontal pixels from (x,y) until a color
// change is perceived.
//-----------------------------------------------------------------
protected int xdist(int x, int y, int d) {
int sample;
int start = getBW3x3(x, y);
for (int i=x+d; i>1 && i<w-1; i+=d) {
sample = getBW3x3(i, y);
if (start + sample == 1) {
return (d > 0) ? i - x : x - i;
}
}
return -1;
}
}