Java tutorial
/////////////////////////////////////////////////////////////////////////////// //Copyright (C) 2012 Assaf Urieli // //This file is part of Jochre. // //Jochre is free software: you can redistribute it and/or modify //it under the terms of the GNU Affero General Public License as published by //the Free Software Foundation, either version 3 of the License, or //(at your option) any later version. // //Jochre 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 Affero General Public License for more details. // //You should have received a copy of the GNU Affero General Public License //along with Jochre. If not, see <http://www.gnu.org/licenses/>. ////////////////////////////////////////////////////////////////////////////// package com.joliciel.jochre.graphics; import java.awt.image.BufferedImage; import java.text.DecimalFormat; import java.util.ArrayList; import java.util.BitSet; import java.util.Collection; import java.util.Dictionary; import java.util.HashMap; import java.util.HashSet; import java.util.Hashtable; import java.util.List; import java.util.Map; import java.util.Set; import java.util.Stack; import java.util.TreeSet; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.commons.math.stat.descriptive.moment.Mean; import com.joliciel.jochre.EntityImpl; import com.joliciel.jochre.boundaries.BoundaryService; import com.joliciel.jochre.boundaries.Split; import com.joliciel.jochre.graphics.util.ImagePixelGrabber; import com.joliciel.jochre.graphics.util.ImagePixelGrabberImpl; import com.joliciel.jochre.letterGuesser.Letter; import com.joliciel.jochre.letterGuesser.LetterGuesserService; import com.joliciel.talismane.machineLearning.Decision; import com.joliciel.talismane.machineLearning.features.Feature; import com.joliciel.talismane.machineLearning.features.FeatureResult; import com.joliciel.talismane.machineLearning.features.RuntimeEnvironment; import com.joliciel.talismane.utils.PersistentList; import com.joliciel.talismane.utils.PersistentListImpl; class ShapeImpl extends EntityImpl implements ShapeInternal { private static final Log LOG = LogFactory.getLog(ShapeImpl.class); private int top; private int left; private int bottom; private int right; private int baseLine; private int meanLine; private int capLine; private int index; private RowOfShapes row; private GroupOfShapes group; private int groupId; private JochreImage jochreImage; private String letter = ""; private String originalGuess = ""; private GraphicsServiceInternal graphicsService; private LetterGuesserService letterGuesserService; private BoundaryService boundaryService; private Map<String, Map<SectionBrightnessMeasurementMethod, double[][]>> brightnessBySectorMap = new HashMap<String, Map<SectionBrightnessMeasurementMethod, double[][]>>(); private Map<String, Map<SectionBrightnessMeasurementMethod, Double>> brightnessMeanBySectorMap = new HashMap<String, Map<SectionBrightnessMeasurementMethod, Double>>(); private Map<String, FeatureResult<?>> featureResults = new HashMap<String, FeatureResult<?>>(); private Dictionary<String, BitSet> bitsets = new Hashtable<String, BitSet>(); private Dictionary<Integer, BitSet> outlines = new Hashtable<Integer, BitSet>(); private int[] brightnessCounts; private boolean blackAndWhite = false; private int[] verticalCounts = null; private int[][] verticalContour = null; private BufferedImage image; private ImagePixelGrabber pixelGrabber; private boolean dirty = true; private Set<Decision<Letter>> letterGuesses = null; private int totalBrightness = 0; private int[] startingPoint; TreeSet<VerticalLineSegment> lines = null; Collection<BridgeCandidate> bridgeCandidates = null; PersistentList<Split> splits = null; Double confidence = null; ShapeImpl() { } public ShapeImpl(JochreImage container) { this.jochreImage = container; } @Override public int getHeight() { return bottom - top + 1; } /** * If the pixel is in the shape, return its brightness value. * Otherwise, return zero. * @param x * @param y * @return */ public int getPixelInShape(int x, int y) { if (x < 0 || x >= this.getWidth() || y < 0 || y >= this.getHeight()) return 0; else return this.getPixel(x, y); } @Override public int getPixel(int x, int y) { if (this.image != null) { int pixel = this.getPixelGrabber().getPixelBrightness(x, y); return this.getJochreImage().normalize(pixel); } else { return jochreImage.getAbsolutePixel(left + x, top + y); } } @Override public int getAbsolutePixel(int x, int y) { if (this.image != null) { int pixel = this.getPixelGrabber().getPixelBrightness(x - this.left, y - this.top); return this.getJochreImage().normalize(pixel); } else return jochreImage.getAbsolutePixel(x, y); } @Override public int getRawPixel(int x, int y) { if (this.image != null) { int pixel = this.getPixelGrabber().getPixelBrightness(x, y); return pixel; } else return jochreImage.getRawAbsolutePixel(left + x, top + y); } @Override public int getRawAbsolutePixel(int x, int y) { if (this.image != null) { int pixel = this.getPixelGrabber().getPixelBrightness(x - this.left, y - this.top); return pixel; } else return jochreImage.getRawAbsolutePixel(x, y); } @Override public int getWidth() { return right - left + 1; } public int getTop() { return top; } public void setTop(int top) { if (this.top != top) { this.top = top; this.dirty = true; } } public int getLeft() { return left; } public void setLeft(int left) { if (this.left != left) { this.left = left; this.dirty = true; } } public int getBottom() { return bottom; } public void setBottom(int bottom) { if (this.bottom != bottom) { this.bottom = bottom; this.dirty = true; } } public int getRight() { return right; } public void setRight(int right) { if (this.right != right) { this.right = right; this.dirty = true; } } public GroupOfShapes getGroup() { if (this.groupId != 0 && this.group == null) { this.group = this.graphicsService.loadGroupOfShapes(this.groupId); } return group; } public void setGroup(GroupOfShapes group) { this.group = group; if (group != null) this.setGroupId(group.getId()); else this.setGroupId(0); } /** * The relative y-index of line at the bottom of standard lowercase letters. * @return */ public int getBaseLine() { return baseLine; } public void setBaseLine(int baseLine) { if (this.baseLine != baseLine) { this.baseLine = baseLine; this.dirty = true; } } /** * The relative y-index of the line at the top of standard lowercase letters. * @return */ public int getMeanLine() { return meanLine; } public void setMeanLine(int meanLine) { if (this.meanLine != meanLine) { this.meanLine = meanLine; this.dirty = true; } } /** * The relative y-index of the line at the top of standard uppercase letters. * @return */ public int getCapLine() { return capLine; } public void setCapLine(int capLine) { if (this.capLine != capLine) { this.capLine = capLine; this.dirty = true; } } public int getIndex() { return index; } public void setIndex(int index) { if (this.index != index) { this.index = index; this.dirty = true; } } @Override public void saveInternal() { if (this.group != null && this.groupId == 0) this.setGroupId(this.group.getId()); if (this.dirty) this.graphicsService.saveShape(this); if (this.splits != null) { for (Split split : this.splits.getItemsRemoved()) { split.delete(); } for (Split split : this.splits) { split.save(); } } ((JochreImageInternal) this.getJochreImage()).onSaveShape(this); } public GraphicsServiceInternal getGraphicsService() { return graphicsService; } public void setGraphicsService(GraphicsServiceInternal graphicsService) { this.graphicsService = graphicsService; } public String getLetter() { return letter; } public void setLetter(String letter) { if (letter == null) letter = ""; if (!this.letter.equals(letter)) { this.letter = letter; this.dirty = true; } } public void delete() { this.graphicsService.deleteShapeInternal(this); } public RowOfShapes getRow() { return row; } public void setRow(RowOfShapes row) { this.row = row; } public int getGroupId() { return groupId; } public void setGroupId(int groupId) { if (this.groupId != groupId) { this.groupId = groupId; this.dirty = true; } } @Override public double[][] getBrightnessBySection(int verticalSectionCount, int horizontalSectionCount, int marginSectionCount, SectionBrightnessMeasurementMethod measurementMethod) { return this.getBrightnessBySector(verticalSectionCount, horizontalSectionCount, marginSectionCount, false, measurementMethod); } @Override public double[][] getBrightnessBySection(int verticalSectionCount, int horizontalSectionCount, double topBottomMarginWidth, double horizontalMarginWidth, SectionBrightnessMeasurementMethod measurementMethod) { String key = verticalSectionCount + "|" + horizontalSectionCount + "|" + topBottomMarginWidth + "|" + horizontalMarginWidth; Map<SectionBrightnessMeasurementMethod, double[][]> brightnessBySector = this.brightnessBySectorMap .get(key); if (brightnessBySector == null) { int xSectorCount = verticalSectionCount; int ySectorCount = horizontalSectionCount; double[] verticalBreaks = new double[xSectorCount + 1]; double[] horizontalBreaks = new double[ySectorCount + 1]; int xHeight = this.getBaseLine() - this.getMeanLine() + 1; if (LOG.isTraceEnabled()) LOG.trace("xHeight: " + xHeight); double totalWidth = xHeight + ((double) xHeight * horizontalMarginWidth); double leftOffset = 0.0; if (this.getJochreImage().isLeftToRight()) leftOffset = this.getWidth() - totalWidth; double verticalSectionWidth = totalWidth / ((double) verticalSectionCount); verticalBreaks[0] = leftOffset; for (int i = 1; i <= xSectorCount; i++) { verticalBreaks[i] = leftOffset + (verticalSectionWidth * i); if (LOG.isTraceEnabled()) LOG.trace("Vertical break " + i + ": " + verticalBreaks[i]); } verticalBreaks[xSectorCount] = this.getWidth(); double totalHeight = xHeight + (xHeight * topBottomMarginWidth * 2.0); double topOffset = this.getMeanLine() - (xHeight * topBottomMarginWidth); double horizontalSectionHeight = totalHeight / ((double) horizontalSectionCount); horizontalBreaks[0] = topOffset; for (int i = 1; i <= ySectorCount; i++) { horizontalBreaks[i] = topOffset + horizontalSectionHeight * (i); if (LOG.isTraceEnabled()) LOG.trace("Horizontal break " + i + ": " + horizontalBreaks[i]); } brightnessBySector = this.getBrightnessBySector(key, verticalBreaks, horizontalBreaks); } return brightnessBySector.get(measurementMethod); } public double[][] getBrightnessBySector(int verticalSectionCount, int horizontalSectionCount, int marginSectionCount, boolean includeHorizontalMargin, SectionBrightnessMeasurementMethod measurementMethod) { String key = verticalSectionCount + "|" + horizontalSectionCount + "|" + marginSectionCount + "|" + includeHorizontalMargin; Map<SectionBrightnessMeasurementMethod, double[][]> brightnessBySector = this.brightnessBySectorMap .get(key); if (brightnessBySector == null) { int xSectorCount = verticalSectionCount; if (includeHorizontalMargin) xSectorCount += marginSectionCount; int ySectorCount = horizontalSectionCount + (2 * marginSectionCount); double[] verticalBreaks = new double[xSectorCount + 1]; double[] horizontalBreaks = new double[ySectorCount + 1]; double xHeight = this.getBaseLine() - this.getMeanLine() + 1; if (LOG.isTraceEnabled()) LOG.trace("xHeight: " + xHeight); double verticalSectorWidth = 0.0; double horizontalMarginSectorWidth = 0.0; double leftOffset = 0; double horizontalMarginWidthPixels = 0; if (includeHorizontalMargin) { double totalWidth = xHeight * 1.5; if (!this.getJochreImage().isLeftToRight()) leftOffset = this.getWidth() - totalWidth; horizontalMarginWidthPixels = totalWidth - xHeight; horizontalMarginSectorWidth = (double) (horizontalMarginWidthPixels) / (double) marginSectionCount; verticalSectorWidth = xHeight / ((double) verticalSectionCount); } else { verticalSectorWidth = (double) this.getWidth() / (double) xSectorCount; } int xIndex = 0; verticalBreaks[xIndex++] = leftOffset; if (this.getJochreImage().isLeftToRight()) { for (int i = 0; i < verticalSectionCount; i++) { verticalBreaks[xIndex++] = verticalSectorWidth * (i + 1); } for (int i = 0; i < xSectorCount - verticalSectionCount; i++) { verticalBreaks[xIndex++] = (verticalSectorWidth * verticalSectionCount) + (horizontalMarginSectorWidth * (i + 1)); } } else { for (int i = 0; i < xSectorCount - verticalSectionCount; i++) { verticalBreaks[xIndex++] = leftOffset + horizontalMarginSectorWidth * (i + 1); } for (int i = 0; i < verticalSectionCount; i++) { verticalBreaks[xIndex++] = leftOffset + horizontalMarginWidthPixels + verticalSectorWidth * (i + 1); } } verticalBreaks[xSectorCount] = this.getWidth(); if (LOG.isTraceEnabled()) for (int i = 0; i < verticalBreaks.length; i++) { LOG.trace("Vertical break " + i + ": " + verticalBreaks[i]); } int yIndex = 0; horizontalBreaks[yIndex++] = 0; double headerHeight = this.getMeanLine(); if (headerHeight < 0) headerHeight = 0; if (LOG.isTraceEnabled()) LOG.trace("headerHeight: " + headerHeight); double headerSectorHeight = headerHeight / (double) marginSectionCount; if (LOG.isTraceEnabled()) LOG.trace("headerSectorHeight: " + headerSectorHeight); for (int i = 0; i < marginSectionCount; i++) { horizontalBreaks[yIndex++] = headerSectorHeight * (i + 1); } double horizontalSectorHeight = (double) xHeight / (double) horizontalSectionCount; if (LOG.isTraceEnabled()) LOG.trace("horizontalSectorHeight: " + horizontalSectorHeight); for (int i = 0; i < horizontalSectionCount; i++) { horizontalBreaks[yIndex++] = this.getMeanLine() + (horizontalSectorHeight * (i + 1)); } double footerHeight = this.getHeight() - this.getBaseLine() - 1; if (footerHeight < 0) footerHeight = 0; if (LOG.isTraceEnabled()) LOG.trace("footerHeight: " + footerHeight); double footerSectorHeight = footerHeight / (double) marginSectionCount; if (LOG.isTraceEnabled()) LOG.trace("footerSectorHeight: " + footerSectorHeight); for (int i = 0; i < marginSectionCount; i++) { horizontalBreaks[yIndex++] = this.getBaseLine() + 1 + (footerSectorHeight * (i + 1)); } horizontalBreaks[horizontalBreaks.length - 1] = this.getHeight(); if (LOG.isTraceEnabled()) for (int i = 0; i < horizontalBreaks.length; i++) { LOG.trace("Horizontal break " + i + ": " + horizontalBreaks[i]); } brightnessBySector = this.getBrightnessBySector(key, verticalBreaks, horizontalBreaks); } return brightnessBySector.get(measurementMethod); } @Override public double[][] getBrightnessBySection(int verticalSectionCount, int horizontalSectionCount, SectionBrightnessMeasurementMethod measurementMethod) { String key = verticalSectionCount + "|" + horizontalSectionCount; Map<SectionBrightnessMeasurementMethod, double[][]> brightnessBySector = this.brightnessBySectorMap .get(key); if (brightnessBySector == null) { int xSectorCount = verticalSectionCount; int ySectorCount = horizontalSectionCount; double[] verticalBreaks = new double[xSectorCount + 1]; double[] horizontalBreaks = new double[ySectorCount + 1]; double verticalSectorWidth = (double) this.getWidth() / (double) xSectorCount; verticalBreaks[0] = 0; for (int i = 1; i <= verticalSectionCount; i++) { verticalBreaks[i] = verticalSectorWidth * i; } verticalBreaks[xSectorCount] = this.getWidth(); if (LOG.isTraceEnabled()) for (int i = 0; i < verticalBreaks.length; i++) { LOG.trace("Vertical break " + i + ": " + verticalBreaks[i]); } double horizontalSectorHeight = (double) this.getHeight() / (double) horizontalSectionCount; if (LOG.isTraceEnabled()) LOG.trace("horizontalSectorHeight: " + horizontalSectorHeight); horizontalBreaks[0] = 0; for (int i = 1; i <= horizontalSectionCount; i++) { horizontalBreaks[i] = horizontalSectorHeight * i; } horizontalBreaks[ySectorCount] = this.getHeight(); if (LOG.isTraceEnabled()) for (int i = 0; i < horizontalBreaks.length; i++) { LOG.trace("Horizontal break " + i + ": " + horizontalBreaks[i]); } brightnessBySector = this.getBrightnessBySector(key, verticalBreaks, horizontalBreaks); } return brightnessBySector.get(measurementMethod); } Map<SectionBrightnessMeasurementMethod, double[][]> getBrightnessBySector(String key, double[] verticalBreaks, double[] horizontalBreaks) { Map<SectionBrightnessMeasurementMethod, double[][]> brightnessByMethod = this.brightnessBySectorMap .get(key); if (brightnessByMethod == null) { int xSectorCount = verticalBreaks.length - 1; int ySectorCount = horizontalBreaks.length - 1; double[][] totals = new double[xSectorCount][ySectorCount]; // calculate the y-distribution among sections double[][] yDistribution = new double[this.getHeight()][ySectorCount]; int[] yStart = new int[this.getHeight()]; int[] yEnd = new int[this.getHeight()]; int[] xStart = new int[this.getWidth()]; int[] xEnd = new int[this.getWidth()]; for (int y = 0; y < this.getHeight(); y++) { for (int i = 0; i < ySectorCount; i++) { double horizontalBreak = horizontalBreaks[i + 1]; if (y < horizontalBreak && horizontalBreak < y + 1) { yDistribution[y][i] = horizontalBreak - Math.floor(horizontalBreak); yStart[y] = i; yEnd[y] = i; if (i + 1 < yDistribution[y].length) { yDistribution[y][i + 1] = 1 - yDistribution[y][i]; yEnd[y] = i + 1; } break; } else if (y < horizontalBreak) { yDistribution[y][i] = 1.0; yStart[y] = i; yEnd[y] = i; break; } } } // next y-coordinate in shape // calculate the x-distribution among sections double[][] xDistribution = new double[this.getWidth()][xSectorCount]; for (int x = 0; x < this.getWidth(); x++) { for (int i = 0; i < xSectorCount; i++) { double verticalBreak = verticalBreaks[i + 1]; if (x < verticalBreak && verticalBreak < x + 1) { xDistribution[x][i] = verticalBreak - Math.floor(verticalBreak); xStart[x] = i; xEnd[x] = i; if (i + 1 < xDistribution[x].length) { xDistribution[x][i + 1] = 1 - xDistribution[x][i]; xEnd[x] = i + 1; } break; } else if (x < verticalBreak) { xDistribution[x][i] = 1.0; xStart[x] = i; xEnd[x] = i; break; } } } // next x-coordinate in shape // get brightnesses double totalBrightness = 0.0; for (int y = 0; y < this.getHeight(); y++) { for (int x = 0; x < this.getWidth(); x++) { double brightness = 255.0 - this.getPixelInShape(x, y); totalBrightness += brightness; for (int i = xStart[x]; i <= xEnd[x]; i++) for (int j = yStart[y]; j <= yEnd[y]; j++) totals[i][j] += xDistribution[x][i] * yDistribution[y][j] * brightness; } } // calculate the pixel count for each section double[][] pixelCounts = new double[xSectorCount][ySectorCount]; for (int i = 0; i < xSectorCount; i++) for (int j = 0; j < ySectorCount; j++) { double sectionWidth = verticalBreaks[i + 1] - verticalBreaks[i]; double sectionHeight = horizontalBreaks[j + 1] - horizontalBreaks[j]; pixelCounts[i][j] = sectionWidth * sectionHeight; } // calculate the ratio for each section double[][] ratios = new double[xSectorCount][ySectorCount]; double maxRatio = 0.0; for (int i = 0; i < xSectorCount; i++) for (int j = 0; j < ySectorCount; j++) { if (pixelCounts[i][j] > 0) { ratios[i][j] = totals[i][j] / pixelCounts[i][j]; if (ratios[i][j] > maxRatio) maxRatio = ratios[i][j]; } else { ratios[i][j] = 0; } } // calculate relative to the max normalised value double[][] relativeToMax = new double[xSectorCount][ySectorCount]; if (maxRatio > 0) { for (int i = 0; i < ratios.length; i++) { for (int j = 0; j < ratios[0].length; j++) { double ratio = ratios[i][j]; relativeToMax[i][j] = ratio / maxRatio; } } } // calculate relative to the total value double[][] relativeToTotal = new double[xSectorCount][ySectorCount]; if (totalBrightness > 0) { for (int i = 0; i < totals.length; i++) { for (int j = 0; j < totals[0].length; j++) { double brightness = totals[i][j]; relativeToTotal[i][j] = brightness / totalBrightness; } } } if (LOG.isTraceEnabled()) { LOG.trace("Results: "); for (int j = 0; j < ratios[0].length; j++) { StringBuilder sb = new StringBuilder(); StringBuilder sb2 = new StringBuilder(); StringBuilder sb3 = new StringBuilder(); sb.append("Totals(" + j + ")"); sb2.append("Counts(" + j + ")"); sb3.append("Ratios(" + j + ")"); for (int i = 0; i < ratios.length; i++) { sb.append((int) totals[i][j]); sb.append(','); sb2.append((int) pixelCounts[i][j]); sb2.append(','); sb3.append((int) ratios[i][j]); sb3.append(','); } LOG.trace(sb.toString()); LOG.trace(sb2.toString()); LOG.trace(sb3.toString()); } } brightnessByMethod = new HashMap<Shape.SectionBrightnessMeasurementMethod, double[][]>(); brightnessByMethod.put(SectionBrightnessMeasurementMethod.RAW, totals); brightnessByMethod.put(SectionBrightnessMeasurementMethod.SIZE_NORMALISED, ratios); brightnessByMethod.put(SectionBrightnessMeasurementMethod.RELATIVE_TO_MAX_SECTION, relativeToMax); brightnessByMethod.put(SectionBrightnessMeasurementMethod.PORTION_OF_TOTAL_BRIGHTNESS, relativeToTotal); this.brightnessBySectorMap.put(key, brightnessByMethod); } return brightnessByMethod; } @Override public double getBrightnessMeanBySection(int verticalSectionCount, int horizontalSectionCount, int marginSectionCount, SectionBrightnessMeasurementMethod measurementMethod) { String key = verticalSectionCount + "|" + horizontalSectionCount + "|" + marginSectionCount; return this.getBrightnessMeanBySector(key, measurementMethod); } @Override public double getBrightnessMeanBySection(int verticalSectionCount, int horizontalSectionCount, SectionBrightnessMeasurementMethod measurementMethod) { String key = verticalSectionCount + "|" + horizontalSectionCount; return this.getBrightnessMeanBySector(key, measurementMethod); } @Override public double getBrightnessMeanBySection(int verticalSectionCount, int horizontalSectionCount, double topBottomMarginWidth, double horizontalMarginWidth, SectionBrightnessMeasurementMethod measurementMethod) { String key = verticalSectionCount + "|" + horizontalSectionCount + "|" + topBottomMarginWidth + "|" + horizontalMarginWidth; return this.getBrightnessMeanBySector(key, measurementMethod); } double getBrightnessMeanBySector(String key, SectionBrightnessMeasurementMethod measurementMethod) { Map<SectionBrightnessMeasurementMethod, Double> methodToMeanMap = this.brightnessMeanBySectorMap.get(key); if (methodToMeanMap == null) { methodToMeanMap = new HashMap<Shape.SectionBrightnessMeasurementMethod, Double>(); this.brightnessMeanBySectorMap.put(key, methodToMeanMap); } Double brightnessMeanBySectorObj = methodToMeanMap.get(measurementMethod); double brightnessMeanBySector = 0.0; if (brightnessMeanBySectorObj == null) { Mean mean = new Mean(); Map<SectionBrightnessMeasurementMethod, double[][]> brightnessByMethod = this.brightnessBySectorMap .get(key); double[][] brightnessGrid = brightnessByMethod.get(measurementMethod); for (int i = 0; i < brightnessGrid.length; i++) mean.incrementAll(brightnessGrid[i]); brightnessMeanBySector = mean.getResult(); methodToMeanMap.put(measurementMethod, brightnessMeanBySector); } else { brightnessMeanBySector = brightnessMeanBySectorObj.doubleValue(); } return brightnessMeanBySector; } @Override public boolean isPixelBlack(int x, int y) { int threshold = this.getJochreImage().getBlackThreshold(); return this.isPixelBlack(x, y, threshold); } @Override public boolean isPixelBlack(int x, int y, int threshold) { return this.isPixelBlack(x, y, threshold, 0); } @Override public boolean isPixelBlack(int x, int y, int threshold, int whiteGapFillFactor) { if (x < 0 || y < 0 || x >= this.getWidth() || y >= this.getHeight()) return false; BitSet bitset = this.getBlackAndWhiteBitSet(threshold, whiteGapFillFactor); return bitset.get(y * this.getWidth() + x); } @Override public BitSet getBlackAndWhiteBitSet(int threshold) { String key = "" + threshold; BitSet bitset = this.bitsets.get(key); if (bitset == null) { bitset = new BitSet(this.getWidth() * this.getHeight()); int counter = 0; for (int j = 0; j < this.getHeight(); j++) for (int i = 0; i < this.getWidth(); i++) { int pixel = this.getPixel(i, j); bitset.set(counter++, pixel <= threshold); } this.bitsets.put(key, bitset); } return bitset; } @Override public BitSet getBlackAndWhiteBitSet(int threshold, int whiteGapFillFactor) { String key = threshold + "|" + whiteGapFillFactor; BitSet bitset = this.bitsets.get(key); if (bitset == null) { bitset = this.getBlackAndWhiteBitSet(threshold); // if the image is black-and-white, fill in any bits // that may have been emptied during the scan if (whiteGapFillFactor > 0 && this.isBlackAndWhite()) { ShapeFiller shapeFiller = this.graphicsService.getShapeFiller(); bitset = shapeFiller.fillShape(this, threshold, whiteGapFillFactor); } else { bitset = this.getBlackAndWhiteBitSet(threshold); } this.bitsets.put(key, bitset); } return bitset; } /** * Find outline of the shape as a BitSet. * @param shape * @return */ public BitSet getOutline(int threshold) { BitSet outline = this.outlines.get((Integer) threshold); if (outline == null) { outline = new BitSet(this.getHeight() * this.getWidth()); int counter = 0; for (int y = 0; y < this.getHeight(); y++) { for (int x = 0; x < this.getWidth(); x++) { boolean black = this.isPixelBlack(x, y, threshold); if (!black) outline.set(counter++, false); else { boolean innerPixel = this.isPixelBlack(x - 1, y, threshold) && this.isPixelBlack(x + 1, y, threshold) && this.isPixelBlack(x, y - 1, threshold) && this.isPixelBlack(x, y + 1, threshold); outline.set(counter++, !innerPixel); } // is it black? } // next x } // next y this.outlines.put((Integer) threshold, outline); } return outline; } @Override public JochreImage getJochreImage() { if (this.jochreImage == null) { this.jochreImage = this.getGroup().getRow().getParagraph().getImage(); } return jochreImage; } @Override public void setJochreImage(JochreImage jochreImage) { this.jochreImage = jochreImage; } boolean isBlackAndWhite() { if (brightnessCounts == null) { brightnessCounts = new int[256]; for (int y = 0; y < this.getHeight(); y++) for (int x = 0; x < this.getWidth(); x++) { int brightness = this.getPixel(x, y); brightnessCounts[brightness]++; } int levelCount = 0; for (int i = 0; i < 256; i++) { if (brightnessCounts[i] > 0) levelCount++; if (levelCount > 2) break; } blackAndWhite = levelCount <= 2; } return blackAndWhite; } @Override public int[] getVerticalCounts() { if (this.verticalCounts == null) { this.verticalCounts = new int[this.getWidth()]; for (int x = 0; x < this.getWidth(); x++) { for (int y = 0; y < this.getHeight(); y++) { int brightness = this.getPixel(x, y); if (brightness < 0) brightness = 256 + brightness; this.verticalCounts[x] += 255 - brightness; } } } return this.verticalCounts; } @Override public void recalculate() { image = null; pixelGrabber = null; brightnessBySectorMap = new HashMap<String, Map<SectionBrightnessMeasurementMethod, double[][]>>(); brightnessMeanBySectorMap = new HashMap<String, Map<SectionBrightnessMeasurementMethod, Double>>(); bitsets = new Hashtable<String, BitSet>(); outlines = new Hashtable<Integer, BitSet>(); brightnessCounts = null; verticalCounts = null; verticalContour = null; totalBrightness = 0; } @Override public String toString() { return "Shape, left(" + this.getLeft() + ")" + ", top(" + this.getTop() + ")" + ", right(" + this.getRight() + ")" + ", bot(" + this.getBottom() + ")" + " [id=" + this.getId() + "]"; } public BufferedImage getImage() { if (image == null && this.jochreImage != null) { image = this.jochreImage.getOriginalImage().getSubimage(this.getLeft(), this.getTop(), this.getWidth(), this.getHeight()); } return image; } public void setImage(BufferedImage image) { this.image = image; } ImagePixelGrabber getPixelGrabber() { if (this.pixelGrabber == null) { this.pixelGrabber = new ImagePixelGrabberImpl(this.getImage()); } return this.pixelGrabber; } void setPixelGrabber(ImagePixelGrabber pixelGrabber) { this.pixelGrabber = pixelGrabber; } @Override public double[] getCentrePoint() { double yCentre = (double) (this.getTop() + this.getBottom()) / 2.0; double xCentre = (double) (this.getLeft() + this.getRight()) / 2.0; return new double[] { xCentre, yCentre }; } public boolean isDirty() { return dirty; } public void setDirty(boolean dirty) { this.dirty = dirty; } @Override public Set<Decision<Letter>> getLetterGuesses() { if (this.letterGuesses == null) { this.letterGuesses = new TreeSet<Decision<Letter>>(); } return letterGuesses; } @Override public void writeImageToLog() { DecimalFormat df = new DecimalFormat("000"); for (int y = 0; y < this.getHeight(); y++) { String line = ""; if (y == this.getMeanLine()) { line += "MMM"; } else if (y == this.getBaseLine()) { line += "BBB"; } else { line += df.format(y); } for (int x = 0; x < this.getWidth(); x++) { if (this.isPixelBlack(x, y, this.getJochreImage().getBlackThreshold())) line += "x"; else line += "o"; } LOG.debug(line); } } public LetterGuesserService getLetterGuesserService() { return letterGuesserService; } public void setLetterGuesserService(LetterGuesserService letterGuesserService) { this.letterGuesserService = letterGuesserService; } public String getOriginalGuess() { return originalGuess; } public void setOriginalGuess(String originalGuess) { if (originalGuess == null) originalGuess = ""; if (!this.originalGuess.equals(originalGuess)) { this.originalGuess = originalGuess; this.dirty = true; } } @Override public int getTotalBrightness() { if (totalBrightness == 0) { for (int y = 0; y < this.getHeight(); y++) { for (int x = 0; x < this.getWidth(); x++) { int brightness = 255 - this.getPixel(x, y); totalBrightness += brightness; } } } return totalBrightness; } public int[] getStartingPoint() { if (this.startingPoint == null) { int startX = -1, startY = -1; for (int y = 0; y < this.getHeight(); y++) { for (int x = 0; x < this.getWidth(); x++) { if (this.isPixelBlack(x, y, this.getJochreImage().getSeparationThreshold())) { startX = x; startY = y; break; } } if (startX >= 0) break; } this.startingPoint = new int[] { startX, startY }; } return startingPoint; } public void setStartingPoint(int[] startingPoint) { this.startingPoint = startingPoint; } @Override public int getXHeight() { return this.baseLine - this.meanLine; } @Override public TreeSet<VerticalLineSegment> getVerticalLineSegments() { if (lines == null) { WritableImageGrid mirror = this.graphicsService.getEmptyMirror(this); int[] startingPoint = this.getStartingPoint(); int startX = startingPoint[0]; int startY = startingPoint[1]; // let's imagine // 0 X 0 0 x x // x x x 0 0 x // 0 0 x x x x //as we build the shape, we keep track in memory of all of the vertical line segments that we find //and which vertical line segments touch them to the right and left //a segment can have more than one left segment (if they're broken by a white space) Stack<VerticalLineSegment> lineStack = new Stack<VerticalLineSegment>(); lines = new TreeSet<VerticalLineSegment>(); VerticalLineSegment firstLine = new VerticalLineSegment(startX, startY); lineStack.push(firstLine); while (!lineStack.isEmpty()) { VerticalLineSegment line = lineStack.pop(); // Add this line's pixels to the mirror so that we don't touch it again. for (int rely = line.yTop; rely <= line.yBottom; rely++) mirror.setPixel(line.x, rely, 1); // extend the vertical line segment up and down from this point for (int rely = line.yTop - 1; rely >= 0; rely--) { if (this.isPixelBlack(line.x, rely, this.getJochreImage().getSeparationThreshold())) { mirror.setPixel(line.x, rely, 1); line.yTop = rely; } else { break; } } for (int rely = line.yBottom + 1; rely < this.getHeight(); rely++) { if (this.isPixelBlack(line.x, rely, this.getJochreImage().getSeparationThreshold())) { mirror.setPixel(line.x, rely, 1); line.yBottom = rely; } else { break; } } //LOG.debug("Adding line x = " + line.x + ", top = " + line.yTop + ", bottom = " + line.yBottom); lines.add(line); // find any points to the left of this segment int relx = line.x - 1; VerticalLineSegment leftLine = null; for (int rely = line.yTop - 1; rely <= line.yBottom + 1; rely++) { if (this.isPixelBlack(relx, rely, this.getJochreImage().getSeparationThreshold())) { if (leftLine == null) { leftLine = new VerticalLineSegment(relx, rely); } else { leftLine.yBottom = rely; } } else { if (leftLine != null) { if (mirror.getPixel(relx, leftLine.yTop) > 0) { // if we already found this line before - let's find it again. for (VerticalLineSegment lineSegment : lines) { if (lineSegment.x == relx) { if (lineSegment.yTop <= leftLine.yTop && leftLine.yTop <= lineSegment.yBottom) { leftLine = lineSegment; break; } } } } else if (lineStack.contains(leftLine)) { leftLine = lineStack.get(lineStack.indexOf(leftLine)); } else { lineStack.push(leftLine); } line.leftSegments.add(leftLine); leftLine = null; } } } // next rely // add the last line if (leftLine != null) { if (mirror.getPixel(relx, leftLine.yTop) > 0) { // if we already found this line before - let's find it again. for (VerticalLineSegment lineSegment : lines) { if (lineSegment.x == relx) { if (lineSegment.yTop <= leftLine.yTop && leftLine.yTop <= lineSegment.yBottom) { leftLine = lineSegment; break; } } } } else if (lineStack.contains(leftLine)) { leftLine = lineStack.get(lineStack.indexOf(leftLine)); } else { lineStack.push(leftLine); } line.leftSegments.add(leftLine); } // find any points to the right of this segment relx = line.x + 1; VerticalLineSegment rightLine = null; for (int rely = line.yTop - 1; rely <= line.yBottom + 1; rely++) { if (this.isPixelBlack(relx, rely, this.getJochreImage().getSeparationThreshold())) { if (rightLine == null) { rightLine = new VerticalLineSegment(relx, rely); } else { rightLine.yBottom = rely; } } else { if (rightLine != null) { if (mirror.getPixel(relx, rightLine.yTop) > 0) { // if we already found this line before - let's find it again. for (VerticalLineSegment lineSegment : lines) { if (lineSegment.x == relx) { if (lineSegment.yTop <= rightLine.yTop && rightLine.yTop <= lineSegment.yBottom) { rightLine = lineSegment; break; } } } } else if (lineStack.contains(rightLine)) { rightLine = lineStack.get(lineStack.indexOf(rightLine)); } else { lineStack.push(rightLine); } line.rightSegments.add(rightLine); rightLine = null; } } } // next rely // add the last line if (rightLine != null) { if (mirror.getPixel(relx, rightLine.yTop) > 0) { // if we already found this line before - let's find it again. for (VerticalLineSegment lineSegment : lines) { if (lineSegment.x == relx) { if (lineSegment.yTop <= rightLine.yTop && rightLine.yTop <= lineSegment.yBottom) { rightLine = lineSegment; break; } } } } else if (lineStack.contains(rightLine)) { rightLine = lineStack.get(lineStack.indexOf(rightLine)); } else { lineStack.push(rightLine); } line.rightSegments.add(rightLine); } } // next line on stack LOG.debug("Found " + lines.size() + " lines"); } return lines; } @Override public Collection<BridgeCandidate> getBridgeCandidates(double maxBridgeWidth) { if (this.bridgeCandidates == null) { TreeSet<VerticalLineSegment> lines = this.getVerticalLineSegments(); // Now, detect "bridges" which could indicate that the shape should be split // First, detect which spaces are "enclosed" and which touch the outer walls // To do this, build up a set of all inverse (white) lines TreeSet<VerticalLineSegment> inverseLines = new TreeSet<VerticalLineSegment>(); int currentX = -1; VerticalLineSegment previousLine = null; for (VerticalLineSegment line : lines) { //LOG.debug("Checking line x = " + line.x + ", top = " + line.yTop + ", bottom = " + line.yBottom); if (line.x != currentX) { // new x-coordinate if (previousLine != null && previousLine.yBottom < this.getHeight() - 1) { VerticalLineSegment inverseLine = new VerticalLineSegment(previousLine.x, previousLine.yBottom + 1); inverseLine.yBottom = this.getHeight() - 1; inverseLines.add(inverseLine); //LOG.debug("Adding inverse line x = " + inverseLine.x + ", top = " + inverseLine.yTop + ", bottom = " + inverseLine.yBottom); } if (line.yTop > 0) { VerticalLineSegment inverseLine = new VerticalLineSegment(line.x, line.yTop - 1); inverseLine.yTop = 0; inverseLines.add(inverseLine); //LOG.debug("Adding inverse line x = " + inverseLine.x + ", top = " + inverseLine.yTop + ", bottom = " + inverseLine.yBottom); } currentX = line.x; } else if (previousLine != null) { VerticalLineSegment inverseLine = new VerticalLineSegment(previousLine.x, previousLine.yBottom + 1); inverseLine.yBottom = line.yTop - 1; inverseLines.add(inverseLine); //LOG.debug("Adding inverse line x = " + inverseLine.x + ", top = " + inverseLine.yTop + ", bottom = " + inverseLine.yBottom); } previousLine = line; } if (previousLine != null && previousLine.yBottom < this.getHeight() - 1) { VerticalLineSegment inverseLine = new VerticalLineSegment(previousLine.x, previousLine.yBottom + 1); inverseLine.yBottom = this.getHeight() - 1; inverseLines.add(inverseLine); //LOG.debug("Adding inverse line x = " + inverseLine.x + ", top = " + inverseLine.yTop + ", bottom = " + inverseLine.yBottom); } LOG.debug("inverseLines size: " + inverseLines.size()); // Calculate neighbours for inverse lines for (VerticalLineSegment inverseLine : inverseLines) { for (VerticalLineSegment otherLine : inverseLines) { if (otherLine.x == inverseLine.x + 1) { if (inverseLine.yTop - 1 <= otherLine.yBottom && otherLine.yTop <= inverseLine.yBottom + 1) { inverseLine.rightSegments.add(otherLine); otherLine.leftSegments.add(inverseLine); } } if (otherLine.x == inverseLine.x - 1) { if (inverseLine.yTop - 1 <= otherLine.yBottom && otherLine.yTop <= inverseLine.yBottom + 1) { inverseLine.leftSegments.add(otherLine); otherLine.rightSegments.add(inverseLine); } } } } // Eliminate any white lines which somehow touch an edge Stack<VerticalLineSegment> lineStack = new Stack<VerticalLineSegment>(); Set<VerticalLineSegment> outerInverseLines = new HashSet<VerticalLineSegment>(); for (VerticalLineSegment inverseLine : inverseLines) { if (inverseLine.yTop == 0 || inverseLine.x == 0 || inverseLine.yBottom == this.getHeight() - 1 || inverseLine.x == this.getWidth() - 1) lineStack.push(inverseLine); } while (!lineStack.isEmpty()) { VerticalLineSegment inverseLine = lineStack.pop(); if (!inverseLine.touched) { inverseLine.touched = true; outerInverseLines.add(inverseLine); //LOG.debug("Outer inverse line x = " + inverseLine.x + ", top = " + inverseLine.yTop + ", bottom = " + inverseLine.yBottom); for (VerticalLineSegment rightLine : inverseLine.rightSegments) lineStack.push(rightLine); for (VerticalLineSegment leftLine : inverseLine.leftSegments) { lineStack.push(leftLine); } } } LOG.debug("outerInverseLines size: " + outerInverseLines.size()); Set<VerticalLineSegment> enclosedInverseLines = new HashSet<VerticalLineSegment>(inverseLines); enclosedInverseLines.removeAll(outerInverseLines); LOG.debug("enclosedInverseLines.size: " + enclosedInverseLines.size()); if (LOG.isDebugEnabled()) { for (VerticalLineSegment inverseLine : enclosedInverseLines) LOG.debug("Enclosed inverse line x = " + inverseLine.x + ", top = " + inverseLine.yTop + ", bottom = " + inverseLine.yBottom); } // Add bridge candidates // based on maximum line length and having exactly one neighbour on each side LOG.debug("Adding bridge candidates"); List<BridgeCandidate> candidateList = new ArrayList<BridgeCandidate>(); for (VerticalLineSegment line : lines) { if (line.rightSegments.size() == 1 && line.leftSegments.size() == 1 && line.length() <= maxBridgeWidth) { // also the bridge width should be considered where two vertical lines touch each other // rather than for the full length of the line BridgeCandidate candidate = null; VerticalLineSegment rightLine = line.rightSegments.iterator().next(); VerticalLineSegment leftLine = line.leftSegments.iterator().next(); int leftTopTouch = (leftLine.yTop > line.yTop ? leftLine.yTop : line.yTop); int leftBottomTouch = (leftLine.yBottom < line.yBottom ? leftLine.yBottom : line.yBottom); int rightTopTouch = (rightLine.yTop > line.yTop ? rightLine.yTop : line.yTop); int rightBottomTouch = (rightLine.yBottom < line.yBottom ? rightLine.yBottom : line.yBottom); int rightLength = rightTopTouch - rightBottomTouch; int leftLength = leftTopTouch - leftBottomTouch; if (line.length() <= maxBridgeWidth || rightLength <= maxBridgeWidth || leftLength <= maxBridgeWidth) { candidate = new BridgeCandidate(this, line); if (rightLength < leftLength && rightLength < line.length()) { candidate.topTouch = rightTopTouch; candidate.bottomTouch = rightBottomTouch; } else if (leftLength < line.length()) { candidate.topTouch = leftTopTouch; candidate.bottomTouch = leftBottomTouch; } LOG.debug("Adding bridge candidate x = " + candidate.x + ", top = " + candidate.yTop + ", bottom = " + candidate.yBottom); candidateList.add(candidate); } } } LOG.debug("Bridge candidate size: " + candidateList.size()); LOG.debug("Eliminating candidates with shorter neighbor"); Set<BridgeCandidate> candidatesToEliminate = null; if (candidateList.size() > 0) { // eliminate any bridge candidates that touch a shorter bridge candidate candidatesToEliminate = new HashSet<BridgeCandidate>(); for (int i = 0; i < candidateList.size() - 1; i++) { BridgeCandidate candidate = candidateList.get(i); for (int j = i + 1; j < candidateList.size(); j++) { BridgeCandidate otherCandidate = candidateList.get(j); if (otherCandidate.x == candidate.x + 1 && candidate.rightSegments.contains(otherCandidate)) { if ((candidate.bridgeWidth()) <= (otherCandidate.bridgeWidth())) { LOG.debug("Eliminating candidate x = " + otherCandidate.x + ", top = " + otherCandidate.yTop + ", bottom = " + otherCandidate.yBottom); candidatesToEliminate.add(otherCandidate); } else { LOG.debug("Eliminating candidate x = " + candidate.x + ", top = " + candidate.yTop + ", bottom = " + candidate.yBottom); candidatesToEliminate.add(candidate); } } } } candidateList.removeAll(candidatesToEliminate); LOG.debug("Bridge candidate size: " + candidateList.size()); // To be a bridge, three additional things have to be true: // (A) intersection between right & left shape = null // (B) weight of right shape & weight of left shape > a certain threshold // (C) little overlap right boundary of left shape, left boundary of right shape LOG.debug("Eliminating candidates touching enclosed space"); // (A) intersection between right & left shape = null // Intersection between right and left shape is non-null // if the line segment X touches an enclosed space immediately above or below candidatesToEliminate = new HashSet<BridgeCandidate>(); for (BridgeCandidate candidate : candidateList) { boolean nullIntersection = true; for (VerticalLineSegment inverseLine : enclosedInverseLines) { if (candidate.x == inverseLine.x) { if (inverseLine.yBottom == candidate.yTop - 1 || inverseLine.yTop == candidate.yBottom + 1) { nullIntersection = false; break; } } } if (!nullIntersection) { LOG.debug("Eliminating candidate x = " + candidate.x + ", top = " + candidate.yTop + ", bottom = " + candidate.yBottom); candidatesToEliminate.add(candidate); } } candidateList.removeAll(candidatesToEliminate); LOG.debug("Remaining bridge candidate size: " + candidateList.size()); // another criterion for avoiding "false splits" is that on both side of the bridge // the shapes pretty rapidly expand in width both up and down LOG.debug("Eliminating candidates without vertical expansion on both sides"); candidatesToEliminate = new HashSet<BridgeCandidate>(); int expansionLimit = (int) Math.ceil(((double) this.getWidth()) / 6.0); for (BridgeCandidate candidate : candidateList) { // take into account the portion touching on the right or left boolean isCandidate = true; Stack<VerticalLineSegment> leftLines = new Stack<VerticalLineSegment>(); Stack<Integer> leftDepths = new Stack<Integer>(); leftLines.push(candidate); leftDepths.push(0); int leftTop = candidate.topTouch; int leftBottom = candidate.bottomTouch; while (!leftLines.isEmpty()) { VerticalLineSegment line = leftLines.pop(); int depth = leftDepths.pop(); if (line.yTop < leftTop) leftTop = line.yTop; if (line.yBottom > leftBottom) leftBottom = line.yBottom; if (depth <= expansionLimit) { for (VerticalLineSegment leftSegment : line.leftSegments) { leftLines.push(leftSegment); leftDepths.push(depth + 1); } } } if (leftTop == candidate.topTouch || leftBottom == candidate.bottomTouch) isCandidate = false; if (isCandidate) { Stack<VerticalLineSegment> rightLines = new Stack<VerticalLineSegment>(); Stack<Integer> rightDepths = new Stack<Integer>(); rightLines.push(candidate); rightDepths.push(0); int rightTop = candidate.topTouch; int rightBottom = candidate.bottomTouch; while (!rightLines.isEmpty()) { VerticalLineSegment line = rightLines.pop(); int depth = rightDepths.pop(); if (line.yTop < rightTop) rightTop = line.yTop; if (line.yBottom > rightBottom) rightBottom = line.yBottom; if (depth <= expansionLimit) { for (VerticalLineSegment rightSegment : line.rightSegments) { rightLines.push(rightSegment); rightDepths.push(depth + 1); } } } if (rightTop == candidate.topTouch || rightBottom == candidate.bottomTouch) isCandidate = false; } if (!isCandidate) { LOG.debug("Eliminating candidate x = " + candidate.x + ", top = " + candidate.yTop + ", bottom = " + candidate.yBottom); candidatesToEliminate.add(candidate); } } candidateList.removeAll(candidatesToEliminate); LOG.debug("Remaining bridge candidate size: " + candidateList.size()); if (LOG.isDebugEnabled()) { for (VerticalLineSegment candidate : candidateList) { LOG.debug("Remaining candidate x = " + candidate.x + ", top = " + candidate.yTop + ", bottom = " + candidate.yBottom); } } } if (candidateList.size() > 0) { // (B) weight of right shape & weight of left shape > a certain threshold // (C) little overlap right boundary of left shape, left boundary of right shape // // We can now divide the shape into n groups, each separated by a candidate // We recursively build a group until we reach a candidate // and indicate whether it's the right or left border of the candidate. // We then keep going from the candidate on to the next one // We keep tab of the size of each group and of its right & left boundaries // at the end we can easily determine the right and left boundaries of each, // as well as the right & left pixel weight List<VerticalLineGroup> groups = new ArrayList<VerticalLineGroup>(); VerticalLineSegment firstLine = lines.first(); lineStack = new Stack<VerticalLineSegment>(); Stack<BridgeCandidate> candidateStack = new Stack<BridgeCandidate>(); Stack<Boolean> fromLeftStack = new Stack<Boolean>(); Stack<Boolean> candidateFromLeftStack = new Stack<Boolean>(); lineStack.push(firstLine); fromLeftStack.push(true); VerticalLineGroup group = new VerticalLineGroup(this); List<BridgeCandidate> touchedCandidates = new ArrayList<BridgeCandidate>(); while (!lineStack.isEmpty()) { while (!lineStack.isEmpty()) { VerticalLineSegment line = lineStack.pop(); boolean fromLeft = fromLeftStack.pop(); if (line.touched) continue; line.touched = true; if (candidateList.contains(line)) { // a candidate! LOG.debug("Touching candidate x = " + line.x + ", top = " + line.yTop + ", bottom = " + line.yBottom); BridgeCandidate candidate = null; for (BridgeCandidate existingCandidate : candidateList) { if (existingCandidate.equals(line)) { candidate = existingCandidate; break; } } boolean foundCandidate = touchedCandidates.contains(candidate); if (!foundCandidate) { touchedCandidates.add(candidate); candidateStack.push(candidate); candidateFromLeftStack.push(fromLeft); if (fromLeft) { // coming from the left group.rightCandidates.add(candidate); candidate.leftGroup = group; } else { group.leftCandidates.add(candidate); candidate.rightGroup = group; } } } else { // not a candidate LOG.debug("Touching line length = " + line.length() + ", x = " + line.x + ", top = " + line.yTop + ", bottom = " + line.yBottom); group.pixelCount += line.length(); if (line.x < group.leftBoundary) group.leftBoundary = line.x; if (line.x > group.rightBoundary) group.rightBoundary = line.x; if (line.yTop < group.topBoundary) group.topBoundary = line.yTop; if (line.yBottom > group.bottomBoundary) group.bottomBoundary = line.yBottom; for (VerticalLineSegment leftLine : line.leftSegments) { lineStack.push(leftLine); fromLeftStack.push(false); } for (VerticalLineSegment rightLine : line.rightSegments) { lineStack.push(rightLine); fromLeftStack.push(true); } } } // no more lines in this group groups.add(group); if (!candidateStack.isEmpty()) { BridgeCandidate candidate = candidateStack.pop(); boolean fromLeft = candidateFromLeftStack.pop(); //lineStack.push(candidate.line); //fromLeftStack.push(fromLeft); LOG.debug("*** New Group ***"); LOG.debug("Next candidate: x = " + candidate.x + ", top = " + candidate.yTop + ", bottom = " + candidate.yBottom); group = new VerticalLineGroup(this); if (fromLeft) { group.leftCandidates.add(candidate); candidate.rightGroup = group; } else { group.rightCandidates.add(candidate); candidate.leftGroup = group; } // add this candidate's neighbours to the lineStack for (VerticalLineSegment leftLine : candidate.leftSegments) { lineStack.push(leftLine); fromLeftStack.push(false); } for (VerticalLineSegment rightLine : candidate.rightSegments) { lineStack.push(rightLine); fromLeftStack.push(true); } } // next candidate on candidate stack } // no more lines to process if (LOG.isDebugEnabled()) { LOG.debug("Found " + groups.size() + " groups"); int i = 1; for (VerticalLineGroup aGroup : groups) { LOG.debug("Group " + i++ + ", pixelCount: " + aGroup.pixelCount + ", leftBoundary: " + aGroup.leftBoundary + ", rightBoundary: " + aGroup.rightBoundary); LOG.debug("Candidates on left: "); for (BridgeCandidate candidate : aGroup.leftCandidates) LOG.debug("Candidate x = " + candidate.x + ", top = " + candidate.yTop + ", bottom = " + candidate.yBottom); LOG.debug("Candidates on right: "); for (BridgeCandidate candidate : aGroup.rightCandidates) LOG.debug("Candidate x = " + candidate.x + ", top = " + candidate.yTop + ", bottom = " + candidate.yBottom); } LOG.debug("Found " + candidateList.size() + " candidates"); for (BridgeCandidate candidate : candidateList) { LOG.debug("Candidate x = " + candidate.x + ", top = " + candidate.yTop + ", bottom = " + candidate.yBottom); LOG.debug("- Left group = pixelCount: " + candidate.leftGroup.pixelCount + ", leftBoundary: " + candidate.leftGroup.leftBoundary + ", rightBoundary: " + candidate.leftGroup.rightBoundary); LOG.debug("- Right group = pixelCount: " + candidate.rightGroup.pixelCount + ", leftBoundary: " + candidate.rightGroup.leftBoundary + ", rightBoundary: " + candidate.rightGroup.rightBoundary); } } // should we log? // calculate each candidate's pixel totals and boundaries for (BridgeCandidate candidate : candidateList) { for (VerticalLineGroup lineGroup : groups) lineGroup.touched = false; Stack<VerticalLineGroup> groupStack = new Stack<VerticalLineGroup>(); groupStack.push(candidate.leftGroup); while (!groupStack.isEmpty()) { VerticalLineGroup lineGroup = groupStack.pop(); if (lineGroup.touched) continue; lineGroup.touched = true; candidate.leftPixels += lineGroup.pixelCount; if (lineGroup.leftBoundary < candidate.leftShapeLeftBoundary) candidate.leftShapeLeftBoundary = lineGroup.leftBoundary; if (lineGroup.rightBoundary > candidate.leftShapeRightBoundary) candidate.leftShapeRightBoundary = lineGroup.rightBoundary; for (BridgeCandidate leftCandidate : lineGroup.leftCandidates) { if (!candidate.equals(leftCandidate)) { candidate.leftPixels += leftCandidate.length(); groupStack.push(leftCandidate.leftGroup); } } for (BridgeCandidate rightCandidate : lineGroup.rightCandidates) { if (!candidate.equals(rightCandidate)) { candidate.leftPixels += rightCandidate.length(); groupStack.push(rightCandidate.rightGroup); } } } // next left group groupStack.push(candidate.rightGroup); while (!groupStack.isEmpty()) { VerticalLineGroup lineGroup = groupStack.pop(); if (lineGroup.touched) continue; lineGroup.touched = true; candidate.rightPixels += lineGroup.pixelCount; if (lineGroup.leftBoundary < candidate.rightShapeLeftBoundary) candidate.rightShapeLeftBoundary = lineGroup.leftBoundary; if (lineGroup.rightBoundary > candidate.rightShapeRightBoundary) candidate.rightShapeRightBoundary = lineGroup.rightBoundary; for (BridgeCandidate leftCandidate : lineGroup.leftCandidates) { if (!candidate.equals(leftCandidate)) { candidate.rightPixels += leftCandidate.length(); groupStack.push(leftCandidate.leftGroup); } } for (BridgeCandidate rightCandidate : lineGroup.rightCandidates) { if (!candidate.equals(rightCandidate)) { candidate.rightPixels += rightCandidate.length(); groupStack.push(rightCandidate.rightGroup); } } } // next right group } // next candidate } // do we have any candidates? this.bridgeCandidates = candidateList; } // lazy load return this.bridgeCandidates; } @Override public BridgeCandidate getBestBridgeCandidate(double maxBridgeWidth) { Collection<BridgeCandidate> bridgeCandidates = this.getBridgeCandidates(maxBridgeWidth); BridgeCandidate bestCandidate = null; if (bridgeCandidates.size() == 0) { // do nothing } else if (bridgeCandidates.size() == 1) { bestCandidate = bridgeCandidates.iterator().next(); } else { //TODO: rank the candidates // we could do machine learning here, but we'll need a proper dataset for that double bestScore = 0; for (BridgeCandidate candidate : bridgeCandidates) { if (candidate.score() > bestScore) { bestCandidate = candidate; bestScore = candidate.score(); } } } return bestCandidate; } @Override public BridgeCandidate getBestBridgeCandidate() { double maxBridgeWidth = (double) this.getXHeight() / 4.0; return this.getBestBridgeCandidate(maxBridgeWidth); } @Override public List<Split> getSplits() { if (splits == null) { splits = new PersistentListImpl<Split>(); splits.addAll(this.boundaryService.findSplits(this)); } return splits; } @Override public Split addSplit(int position) { List<Split> splits = this.getSplits(); Split split = this.boundaryService.getEmptySplit(this); split.setPosition(position); splits.add(split); return split; } @Override public void deleteSplit(int position) { Split splitToDelete = null; for (Split split : this.getSplits()) { if (split.getPosition() == position) { splitToDelete = split; break; } } if (splitToDelete != null) this.getSplits().remove(splitToDelete); } @Override public int[][] getVerticalContour() { if (verticalContour == null) { verticalContour = new int[this.getWidth()][2]; for (int x = 0; x < this.getWidth(); x++) { for (int y = 0; y < this.getHeight(); y++) { if (this.isPixelBlack(x, y)) { verticalContour[x][0] = y; break; } } for (int y = this.getHeight() - 1; y >= 0; y--) { if (this.isPixelBlack(x, y)) { verticalContour[x][1] = y; break; } } } } return verticalContour; } public BoundaryService getBoundaryService() { return boundaryService; } public void setBoundaryService(BoundaryService boundaryService) { this.boundaryService = boundaryService; } @Override public int hashCode() { final int prime = 3; int result = 1; result = prime * result + bottom; result = prime * result + ((jochreImage == null) ? 0 : jochreImage.hashCode()); result = prime * result + left; result = prime * result + right; result = prime * result + top; return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; ShapeImpl other = (ShapeImpl) obj; if (bottom != other.bottom) return false; if (left != other.left) return false; if (right != other.right) return false; if (top != other.top) return false; if (!this.getJochreImage().equals(other.getJochreImage())) return false; return true; } @Override public Shape getShape() { return this; } @Override public double getConfidence() { if (confidence == null) { confidence = 1.0; if (this.letterGuesses != null) { for (Decision<Letter> guess : this.letterGuesses) { if (guess.getOutcome().getString().equals(this.letter)) { confidence = guess.getProbability(); break; } } } } return confidence; } public void setConfidence(double confidence) { this.confidence = confidence; } @SuppressWarnings("unchecked") @Override public <T, Y> FeatureResult<Y> getResultFromCache(Feature<T, Y> feature, RuntimeEnvironment env) { FeatureResult<Y> result = null; String key = feature.getName() + env.getKey(); if (this.featureResults.containsKey(key)) { result = (FeatureResult<Y>) this.featureResults.get(key); } return result; } @Override public <T, Y> void putResultInCache(Feature<T, Y> feature, FeatureResult<Y> featureResult, RuntimeEnvironment env) { String key = feature.getName() + env.getKey(); this.featureResults.put(key, featureResult); } }