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 Ch11_Circle_Ellipse_Fitting;
010
011import ij.IJ;
012import ij.ImagePlus;
013import ij.gui.GenericDialog;
014import ij.gui.Overlay;
015import ij.gui.Roi;
016import ij.plugin.filter.PlugInFilter;
017import ij.process.ImageProcessor;
018import imagingbook.common.color.sets.BasicAwtColor;
019import imagingbook.common.geometry.basic.Pnt2d;
020import imagingbook.common.geometry.ellipse.AlgebraicEllipse;
021import imagingbook.common.geometry.ellipse.GeometricEllipse;
022import imagingbook.common.geometry.fitting.ellipse.algebraic.EllipseFitAlgebraic;
023import imagingbook.common.geometry.fitting.ellipse.geometric.EllipseFitGeometric;
024import imagingbook.common.ij.DialogUtils;
025import imagingbook.common.ij.IjUtils;
026import imagingbook.common.ij.RoiUtils;
027import imagingbook.common.ij.overlay.ColoredStroke;
028import imagingbook.common.ij.overlay.ShapeOverlayAdapter;
029import imagingbook.core.jdoc.JavaDocHelp;
030
031import java.util.Locale;
032
033import static imagingbook.common.geometry.fitting.ellipse.algebraic.EllipseFitAlgebraic.FitType.FitzgibbonStable;
034import static imagingbook.common.geometry.fitting.ellipse.geometric.EllipseFitGeometric.FitType.DistanceBased;
035import static imagingbook.common.ij.IjUtils.noCurrentImage;
036
037/**
038 * <p>
039 * ImageJ plugin, performs algebraic ellipse fitting on the current ROI to find an initial ellipse, followed by
040 * geometric fitting. Algebraic and geometric fit methods can be selected (see Sec. 11.2 of [1] for details). If
041 * successful, the resulting ellipses are displayed as a vector overlay (color can be chosen). Sample points are either
042 * collected from the ROI (if available) or collected as foreground pixels (values &gt; 0) from the image. If no image
043 * is currently open, the user is asked to create a suitable sample image.
044 * </p>
045 * <p>
046 * [1] W. Burger, M.J. Burge, <em>Digital Image Processing &ndash; An Algorithmic Introduction</em>, 3rd ed, Springer
047 * (2022).
048 * </p>
049 *
050 * @author WB
051 * @version 2022/10/03
052 */
053public class Ellipse_Fitting implements PlugInFilter, JavaDocHelp {
054        
055        static EllipseFitAlgebraic.FitType AlgebraicFitMethod = FitzgibbonStable;
056        static EllipseFitGeometric.FitType GeometricFitMethod = DistanceBased;
057        static boolean UsePointsFromRoi = false;
058        
059        private static BasicAwtColor AlgebraicFitColor = BasicAwtColor.Red;
060        private static BasicAwtColor GeometricFitColor = BasicAwtColor.Blue;
061        private static double StrokeWidth = 0.5;
062        
063        private ImagePlus im;
064        
065        /**
066         * Constructor, asks to open a predefined sample image if no other image
067         * is currently open.
068         */
069        public Ellipse_Fitting() {
070                if (noCurrentImage()) {
071                        if (DialogUtils.askForSampleImage()) {
072                                IjUtils.run(new Ellipse_Make_Random()); //runPlugIn(Ellipse_Make_Random.class);
073                        }                       
074                }
075        }
076        
077        @Override
078        public int setup(String arg, ImagePlus im) {
079                this.im = im;
080                return DOES_ALL;
081        }
082
083        @Override
084        public void run(ImageProcessor ip) {
085                Roi roi = im.getRoi();
086                UsePointsFromRoi = (roi != null);
087                
088                if (!runDialog()) {
089                        return;
090                }
091                
092                Pnt2d[] points = (UsePointsFromRoi) ?
093                                RoiUtils.getOutlinePointsFloat(roi) :
094                                IjUtils.collectNonzeroPoints(ip);
095                
096                IJ.log("Found points " + points.length);
097                if (points.length < 5) {
098                        IJ.error("At least 5 points are required, but found only " + points.length);
099                        return;
100                }
101                
102                Overlay oly = im.getOverlay();
103                if (oly == null) {
104                        oly = new Overlay();
105                        im.setOverlay(oly);
106                }
107                ShapeOverlayAdapter ola = new ShapeOverlayAdapter(oly); 
108                
109                Pnt2d xref = Pnt2d.from(ip.getWidth()/2, ip.getHeight()/2);     // reference point for ellipse fitting
110                
111                // ------------------------------------------------------------------------
112                EllipseFitAlgebraic fitA = EllipseFitAlgebraic.getFit(AlgebraicFitMethod, points, xref);
113                // ------------------------------------------------------------------------
114                
115                AlgebraicEllipse ae = fitA.getEllipse();                
116                if (ae == null) {
117                        IJ.log("Algebraic fit: no result!");
118                        return;
119                }
120                
121                GeometricEllipse initEllipse = new GeometricEllipse(ae);
122                
123                IJ.log("Initial fit (algebraic):");
124                IJ.log("  ellipse: " + initEllipse.toString());
125                IJ.log(String.format(Locale.US, "  error = %.3f", initEllipse.getMeanSquareError(points)));
126                
127                ColoredStroke initialStroke = new ColoredStroke(StrokeWidth, AlgebraicFitColor.getColor());
128                ola.addShapes(initEllipse.getShapes(3), initialStroke);
129
130                // ------------------------------------------------------------------------
131                EllipseFitGeometric fitG = EllipseFitGeometric.getFit(GeometricFitMethod, points, initEllipse);
132                // ------------------------------------------------------------------------
133                
134                GeometricEllipse finalEllipse = fitG.getEllipse();
135                if (finalEllipse == null) {
136                        IJ.log("Geometric fit: no result!");
137                        return;
138                }
139                
140                IJ.log("Final fit (geometric):");
141                IJ.log("  ellipse: " + finalEllipse.toString());
142                IJ.log(String.format(Locale.US, "  error = %.3f", finalEllipse.getMeanSquareError(points)));
143                IJ.log("  iterations = " + fitG.getIterations());
144
145                ColoredStroke finalStroke = new ColoredStroke(StrokeWidth, GeometricFitColor.getColor());
146                ola.addShapes(finalEllipse.getShapes(3), finalStroke);
147        }
148
149        // ------------------------------------------
150        
151        private boolean runDialog() {
152                GenericDialog gd = new GenericDialog(this.getClass().getSimpleName());
153                gd.addHelp(getJavaDocUrl());
154                gd.addMessage(DialogUtils.formatText(50,
155                                "This plugin performs algebraic + geometric ellipse fitting,",
156                                "either to ROI points (if available) or foreground points",
157                                "collected from the pixel image."
158                                ));
159                
160                gd.addCheckbox("Use ROI (float) points", UsePointsFromRoi);
161                gd.addEnumChoice("Algebraic fit method", AlgebraicFitMethod);
162                gd.addEnumChoice("Algebraic ellipse color", AlgebraicFitColor);
163                gd.addEnumChoice("Geometric fit method", GeometricFitMethod);
164                gd.addEnumChoice("Geometric ellipse color", GeometricFitColor);
165                
166                gd.showDialog();
167                if (gd.wasCanceled())
168                        return false;
169                
170                AlgebraicFitMethod = gd.getNextEnumChoice(EllipseFitAlgebraic.FitType.class);
171                AlgebraicFitColor = gd.getNextEnumChoice(BasicAwtColor.class);
172                
173                GeometricFitMethod = gd.getNextEnumChoice(EllipseFitGeometric.FitType.class);
174                GeometricFitColor = gd.getNextEnumChoice(BasicAwtColor.class);
175                
176                return true;
177        }
178
179}