topcodes.Scanner.java Source code

Java tutorial

Introduction

Here is the source code for topcodes.Scanner.java

Source

/*
 * @(#) 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.
 */
package topcodes;

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.DataBufferByte;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;

import javax.imageio.ImageIO;

import org.opencv.core.Mat;

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 TopCode bullseye patterns.  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.
 *
 * @author Michael Horn
 * @version $Revision: 1.4 $, $Date: 2008/02/04 15:02:13 $
 */
public class Scanner {

    /** Original image */
    protected BufferedImage image;

    /** Total width of image */
    protected int w;

    /** Total height of image */
    protected int h;

    /** Holds processed binary pixel data */
    protected int[] data;

    /** Binary view of the image */
    protected BufferedImage preview;

    /** Candidate code count */
    protected int ccount;

    /** Number of candidates tested */
    protected int tcount;

    /** Maximum width of a TopCode unit in pixels */
    protected int maxu;

    /**
     * Default constructor
     */
    public Scanner() {
        this.image = null;
        this.w = 0;
        this.h = 0;
        this.data = null;
        this.preview = null;
        this.ccount = 0;
        this.tcount = 0;
        this.maxu = 80;
    }

    /**
    * Scan the image and return a list of all topcodes found in it
    */
    public List<TopCode> scan(Mat mat) throws IOException {
        int width = mat.width(), height = mat.height(), channels = mat.channels();
        byte[] sourcePixels = new byte[width * height * channels];
        mat.get(0, 0, sourcePixels);

        // create new BufferedImage and
        image = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);

        // Get reference to backing data
        final byte[] targetPixels = ((DataBufferByte) image.getRaster().getDataBuffer()).getData();
        System.arraycopy(sourcePixels, 0, targetPixels, 0, sourcePixels.length);

