/* * Copyright 2010-2015 Institut Pasteur. * * This file is part of Icy. * * Icy 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. * * Icy 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 Icy. If not, see <http://www.gnu.org/licenses/>. */ package plugins.kernel.roi.roi2d; import icy.math.ArrayMath; import icy.resource.ResourceUtil; import icy.sequence.Sequence; import icy.type.point.Point5D; import java.awt.geom.Ellipse2D; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.util.Collection; /** * @author Stephane */ public class ROI2DEllipse extends ROI2DRectShape { /** * @deprecated */ @Deprecated public ROI2DEllipse(Point2D topLeft, Point2D bottomRight, boolean cm) { this(topLeft, bottomRight); } public ROI2DEllipse(Point2D topLeft, Point2D bottomRight) { super(new Ellipse2D.Double(), topLeft, bottomRight); // set icon (default name is defined by getDefaultName()) setIcon(ResourceUtil.ICON_ROI_OVAL); } /** * Create a ROI ellipse from its rectangular bounds. */ public ROI2DEllipse(double xmin, double ymin, double xmax, double ymax) { this(new Point2D.Double(xmin, ymin), new Point2D.Double(xmax, ymax)); } /** * @deprecated */ @Deprecated public ROI2DEllipse(Rectangle2D rectangle, boolean cm) { this(rectangle); } public ROI2DEllipse(Rectangle2D rectangle) { this(rectangle.getMinX(), rectangle.getMinY(), rectangle.getMaxX(), rectangle.getMaxY()); } public ROI2DEllipse(Ellipse2D ellipse) { this(ellipse.getBounds2D()); } /** * @deprecated */ @Deprecated public ROI2DEllipse(Point2D pt, boolean cm) { this(pt); } public ROI2DEllipse(Point2D pt) { this(new Point2D.Double(pt.getX(), pt.getY()), pt); } /** * Generic constructor for interactive mode */ public ROI2DEllipse(Point5D pt) { this(pt.toPoint2D()); } public ROI2DEllipse() { this(new Point2D.Double(), new Point2D.Double()); } @Override public String getDefaultName() { return "Ellipse2D"; } public Ellipse2D getEllipse() { return (Ellipse2D) shape; } public void setEllipse(Ellipse2D ellipse) { setBounds2D(ellipse.getBounds2D()); } @Override public double getLength(Sequence sequence) throws UnsupportedOperationException { final Ellipse2D ellipse = getEllipse(); return computeEllipsePerimeter(ellipse.getWidth() * 0.5d * sequence.getPixelSizeX(), ellipse.getHeight() * 0.5d * sequence.getPixelSizeY()); } @Override public double computeNumberOfContourPoints() { final Ellipse2D ellipse = getEllipse(); return computeEllipsePerimeter(ellipse.getWidth() * 0.5d, ellipse.getHeight() * 0.5d); } /** * Calculating the perimeter of an ellipse is non-trivial. Here we follow the approximation * proposed in:<br/> * Ramanujan, S., "Modular Equations and Approximations to Pi," Quart. J. Pure. Appl. Math., * v.45 (1913-1914), 350-372 * * @since Icy 1.5.3.2 */ public static double computeEllipsePerimeter(double w, double h) { double result = (w - h) / (w + h); result *= result; return Math.PI * (w + h) * (1 + (result / 4) + ((result * result) / 64) + ((result * result * result) / 256)); } @Override public double computeNumberOfPoints() { final Ellipse2D ellipse = getEllipse(); return Math.PI * ellipse.getWidth() * ellipse.getHeight() / 4d; } /** * Adjust the ROI to fit the specified list of coordinates with a circle * * @param points * the list of points to fit */ public void setToFitCircle(Collection<? extends Point2D> points) { int nbPoints = points.size(); double[] xCoords = new double[nbPoints]; double[] yCoords = new double[nbPoints]; for (Point2D point : points) { nbPoints--; xCoords[nbPoints] = point.getX(); yCoords[nbPoints] = point.getY(); } setToFitCircle(xCoords, yCoords); } /** * Circle fit by Taubin. <br/> * Reference: G. Taubin, "Estimation Of Planar Curves, Surfaces And Nonplanar Space Curves * Defined By Implicit * Equations, With Applications To Edge And Range Image Segmentation", IEEE Trans. PAMI, Vol. * 13, pages 1115-1138, * (1991).<br/> * This method is a port to Icy from the original <a * href="http://www.mathworks.com/matlabcentral/fileexchange/22678">Matlab code from Nikolai * Chernov (2009)</a> * * @param xCoords * the X coordinates of the points to fit * @param yCoords * the Y coordinates of the points to fit */ private void setToFitCircle(double[] xCoords, double[] yCoords) { int n = xCoords.length; if (n != yCoords.length) throw new IllegalArgumentException("Coordinate arrays must have the same size"); Point2D centroid = new Point2D.Double(ArrayMath.mean(xCoords), ArrayMath.mean(yCoords)); // temporary buffer to save memory double[] buffer = new double[n]; double[] X = ArrayMath.subtract(xCoords, centroid.getX()); double[] Y = ArrayMath.subtract(yCoords, centroid.getY()); double[] XX = ArrayMath.multiply(X, X); double[] YY = ArrayMath.multiply(Y, Y); double[] Z = ArrayMath.add(XX, YY); // compute normalized moments double Mxx = ArrayMath.sum(XX) / n; double Mxy = ArrayMath.sum(ArrayMath.multiply(X, Y, buffer)) / n; double Myy = ArrayMath.sum(YY) / n; double Mxz = ArrayMath.sum(ArrayMath.multiply(X, Z, buffer)) / n; double Myz = ArrayMath.sum(ArrayMath.multiply(Y, Z, buffer)) / n; double Mzz = ArrayMath.sum(ArrayMath.multiply(Z, Z, buffer)) / n; // computing the coefficients of the characteristic polynomial double Mz = Mxx + Myy; double Cov_xy = Mxx * Myy - Mxy * Mxy; double A3 = 4 * Mz; double A2 = -3 * Mz * Mz - Mzz; double A1 = Mzz * Mz + 4 * Cov_xy * Mz - Mxz * Mxz - Myz * Myz - Mz * Mz * Mz; double A0 = Mxz * Mxz * Myy + Myz * Myz * Mxx - Mzz * Cov_xy - 2 * Mxz * Myz * Mxy + Mz * Mz * Cov_xy; double A22 = A2 + A2; double A33 = A3 + A3 + A3; double xold, xnew = 0; double yold, ynew = 1e+20; double epsilon = 1e-12; int IterMax = 20; // Newton's method starting at x=0 for (int iter = 0; iter < IterMax; iter++) { yold = ynew; ynew = A0 + xnew * (A1 + xnew * (A2 + xnew * A3)); if (Math.abs(ynew) > Math.abs(yold)) { System.err.println("Circle fitting error: Newton-Taubin goes wrong direction: |ynew| > |yold|"); xnew = 0; break; } double Dy = A1 + xnew * (A22 + xnew * A33); xold = xnew; xnew = xold - ynew / Dy; if (Math.abs((xnew - xold) / xnew) < epsilon) break; if (iter >= IterMax) { System.err.println("Circle fitting error: Newton-Taubin will not converge"); xnew = 0; } if (xnew < 0) { System.out.println("Newton-Taubin negative root: x=" + xnew); xnew = 0; } } // computing the circle parameters double DET = xnew * xnew - xnew * Mz + Cov_xy; double xCenter = (Mxz * (Myy - xnew) - Myz * Mxy) / DET / 2; double yCenter = (Myz * (Mxx - xnew) - Mxz * Mxy) / DET / 2; double radius = Math.sqrt(xCenter * xCenter + yCenter * yCenter + Mz); xCenter += centroid.getX(); yCenter += centroid.getY(); setEllipse(new Ellipse2D.Double(xCenter - radius, yCenter - radius, 2 * radius, 2 * radius)); } }