ocr.sapphire.image.EdgeBasedImagePreprocessor.java Source code

Java tutorial

Introduction

Here is the source code for ocr.sapphire.image.EdgeBasedImagePreprocessor.java

Source

/*
 * Copyright Sapphire-group 2010
 *
 * This file is part of sapphire-ocr.
 *
 * sapphire-ocr is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * sapphire-ocr 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 sapphire-ocr.  If not, see <http://www.gnu.org/licenses/>.
 *
 */
package ocr.sapphire.image;

import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.Deque;
import java.util.LinkedList;
import java.util.SortedMap;
import java.util.TreeMap;

import javax.imageio.ImageIO;

import org.apache.commons.lang.ArrayUtils;

import com.tomgibara.CannyEdgeDetector;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;
import static com.tomgibara.CannyEdgeDetector.BLACK;
import static com.tomgibara.CannyEdgeDetector.WHITE;
import static ocr.sapphire.util.Utils.getBounds;

public class EdgeBasedImagePreprocessor extends ImagePreprocessor {

    public EdgeBasedImagePreprocessor() {
        this(4, 10);
    }

    public EdgeBasedImagePreprocessor(PreprocessorConfig config) {
        this(config.getComponentCount(), config.getSeriesLength());
    }

    public EdgeBasedImagePreprocessor(int componentCount, int seriesLength) {
        super(componentCount, seriesLength);
        this.detector = new CannyEdgeDetector();
        detector.setLowThreshold(0.1f);
        detector.setHighThreshold(0.2f);
    }

    private int[] findEdges(BufferedImage image) {
        detector.setSourceImage(image);
        detector.process();
        return detector.getData();
    }

    private static final int CLOSE_THRESHOLD = 10;

    /**
     * <p>Rotate the component so that the first point is the leftmost.</p>
     * <p>Normalization doesnot make difference when reverse the Fourier series
     * but neuron networks may "feel" easier to recorgnize normalized series.</p>
     * @param points
     * @return
     */
    private static Deque<Point> normalize(Deque<Point> c) {
        double leftmost = Double.MAX_VALUE;
        for (Point p : c) {
            if (p.x < leftmost) {
                leftmost = p.x;
            }
        }
        while (c.getFirst().x > leftmost) {
            c.addLast(c.removeFirst());
        }
        return c;
    }

