/* * @(#) 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> 16) & 0xff; g = (pixel >> 8) & 0xff; b = pixel & 0xff; a = (r + g + b) / 3; //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; //---------------------------------------- // 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: if (a == 0) { // First black encountered level = 1; b1 = 1; w1 = 0; b2 = 0; } break; case 1: if (a == 0) { b1++; } else { level = 2; w1 = 1; } break; case 2: if (a == 0) { level = 3; b2 = 1; } else { w1++; } break; case 3: if (a == 0) { b2++; } else { int mask; 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; dk = 1 + b2 + w1/2; if (j % 2 == 0) { dk = k - dk; } else { dk = k + dk; } data[dk - 1] |= mask; data[dk] |= mask; data[dk + 1] |= mask; } b1 = b2; w1 = 1; b2 = 0; level = 2; } 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 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> 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; i1 && j 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 0) ? i - x : x - i; } } return -1; } }