001/*******************************************************************************
002 * This software is provided as a supplement to the authors' textbooks on digital
003 * image processing published by Springer-Verlag in various languages and editions.
004 * Permission to use and distribute this software is granted under the BSD 2-Clause
005 * "Simplified" License (see http://opensource.org/licenses/BSD-2-Clause).
006 * Copyright (c) 2006-2023 Wilhelm Burger, Mark J. Burge. All rights reserved.
007 * Visit https://imagingbook.com for additional details.
008 ******************************************************************************/
009package imagingbook.common.geometry.circle;
010
011import imagingbook.common.geometry.basic.Pnt2d;
012import imagingbook.common.geometry.basic.Primitive2d;
013import imagingbook.common.geometry.shape.ShapeProducer;
014import imagingbook.common.math.Arithmetic;
015
016import java.awt.Shape;
017import java.awt.geom.Arc2D;
018import java.awt.geom.Path2D;
019import java.util.Locale;
020
021import static imagingbook.common.math.Arithmetic.sqr;
022import static java.lang.Math.abs;
023import static java.lang.Math.sqrt;
024
025/**
026 * <p>
027 * Represents a geometric circle with center point (xc, yc) and radius r. Instances are immutable. See Sec. 11.1.1 and
028 * Appendix F.2.1 of [1] for details.
029 * </p>
030 * <p>
031 * [1] W. Burger, M.J. Burge, <em>Digital Image Processing &ndash; An Algorithmic Introduction</em>, 3rd ed, Springer
032 * (2022).
033 * </p>
034 *
035 * @author WB
036 * @version 2022/11/17
037 */
038public class GeometricCircle implements ShapeProducer, Primitive2d {
039        
040        /**
041         * Circle parameter.
042         */
043        public final double xc, yc, r;
044
045        /**
046         * Constructor.
047         * @param xc center x-coordinate
048         * @param yc center y-coordinate
049         * @param r circle radius
050         */
051        public GeometricCircle(double xc, double yc, double r) {
052                if (r < 0) {
053                        throw new IllegalArgumentException("negative circle radius");
054                }
055                this.xc = xc;
056                this.yc = yc;
057                this.r = r;             
058        }
059
060        /**
061         * Constructor. Creates a new {@link GeometricCircle} instance from parameters p = [xc, yc, r].
062         *
063         * @param p circle parameters [xc, yc, r]
064         */
065        public GeometricCircle(double[] p) {
066                this(p[0], p[1], p[2]);
067        }
068
069        /**
070         * Constructor. Creates a new {@link GeometricCircle} from a {@link AlgebraicCircle} instance.
071         *
072         * @param ac a {@link AlgebraicCircle} instance
073         */
074        public GeometricCircle(AlgebraicCircle ac) {
075                this(getGeometricCircleParameters(ac));
076        }
077        
078        private static double[] getGeometricCircleParameters(AlgebraicCircle ac) {
079                double[] p = ac.getParameters();
080                if (Arithmetic.isZero(p[0])) {
081                        throw new RuntimeException("infinite circle radius");
082                }
083                double A = p[0];
084                double B = p[1];
085                double C = p[2];
086                double D = p[3];
087                double xc = -B / (2 * A);
088                double yc = -C / (2 * A);
089                double r = sqrt(sqr(B) + sqr(C) - 4 * A * D) / abs(2 * A);
090                return new double[] {xc, yc, r};
091        }
092        
093        // --------------------------------------------------------------------------------
094        
095        /**
096         * Returns a vector of geometric circle parameters (xc, yc, r).
097         * 
098         * @return geometric circle parameters (xc, yc, r)
099         */
100        public double[] getParameters() {
101                return new double[] {xc, yc, r};
102        }
103        
104        /**
105         * Returns the center point of this circle.
106         * @return the center point
107         */
108        public Pnt2d getCenter() {
109                return Pnt2d.from(xc, yc);
110        }
111
112        // --------------------------------------------------------------------------------
113
114        /**
115         * Calculates and returns the mean of the squared distances between this circle and a set of 2D points.
116         *
117         * @param points a set of sample points (usually the points used for fitting)
118         * @return the mean squared error
119         */
120        public double getMeanSquareError(Pnt2d[] points) {
121                final int n = points.length;
122                double sumR2 = 0;
123                for (int i = 0; i < n; i++) {
124                        sumR2 += sqr(getDistance(points[i]));
125                }
126                return sumR2 / n;
127        }
128
129        /**
130         * Returns the (unsigned) distance between the specified point and this circle. The result is always non-negative.
131         *
132         * @param p a 2D point
133         * @return the point's distance from the circle
134         */
135        @Override
136        public double getDistance(Pnt2d p) {
137                return Math.abs(getSignedDistance(p));
138        }
139
140        /**
141         * Returns the signed distance between the specified point and this circle. The result is positive for points
142         * outside the circle, negative inside.
143         *
144         * @param p a 2D point
145         * @return the point's signed distance from the circle
146         */
147        public double getSignedDistance(Pnt2d p) {
148                double dx = p.getX() - this.xc;
149                double dy = p.getY() - this.yc;
150                double rp = Math.hypot(dx, dy);
151                return rp - this.r;
152        }
153        
154        // ------------------------------------------------------------------
155
156        private Shape getCenterShape(double radius) {
157                Path2D path = new Path2D.Double();
158                path.moveTo(xc - radius, yc);
159                path.lineTo(xc + radius, yc);
160                path.moveTo(xc, yc - radius);
161                path.lineTo(xc, yc + radius);
162                return path;
163        }
164        
165        private Shape getOuterShape() {
166                return new Arc2D.Double(xc - r, yc - r, 2 * r, 2 * r, 0, 360, Arc2D.OPEN);
167        }
168        
169        @Override
170        public Shape getShape(double scale) {
171                return getOuterShape() ;
172        }
173        
174        @Override
175        public Shape[] getShapes(double scale) {
176                return new Shape[] {getOuterShape(), getCenterShape(scale)};
177        }
178        
179        // ---------------------------------------------------------------------------------
180        
181        @Override
182        public boolean equals(Object other) {
183                if (this == other) {
184                        return true;
185                }
186                if (!(other instanceof GeometricCircle)) {
187            return false;
188        }
189                return this.equals((GeometricCircle) other, Arithmetic.EPSILON_DOUBLE);
190        }
191        
192        
193        public boolean equals(GeometricCircle other, double tolerance) {
194                return 
195                                Arithmetic.equals(xc, other.xc, tolerance) &&
196                                Arithmetic.equals(yc, other.yc, tolerance) &&
197                                Arithmetic.equals(r, other.r, tolerance) ;
198        }
199        
200        public boolean equals(GeometricCircle other, double tolXc, double tolYc, double tolR) {
201                return 
202                                Arithmetic.equals(xc, other.xc, tolXc) &&
203                                Arithmetic.equals(yc, other.yc, tolYc) &&
204                                Arithmetic.equals(r, other.r, tolR) ;
205        }
206        
207        /**
208         * Returns a copy of this circle.
209         * @return a copy of this circle
210         */
211        public GeometricCircle duplicate() {
212                return new GeometricCircle(this.getParameters());
213        }
214        
215        @Override
216        public String toString() {
217                return String.format(Locale.US, "%s [xc=%f, yc=%f, r=%f]", 
218                                this.getClass().getSimpleName(), xc, yc, r);
219        }
220}