        return scan(image);
    }

    /**
     * Scan the given image file and return a list of topcodes found in it.
     */
    public List<TopCode> scan(String filename) throws IOException {
        return scan(ImageIO.read(new File(filename)));
    }

    /**
     * Scan the given image and return a list of all topcodes found in it.
     */
    public List<TopCode> scan(BufferedImage image) {
        this.image = image;
        this.preview = null;
        this.w = image.getWidth();
        this.h = image.getHeight();
        this.data = image.getRGB(0, 0, w, h, null, 0, w);

        threshold(); // run the adaptive threshold filter
        return findCodes(); // scan for topcodes
    }

    /**
     * Scan the image and return a list of all topcodes found in it.
     *
     * @param rgb an array of pixel data in packed RGB format
     * @param width width of the image
     * @param height height of the image
     */
    public List<TopCode> scan(int[] rgb, int width, int height) {
        this.w = width;
        this.h = height;
        this.data = rgb;
        this.preview = null;
        this.image = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
        this.image.setRGB(0, 0, w, h, rgb, 0, w);

        threshold(); // run the adaptive threshold filter
        return findCodes(); // scan for topcodes
    }

    /**
     * Returns the original (unaltered) image
     */
    public BufferedImage getImage() {
        return this.image;
    }

    /**
     * Returns the width in pixels of the current image (or zero if no image is
     * loaded).
     */
    public int getImageWidth() {
        return this.w;
    }

    /**
     * Returns the width in pixels of the current image (or zero if no image is
     * loaded).
     */
    public int getImageHeight() {
        return this.h;
    }

    /**
     * Sets the maximum allowable diameter (in pixels) for a TopCode
     * identified by the scanner.  Setting this to a reasonable value for
     * your application will reduce false positives (recognizing codes that
     * aren't actually there) and improve performance (because fewer
     * candidate codes will be tested).  Setting this value to as low as 50
     * or 60 pixels could be advisable for some applications.  However,
     * setting the maximum diameter too low will prevent valid codes from
     * being recognized.  The default value is 640 pixels.
     */
    public void setMaxCodeDiameter(int diameter) {
        float f = diameter / 8.0f;
        this.maxu = (int) Math.ceil(f);
    }

    /**
     * Returns the number of candidate topcodes found during a scan
     */
    protected int getCandidateCount() {
        return this.ccount;
    }

    /**
     * Returns the number of topcodes tested during a scan
     */
    protected int getTestedCount() {
        return this.tcount;
    }

    /**
     * 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;
    }

    /**
     * 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
     */
    protected void threshold() {

        int pixel, r, g, b, a;
        int threshold, sum = 128;
        int s = 30;
        int k;
        int b1, w1, b2, level, dk;

        this.ccount = 0;

        for (int j = 0; j < h; j++) {
            level = b1 = b2 = w1 = 0;

            //----------------------------------------
            // Process rows back and forth (alternating
            // left-to-right, right-to-left)
            //----------------------------------------
            k = (j % 2 == 0) ? 0 : w - 1;
            k += (j * w);

            for (int i = 0; i < w; i++) {

                //----------------------------------------
                // Calculate pixel intensity (0-255)
                //----------------------------------------
                pixel = data[k];
                r = (pixel >> 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) {

                // On a white region. No black pixels yet
                case 0:
                    if (a == 0) { // First black encountered
                        level = 1;
                        b1 = 1;
                        w1 = 0;
                        b2 = 0;
                    }
                    break;

                // On first black region
                case 1:
                    if (a == 0) {
                        b1++;
                    } else {
                        level = 2;
                        w1 = 1;
                    }
                    break;

                // On second white region (bulls-eye of a code?)
                case 2:
                    if (a == 0) {
                        level = 3;
                        b2 = 1;
                    } else {
                        w1++;
                    }
                    break;

                // On second black region
                case 3:
                    if (a == 0) {
                        b2++;
                    }
                    // This could be a top code
                    else {
                        int mask;
                        if (b1 >= 2 && b2 >= 2 && // less than 2 pixels... not interested
                                b1 <= maxu && b2 <= maxu && w1 <= (maxu + maxu)
                                && Math.abs(b1 + b2 - w1) <= (b1 + b2) && Math.abs(b1 + b2 - w1) <= w1
                                && Math.abs(b1 - b2) <= b1 && Math.abs(b1 - b2) <= b2) {
                            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;
                            ccount += 3; // count candidate codes
                        }
                        b1 = b2;
                        w1 = 1;
                        b2 = 0;
                        level = 2;
                    }
                    break;
                }

                k += (j % 2 == 0) ? 1 : -1;
            }
        }
    }

    /**
     * Scan the image line by line looking for TopCodes
     */
    protected List<TopCode> findCodes() {
        this.tcount = 0;
        List<TopCode> spots = new java.util.ArrayList<TopCode>();

        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) {
                    if ((data[k - 1] & 0x2000000) > 0 && (data[k + 1] & 0x2000000) > 0
                            && (data[k - w] & 0x2000000) > 0 && (data[k + w] & 0x2000000) > 0) {
                        /*
                                       if ((data[k-w] & 0x2000000) > 0 ||
                                           (data[k+w] & 0x2000000) > 0)) {
                        */
                        if (!overlaps(spots, i, j)) {
                            this.tcount++;
                            spot.decode(this, i, j);
                            if (spot.isValid()) {
                                spots.add(spot);
                                spot = new TopCode();
                            }
                        }
                    }
                }
                k++;
            }
        }

        return spots;
    }

    /**
     * Returns true if point (x,y) is in an existing TopCode bullseye
     */
    protected boolean overlaps(List<TopCode> spots, int x, int y) {
        for (TopCode top : spots) {
            if (top.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;
    }

    //   protected void markTest(int x, int y) {
    //      Graphics2D g = (Graphics2D)getImage().getGraphics();
    //      g.setColor(Color.red);
    //      g.fillRect(x - 1, y - 1, 3, 3);
    //   }

    /**
     * For debugging purposes, create a black and white image that
     * shows the result of adaptive thresholding.
     */
    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;
    }
}