    private Point[][] findEdgePoints(int[] edgeData) {
        List<Deque<Point>> components = new ArrayList<Deque<Point>>();
        // find close paths
        for (int y = 0; y < height; y++) {
            for (int x = 0; x < width; x++) {
                if (edgeData[x + y * width] == BLACK && isBridge(edgeData, x, y)) {
                    edgeData[x + y * width] = WHITE;
                    Deque<Point> firstPart = null, secondPart = null;
                    for (int k = 0; k < DX.length; k++) {
                        int x2 = x + DX[k];
                        int y2 = y + DY[k];
                        if (x2 < 0 || x2 >= width || y2 < 0 || y2 >= height) {
                            continue;
                        }
                        if (edgeData[x2 + y2 * width] == BLACK) {
                            Deque<Point> points = findConnectedComponent(edgeData, x2, y2);
                            if (firstPart == null) {
                                firstPart = points;
                            } else {
                                secondPart = points;
                            }
                        }
                    }
                    firstPart.addFirst(new Point(x, y));
                    if (secondPart != null) { // the path is not closed
                        join(firstPart, true, secondPart, true);
                    }
                    components.add(firstPart);
                }
            }
        }

        // remove contained components
        for (int i = 0; i < components.size() - 1; i++) {
            Rectangle r1 = getBounds(components.get(i));
            for (int j = i + 1; j < components.size();) {
                Rectangle r2 = getBounds(components.get(j));
                if (r1.contains(r2)) {
                    components.remove(j);
                } else if (r2.contains(r1)) {
                    components.set(i, components.get(j));
                    components.remove(j);
                } else {
                    j++;
                }
            }
        }

        // try to connect some paths
        int connectedCount;
        do {
            connectedCount = 0;
            for (int i = 0; i < components.size() - 1; i++) {
                for (int j = i + 1; j < components.size(); j++) {
                    Deque<Point> a = components.get(i);
                    Deque<Point> b = components.get(j);

                    double d0 = d(a.getFirst(), a.getLast()) + d(b.getFirst(), b.getLast());
                    double d1 = d(a.getFirst(), b.getFirst()) + d(a.getLast(), b.getLast());
                    double d2 = d(a.getFirst(), b.getLast()) + d(a.getLast(), b.getFirst());
                    double d3 = d(a.getFirst(), b.getFirst());
                    double d4 = d(a.getFirst(), b.getLast());
                    double d5 = d(a.getLast(), b.getFirst());
                    double d6 = d(a.getLast(), b.getLast());

                    if (d3 <= CLOSE_THRESHOLD && d3 <= d4) {
                        join(a, true, b, true);
                        components.remove(j);
                        connectedCount++;
                    } else if (d4 <= CLOSE_THRESHOLD && d4 <= d3) {
                        join(a, true, b, false);
                        components.remove(j);
                        connectedCount++;
                    } else if (d5 <= CLOSE_THRESHOLD && d5 <= d6) {
                        join(a, false, b, true);
                        components.remove(j);
                        connectedCount++;
                    } else if (d6 <= CLOSE_THRESHOLD && d6 <= d5) {
                        join(a, false, b, false);
                        components.remove(j);
                        connectedCount++;
                    } else if (d1 <= d0 && d1 <= d2) {
                        if (d3 < d6) {
                            join(a, true, b, true);
                        } else {
                            join(a, false, b, false);
                        }
                        components.remove(j);
                        connectedCount++;
                    } else if (d2 <= d0 && d2 <= d1) {
                        if (d4 < d5) {
                            join(a, true, b, false);
                        } else {
                            join(a, false, b, true);
                        }
                        components.remove(j);
                        connectedCount++;
                    }
                } // end of for j
            } // end of for i
        } while (connectedCount > 0);

        // choose (componentCount) biggest components
        SortedMap<Integer, Deque<Point>> componentMap = new TreeMap<Integer, Deque<Point>>();
        for (Deque<Point> c : components) {
            componentMap.put(-c.size(), c);
        }

        // remove noise
        boolean firstPoint = true;
        for (Iterator<Entry<Integer, Deque<Point>>> iterator = componentMap.entrySet().iterator(); iterator
                .hasNext();) {
            Entry<Integer, Deque<Point>> entry = iterator.next();
            Rectangle r = getBounds(entry.getValue());
            if (r.width <= 10 && r.height <= 10) {
                if (firstPoint) {
                    firstPoint = false;
                } else {
                    iterator.remove();
                }
            }
        }

        // convert components: normalize points, to array
        int foundComponentCount = Math.min(componentCount, componentMap.size());
        componentArr = new Point[foundComponentCount][];
        Rectangle r = getBounds(componentMap.get(componentMap.firstKey()));
        for (int c = 0; c < foundComponentCount; c++) {
            int key = componentMap.firstKey();
            componentArr[c] = new Point[componentMap.get(key).size()];
            normalize(componentMap.get(key)).toArray(componentArr[c]);
            componentMap.remove(key);

            for (int i = 0; i < componentArr[c].length; i++) {
                componentArr[c][i].x = (componentArr[c][i].x - r.x) / r.width;
                componentArr[c][i].y = (componentArr[c][i].y - r.y) / r.height;
            }
        }
        return componentArr;
    }

