//----------------------------------------------------------------------------// // // // C i r c l e // // // //----------------------------------------------------------------------------// // <editor-fold defaultstate="collapsed" desc="hdr"> // // Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. // // This software is released under the GNU General Public License. // // Goto http://kenai.com/projects/audiveris to report bugs or suggestions. // //----------------------------------------------------------------------------// // </editor-fold> package omr.math; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import Jama.Matrix; import java.awt.geom.CubicCurve2D; import java.awt.geom.Line2D; import java.awt.geom.Point2D; import static java.lang.Math.*; import java.util.ArrayList; /** * Class {@code Circle} handles a circle (or a portion of circle) * which approximates a collection of data points. * Besides usual characteristics of a circle (center, radius), and of a circle * arc (start and stop angles) it also defines the approximating bezier curve. * * @author Hervé Bitteur */ public class Circle { //~ Static fields/initializers --------------------------------------------- /** Usual logger utility */ private static final Logger logger = LoggerFactory.getLogger(Circle.class); /** Size for matrices used to compute the circle */ private static final int dim = 4; //~ Instance fields -------------------------------------------------------- /** Mean algebraic distance between circle and the defining points */ private final double distance; // Circle characteristics /** Center */ private Point2D.Double center; /** Radius */ private Double radius; /** Starting limit of circle arc */ private Double startAngle; /** Stopping limit of circle arc */ private Double stopAngle; /** Bezier curve for circle arc */ private CubicCurve2D.Double curve; // Bounding coordinates private double xMax = Double.MIN_VALUE; private double yMax = Double.MIN_VALUE; private double xMin = Double.MAX_VALUE; private double yMin = Double.MAX_VALUE; //~ Constructors ----------------------------------------------------------- //--------// // Circle // //--------// /** * Creates a new instance of Circle, defined by a set of points. * * @param x array of abscissae * @param y array of ordinates */ public Circle (double[] x, double[] y) { fit(x, y); computeAngles(x, y); distance = computeDistance(x, y); } //--------// // Circle // //--------// /** * Creates a new instance of Circle, fitted to 3 defining points. * The provided collection of coordinates is used only to compute the * resulting distance. * * @param left left defining point * @param middle middle defining point * @param right right defining point * @param x array of abscissae (including the defining points) * @param y array of ordinates (including the defining points) */ public Circle (Point2D left, Point2D middle, Point2D right, double[] x, double[] y) { defineCircle(left, middle, right); computeAngles(x, y); distance = computeDistance(x, y); } //~ Methods ---------------------------------------------------------------- //-----------// // getCenter // //-----------// /** * Report the circle center. * * @return the center of the circle */ public Point2D.Double getCenter () { return center; } //----------// // getCurve // //----------// /** * Report the Bezier curve which best approximates the circle arc. * * @return the Bezier curve */ public CubicCurve2D.Double getCurve () { if (curve == null) { computeCurve(); } return curve; } //-------------// // getDistance // //-------------// /** * Report the mean distance between the data points and the circle. * * @return the mean distance */ public double getDistance () { return distance; } //-----------// // getRadius // //-----------// /** * Report the circle radius. * * @return the circle radius */ public Double getRadius () { return radius; } //---------------// // getStartAngle // //---------------// /** * Report the angle at start of the circle arc. * * @return the starting angle, in radians */ public Double getStartAngle () { return startAngle; } //--------------// // getStopAngle // //--------------// /** * Report the angle at stop of the circle arc. * * @return the stopping angle, in radians */ public Double getStopAngle () { return stopAngle; } //----------// // toString // //----------// @Override public String toString () { StringBuilder sb = new StringBuilder(); sb.append("{Circle"); sb.append(String.format(" dist=%g", distance)); sb.append(String.format(" center[%g,%g]", center.x, center.y)); sb.append(String.format(" radius=%g", radius)); if ((startAngle != null) && (stopAngle != null)) { sb.append( String.format( " angles=(%g,%g)", toDegrees(startAngle), toDegrees(stopAngle))); } sb.append("}"); return sb.toString(); } //---------------// // computeAngles // //---------------// /** * Compute the start and stop angles of a circle. */ private void computeAngles (double[] x, double[] y) { // Get all angles, split into buckets final int BUCKET_NB = 8; final int[] buckets = new int[BUCKET_NB]; for (int i = 0; i < BUCKET_NB; i++) { buckets[i] = 0; } final double bucketSize = (2 * PI) / BUCKET_NB; ArrayList<Double> angles = new ArrayList<>(); for (int i = 0; i < x.length; i++) { // Get an angle between 0 and 2*PI double angle = PI + atan2(y[i] - center.y, x[i] - center.x); angles.add(angle); int idx = (int) (angle / bucketSize); if ((idx >= 0) && (idx < BUCKET_NB)) { buckets[idx] += 1; } } // Find an empty bucket int emptyIdx; for (emptyIdx = 0; emptyIdx < BUCKET_NB; emptyIdx++) { if (buckets[emptyIdx] == 0) { break; } } if (emptyIdx >= BUCKET_NB) { logger.debug("No empty sector in circle, this is not a slur"); } else { final double bottom = emptyIdx * bucketSize; double start = 2 * PI; double stop = 0; for (double angle : angles) { angle -= bottom; if (angle < 0) { angle += (2 * PI); } if (angle < start) { start = angle; } if (angle > stop) { stop = angle; } } stop += (bottom - PI); start += (bottom - PI); if (stop < start) { stop += (2 * PI); } startAngle = start; stopAngle = stop; // System.out.println( // "emptyIdx=" + emptyIdx + " startDeg=" + // (float) toDegrees(start) + " stopDeg=" + // (float) toDegrees(stop)); } } //--------------// // computeCurve // //--------------// /** * Compute the bezier points for the circle arc. */ private void computeCurve () { // Make sure we do have an arc defined, rather than a full circle if (((stopAngle == null) || (stopAngle.isNaN())) || ((startAngle == null) || (startAngle.isNaN()))) { return; } // Bezier points for circle arc, centered at origin, with radius 1 double arc = stopAngle - startAngle; double x0 = cos(arc / 2); double y0 = sin(arc / 2); double x1 = (4 - x0) / 3; double y1 = ((1 - x0) * (3 - x0)) / (3 * y0); double x2 = x1; double y2 = -y1; double x3 = x0; double y3 = -y0; // Rotation final double theta = (startAngle + stopAngle) / 2; ///System.out.println("angleDeg/2=" + Math.toDegrees(theta)); final Matrix rotation = new Matrix( new double[][]{ {cos(theta), -sin(theta), 0}, {sin(theta), cos(theta), 0}, {0, 0, 1} }); // Scaling final Matrix scaling = new Matrix( new double[][]{ {radius, 0, 0}, {0, radius, 0}, {0, 0, 1} }); // Translation final Matrix translation = new Matrix( new double[][]{ {1, 0, center.x}, {0, 1, center.y}, {0, 0, 1} }); // Composite operation final Matrix op = translation.times(scaling).times(rotation); final Matrix M0 = op.times( new Matrix( new double[][]{ {x0}, {y0}, {1} })); final Matrix M1 = op.times( new Matrix( new double[][]{ {x1}, {y1}, {1} })); final Matrix M2 = op.times( new Matrix( new double[][]{ {x2}, {y2}, {1} })); final Matrix M3 = op.times( new Matrix( new double[][]{ {x3}, {y3}, {1} })); // Bezier curve (make sure the curve goes from left to right) if (M0.get(0, 0) <= M3.get(0, 0)) { curve = new CubicCurve2D.Double( M0.get(0, 0), M0.get(1, 0), M1.get(0, 0), M1.get(1, 0), M2.get(0, 0), M2.get(1, 0), M3.get(0, 0), M3.get(1, 0)); } else { curve = new CubicCurve2D.Double( M3.get(0, 0), M3.get(1, 0), M2.get(0, 0), M2.get(1, 0), M1.get(0, 0), M1.get(1, 0), M0.get(0, 0), M0.get(1, 0)); } // System.out.println(" P1=" + curve.getP1()); // System.out.println("CP1=" + curve.getCtrlP1()); // System.out.println("CP2=" + curve.getCtrlP2()); // System.out.println(" P2=" + curve.getP2()); } //-----------------// // computeDistance // //-----------------// /** * Compute the mean quadratic distance of all points to the circle. * * @param x array of abscissae * @param y array of ordinates * @return the mean quadratic distance */ private double computeDistance (double[] x, double[] y) { final int nbPoints = x.length; double sum = 0; for (int i = 0; i < nbPoints; i++) { double delta = Math.hypot( x[i] - getCenter().x, y[i] - getCenter().y) - getRadius(); sum += (delta * delta); } return Math.sqrt(sum) / nbPoints; } //--------------// // defineCircle // //--------------// /** * Define the circle by means of a sequence of 3 key points. * * @param left precise left point * @param middle a point rather in the middle * @param right precise right point */ private void defineCircle (Point2D left, Point2D middle, Point2D right) { Line2D prevBisector = LineUtil.bisector( new Line2D.Double(left, middle)); Line2D bisector = LineUtil.bisector( new Line2D.Double(middle, right)); center = LineUtil.intersection( prevBisector.getP1(), prevBisector.getP2(), bisector.getP1(), bisector.getP2()); radius = Math.hypot( center.getX() - right.getX(), center.getY() - right.getY()); } //-----// // fit // //-----// /** * Given a collection of points, determine the best approximating * circle. * The result is available in the center and radius variables. * * @param x the array of abscissae * @param y the array of ordinates */ private void fit (double[] x, double[] y) { // Target is to minimize sum(||a(x2+y2) +dx +ey +f||) w/ constraint a=1 // That is DV, w/ D=Design matrix, and V= [a d e f] // a = 1 can be written CV = 1, w/ C = [1 0 0 0] // Function to minimize is V'D'DV - lambda*(CV -1) // w/ lambda as the Lagrange multiplier // At the extremum, the gradient of this function is nul, so: // 2D'DV -lambda.C' = 0 and a=1 /** number of points */ int nbPoints = x.length; if (nbPoints < 3) { throw new IllegalArgumentException("Less than 3 defining points"); } // Build the design matrix, and remember the bounding box Matrix design = new Matrix(nbPoints, dim); for (int i = 0; i < nbPoints; i++) { final double tx = x[i]; final double ty = y[i]; design.set(i, 0, (tx * tx) + (ty * ty)); design.set(i, 1, tx); design.set(i, 2, ty); design.set(i, 3, 1); if (tx > xMax) { xMax = tx; } if (tx < xMin) { xMin = tx; } if (ty > yMax) { yMax = ty; } if (ty < yMin) { yMin = ty; } } ///print(design, "design"); // Build the scatter matrix Matrix scatter = design.transpose().times(design); ///print(scatter, "scatter"); // Let's impose A = 1 // So let's swap the first column with the Lambda.C' column Matrix first = new Matrix(dim, 1); for (int i = 0; i < dim; i++) { first.set(i, 0, -scatter.get(0, i)); } Matrix newScatter = new Matrix(dim, dim); for (int i = 0; i < dim; i++) { for (int j = 1; j < dim; j++) { newScatter.set(i, j - 1, scatter.get(i, j)); } newScatter.set(i, dim - 1, 0.0); } newScatter.set(0, dim - 1, -0.5); ///print(newScatter, "newScatter"); // Solution [D E F lambda] Matrix newScatterInv = newScatter.inverse(); ///print(newScatterInv, "newScatterInv"); Matrix Solution = newScatterInv.times(first); ///print(Solution, "Solution [D E F Lambda]"); // Coefficients of the algebraic equation // x**2 + y**2 + D*x + E*y + F = 0 double D = Solution.get(0, 0); double E = Solution.get(1, 0); double F = Solution.get(2, 0); // Compute center & radius center = new Point2D.Double(-D / 2, -E / 2); radius = Math.sqrt(((center.x * center.x) + (center.y * center.y)) - F); } }