Topcodes: Code for algorithm: Scanner.java

/*
 * @(#) 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;
   }
}
[download file]