    /**
     * Join b to a, return a.
     * @param a
     * @param afirst join at the first point of a?
     * @param b
     * @param bfirst join at the first point of b?
     * @return
     */
    private static Deque<Point> join(Deque<Point> a, boolean afirst, Deque<Point> b, boolean bfirst) {
        if (!bfirst) {
            Collections.reverse((List<Point>) b);
        }
        // don't reverse a, may confuse the network
        if (afirst) {
            for (Point p : b) {
                a.addFirst(p);
            }
        } else {
            for (Point p : b) {
                a.addLast(p);
            }
        }
        return a;
    }

    private boolean isBridge(int[] edgeData, int x, int y) {
        Point[] points = new Point[DX.length];
        int adjacentCounter = 0;
        for (int k = 0; k < DX.length; k++) {
            int x2 = x + DX[k];
            int y2 = y + DY[k];
            if (x2 < 0 || x2 >= width || y2 < 0 || y2 >= height) {
                continue;
            }
            if (edgeData[x2 + y2 * width] == BLACK) {
                points[adjacentCounter] = new Point(x2, y2);
                adjacentCounter++;
            }
        }
        return (adjacentCounter == 2
                && (Math.abs(points[0].x - points[1].x) >= 2 || Math.abs(points[0].y - points[1].y) >= 2));
    }

    /**
     * Compute distance from a to b
     * @param a
     * @param b
     * @return
     */
    private static double d(Point a, Point b) {
        return Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
    }

    private Deque<Point> findConnectedComponent(int[] edgeData, int x, int y) {
        Deque<Point> points = new LinkedList<Point>();
        Deque<Point> queue = new LinkedList<Point>();

        edgeData[x + y * width] = WHITE;
        Point initialPoint = new Point(x, y);
        points.add(initialPoint);
        queue.push(initialPoint);

        while (!queue.isEmpty()) {
            Point point = queue.removeFirst();
            for (int k = 0; k < 8; k++) {
                int x2 = (int) (point.x + DX[k]);
                int y2 = (int) (point.y + DY[k]);
                if (x2 < 0 || y2 < 0 || x2 >= width || y2 >= height) {
                    continue;
                }
                if (edgeData[x2 + y2 * width] == BLACK) {
                    edgeData[x2 + y2 * width] = WHITE;
                    Point point2 = new Point(x2, y2);
                    points.add(point2);
                    queue.addLast(point2);
                }
            }
        }
        return points;
    }

    public double[][][] process(BufferedImage image) {
        height = image.getHeight();
        width = image.getWidth();
        int[] edgeData = findEdges(image);
        componentArr = findEdgePoints(edgeData);
        coefficients = new double[componentArr.length][][];
        for (int c = 0; c < componentArr.length; c++) {
            coefficients[c] = fourierTransform(componentArr[c]);
        }
        return coefficients;
    }

    public static void main(String[] args) throws IOException {
        BufferedImage image = ImageIO.read(new File("a.png"));
        EdgeBasedImagePreprocessor preprocessor = new EdgeBasedImagePreprocessor();
        double[][][] coefficients = preprocessor.process(image);
        BufferedImage[] reversedImages = preprocessor.reverse();
        for (int c = 0; c < preprocessor.getComponentCount(); c++) {
            System.out.println("component " + (c + 1));
            System.out.println("\treal(a): " + ArrayUtils.toString(coefficients[c][0]));
            System.out.println("\timagine(a): " + ArrayUtils.toString(coefficients[c][1]));
            System.out.println("\treal(b): " + ArrayUtils.toString(coefficients[c][2]));
            System.out.println("\timagine(b): " + ArrayUtils.toString(coefficients[c][3]));
            ImageIO.write(reversedImages[c], "PNG", new File("reverse" + c + ".png"));
        }
        ImageIO.write(preprocessor.getContourImage(), "PNG", new File("edge.png"));
    }

    private static final int[] DX = { 0, 1, 1, 1, 0, -1, -1, -1 };
    private static final int[] DY = { -1, -1, 0, 1, 1, 1, 0, -1 };
    private CannyEdgeDetector detector;

    public BufferedImage getContourImage() {
        return detector.getEdgesImage();
    }
}