/* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License version 3 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, see <http://www.gnu.org/licenses/>. */ package org.cirqwizard.generation; import org.cirqwizard.geom.*; import org.cirqwizard.logging.LoggerFactory; import org.cirqwizard.settings.ApplicationConstants; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.logging.Level; public class Vectorizer { private static final int INITIAL_SAMPLE_COUNT = (int)(0.15 * ApplicationConstants.RESOLUTION); // Amount of samples to process before trying to decide which curve it is private static final int SAMPLE_COUNT = (int)(0.1 * ApplicationConstants.RESOLUTION); // Amount of last processed points to hold for deviation calculation private static final double ANGULAR_THRESHOLD = Math.toRadians(3); // Threshold of angular difference which results in a new segment start private static final int MAX_ARC_DEVIATION = 10; // Tolerated deviation of the distance from arc's center to its points from the radius private static final double LOW_UNCERTAINTY_THRESHOLD = 0.6; // Arcs with uncertainty lower than that are processed as arcs private static final double HIGH_UNCERTAINTY_THRESHOLD = 10.0; // Arcs with uncertainty higher than that are processed as segments private byte[] windowData; private int width; private int height; private List<Circle> knownCircles; private Point current; private Line currentSegment; private MatchedArc matchedArc; private ArrayList<Curve> result = new ArrayList<>(); private LinkedList<Point> segmentPoints = new LinkedList<>(); public Vectorizer(byte[] windowData, int width, int height, List<Circle> knownCircles, int x, int y) { this.windowData = windowData; this.width = width; this.height = height; this.knownCircles = knownCircles; current = new Point(x, y); currentSegment = new Line(current, current); } public List<Curve> trace() { int segmentCounter = 0; LinkedList<Point> lastPoints = new LinkedList<>(); segmentPoints = new LinkedList<>(); double angle = 0; matchedArc = null; do { lastPoints.addLast(current); int sampleCount = segmentCounter <= INITIAL_SAMPLE_COUNT ? INITIAL_SAMPLE_COUNT : (matchedArc == null ? SAMPLE_COUNT : (int)((double) matchedArc.getCircle().getRadius() * 2 * (Math.PI / 15))); while (lastPoints.size() > sampleCount) lastPoints.removeFirst(); segmentPoints.addLast(current); segmentCounter++; currentSegment.setTo(current); boolean restart = false; if (segmentCounter == INITIAL_SAMPLE_COUNT) { angle = calculateAngle(currentSegment.getFrom(), current); matchedArc = fitArc(segmentPoints, calculateSegmentDeviation(segmentPoints)); } else if (segmentCounter > INITIAL_SAMPLE_COUNT) { if (matchedArc != null && matchedArc.getUncertainty() >= LOW_UNCERTAINTY_THRESHOLD) matchedArc = fitArc(segmentPoints, calculateSegmentDeviation(segmentPoints)); if (matchedArc != null && matchedArc.getUncertainty() < LOW_UNCERTAINTY_THRESHOLD) restart = Math.abs(matchedArc.getCircle().getRadius() - matchedArc.getCircle().getCenter().distanceTo(current)) >= MAX_ARC_DEVIATION; else if (matchedArc == null || segmentCounter > (double)matchedArc.getCircle().getRadius() * 2 * (Math.PI / 15)) restart = Math.abs(calculateAngle(lastPoints.getFirst(), lastPoints.getLast()) - angle) > ANGULAR_THRESHOLD; } if (restart) { Curve curve = getCurve(calculateAngle(lastPoints.get(0), current), lastPoints.get(0)); result.add(curve); currentSegment = new Line(current, current); segmentPoints.clear(); matchedArc = null; segmentCounter = 0; } windowData[current.getX() + current.getY() * width] = 0; } while (calculateNextPoint()); if (segmentCounter > 10) result.add(getCurve(calculateAngle(lastPoints.get(0), current), lastPoints.get(0))); return result; } private boolean calculateNextPoint() { for (Direction d : directions) { Point p = current.add(d.getVector()); if (p.getX() < 0 || p.getX() >= width || p.getY() < 0 || p.getY() >= height) continue; if (windowData[p.getX() + p.getY() * width] != 0) { current = p; return true; } } return false; } private Curve getCurve(double heading, Point headingStartPoint) { if (matchedArc == null || matchedArc.getUncertainty() > HIGH_UNCERTAINTY_THRESHOLD) return currentSegment; double centerAngle = calculateAngle(headingStartPoint, matchedArc.getCircle().getCenter()); double headingCenterAngle = heading - centerAngle; if (headingCenterAngle < -Math.PI) headingCenterAngle += Math.PI * 2; if (headingCenterAngle > Math.PI) headingCenterAngle -= Math.PI * 2; boolean clockwise = headingCenterAngle > 0; int r1 = (int)matchedArc.getCircle().getCenter().distanceTo(currentSegment.getFrom()); int r2 = (int)matchedArc.getCircle().getCenter().distanceTo(currentSegment.getTo()); if (Math.abs(r1 - matchedArc.getCircle().getRadius()) > MAX_ARC_DEVIATION + 1|| Math.abs(r2 - matchedArc.getCircle().getRadius()) > MAX_ARC_DEVIATION + 1) LoggerFactory.getApplicationLogger().log(Level.WARNING, "Arc geometry violation: " + matchedArc + " / " + currentSegment + " / " + r1 + " | " + r2); return new Arc(currentSegment.getFrom(), currentSegment.getTo(), matchedArc.getCircle().getCenter(), matchedArc.getCircle().getRadius(), clockwise); } private double calculateAngle(Point start, Point end) { return Math.atan2(end.getY() - start.getY(), end.getX() - start.getX()); } private double calculateSegmentDeviation(LinkedList<Point> points) { double deviation = 0; Point start = points.getFirst(); Point end = points.getLast(); for (Point p : points) { double d = (p.getY() - start.getY()) * (end.getX() - p.getX()) - (end.getY() - start.getY()) * (p.getX() - start.getX()); deviation += d * d; } return Math.sqrt(deviation); } private double calculateArcDeviation(LinkedList<Point> points, Point center, int radius) { double deviation = 0; for (Point p : points) { double d = (double)((p.getX() - center.getX()) * (p.getX() - center.getX()) + (p.getY() - center.getY()) * (p.getY() - center.getY())) - radius * radius; deviation += d * d; } return Math.sqrt(deviation); } private MatchedArc fitArc(LinkedList<Point> points, double segmentDeviation) { double minDeviation = Double.MAX_VALUE; Circle bestFit = null; for (Circle circle : knownCircles) { double deviation = calculateArcDeviation(points, circle.getCenter(), circle.getRadius()); if (deviation < minDeviation) { minDeviation = deviation; bestFit = circle; } } if (bestFit == null) return null; if (Math.abs(bestFit.getRadius() - bestFit.getCenter().distanceTo(points.getFirst())) >= MAX_ARC_DEVIATION || Math.abs(bestFit.getRadius() - bestFit.getCenter().distanceTo(points.getLast())) >= MAX_ARC_DEVIATION) return null; double uncertainty = minDeviation / segmentDeviation; if (uncertainty > HIGH_UNCERTAINTY_THRESHOLD) return null; return new MatchedArc(bestFit, uncertainty); } private static Direction[] directions = {Direction.EAST, Direction.SOUTH_EAST, Direction.SOUTH, Direction.SOUTH_WEST, Direction.WEST, Direction.NORTH_WEST, Direction.NORTH, Direction.NORTH_EAST}; private static enum Direction { NORTH(new Point(0, 1)), NORTH_EAST(new Point(1, 1)), EAST(new Point(1, 0)), SOUTH_EAST(new Point(1, -1)), SOUTH(new Point(0, -1)), SOUTH_WEST(new Point(-1, -1)), WEST(new Point(-1, 0)), NORTH_WEST(new Point(-1, 1)); private Point vector; Direction(Point vector) { this.vector = vector; } public Point getVector() { return vector; } } }