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.circle.GeometricCircle;
021import imagingbook.common.geometry.fitting.circle.algebraic.CircleFitAlgebraic;
022import imagingbook.common.geometry.fitting.circle.geometric.CircleFitGeometric;
023import imagingbook.common.ij.DialogUtils;
024import imagingbook.common.ij.IjUtils;
025import imagingbook.common.ij.RoiUtils;
026import imagingbook.common.ij.overlay.ColoredStroke;
027import imagingbook.common.ij.overlay.ShapeOverlayAdapter;
028import imagingbook.core.jdoc.JavaDocHelp;
029
030import java.util.Locale;
031
032import static imagingbook.common.geometry.fitting.circle.algebraic.CircleFitAlgebraic.FitType.Pratt;
033import static imagingbook.common.geometry.fitting.circle.geometric.CircleFitGeometric.FitType.DistanceBased;
034
035
036/**
037 * <p>
038 * ImageJ plugin, performs algebraic circle fitting on the current ROI to find an initial circle, followed by geometric
039 * fitting. Algebraic and geometric fitting methods can be selected (see Sec. 11.1 of [1] for details). If successful,
040 * the resulting ellipses are displayed as a vector overlay (color can be chosen). Sample points are either collected
041 * from the ROI (if available) or collected as foreground pixels (values &gt; 0) from the image. If no image is
042 * currently open, the user is asked to create a suitable sample image.
043 * </p>
044 * <p>
045 * [1] W. Burger, M.J. Burge, <em>Digital Image Processing &ndash; An Algorithmic Introduction</em>, 3rd ed, Springer
046 * (2022).
047 * </p>
048 *
049 * @author WB
050 * @version 2022/10/03
051 */
052public class Circle_Fitting implements PlugInFilter, JavaDocHelp {
053        
054        static CircleFitAlgebraic.FitType AlgebraicFitMethod = Pratt;
055        static CircleFitGeometric.FitType GeometricFitMethod = DistanceBased;
056        static boolean UsePointsFromRoi = false;
057        
058        private static BasicAwtColor AlgebraicFitColor = BasicAwtColor.Red;
059        private static BasicAwtColor GeometricFitColor = BasicAwtColor.Blue;
060        private static double StrokeWidth = 0.5;
061        
062        private ImagePlus im;
063        
064        /**
065         * Constructor, asks to open a predefined sample image if no other image
066         * is currently open.
067         */
068        public Circle_Fitting() {
069                if (IjUtils.noCurrentImage()) {
070                        if (DialogUtils.askForSampleImage()) {
071                                IjUtils.run(new Circle_Make_Random()); //runPlugIn(Circle_Make_Random.class);
072                        }                       
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 < 3) {
098                        IJ.error("At least 3 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                // ------------------------------------------------------------------------
110                CircleFitAlgebraic fitA = CircleFitAlgebraic.getFit(AlgebraicFitMethod, points);
111                // ------------------------------------------------------------------------
112                
113                GeometricCircle ac = fitA.getGeometricCircle();
114                if (ac == null) {
115                        IJ.log("Algebraic fit: no result!");
116                        return;
117                }
118                
119                GeometricCircle initCircle = ac;
120                
121                IJ.log("Initial fit (algebraic):");
122                IJ.log("  circle: " + initCircle.toString());
123                IJ.log(String.format(Locale.US, "  error = %.3f", initCircle.getMeanSquareError(points)));
124                
125                ColoredStroke initialStroke = new ColoredStroke(StrokeWidth, AlgebraicFitColor.getColor());
126                ola.addShapes(initCircle.getShapes(3), initialStroke);
127
128                // ------------------------------------------------------------------------
129                CircleFitGeometric fitG = CircleFitGeometric.getFit(GeometricFitMethod, points, initCircle);
130                // ------------------------------------------------------------------------
131                
132                GeometricCircle finalCircle = fitG.getCircle();
133                if (finalCircle == null) {
134                        IJ.log("Geometric fit: no result!");
135                        return;
136                }
137                
138                IJ.log("Final fit (geometric):");
139                IJ.log("  circle: " + finalCircle.toString());
140                IJ.log(String.format(Locale.US, "  error = %.3f", finalCircle.getMeanSquareError(points)));
141                IJ.log("  iterations = " + fitG.getIterations());
142
143                ColoredStroke finalStroke = new ColoredStroke(StrokeWidth, GeometricFitColor.getColor());
144                ola.addShapes(finalCircle.getShapes(3), finalStroke);
145        }
146
147        // ------------------------------------------
148        
149        private boolean runDialog() {
150                GenericDialog gd = new GenericDialog(this.getClass().getSimpleName());
151                gd.addHelp(getJavaDocUrl());
152                gd.addMessage(DialogUtils.formatText(50,
153                                "This plugin performs algebraic + geometric circle fitting,",
154                                "either to ROI points (if available) or foreground points",
155                                "collected from the pixel image."
156                                ));
157                
158                gd.addCheckbox("Use ROI (float) points", UsePointsFromRoi);
159                gd.addEnumChoice("Algebraic fit method", AlgebraicFitMethod);
160                gd.addEnumChoice("Algebraic ellipse color", AlgebraicFitColor);
161                gd.addEnumChoice("Geometric fit method", GeometricFitMethod);
162                gd.addEnumChoice("Geometric ellipse color", GeometricFitColor);
163                
164                gd.showDialog();
165                if (gd.wasCanceled())
166                        return false;
167                
168                UsePointsFromRoi = gd.getNextBoolean();
169                AlgebraicFitMethod = gd.getNextEnumChoice(CircleFitAlgebraic.FitType.class);
170                AlgebraicFitColor = gd.getNextEnumChoice(BasicAwtColor.class);
171                GeometricFitMethod = gd.getNextEnumChoice(CircleFitGeometric.FitType.class);
172                GeometricFitColor = gd.getNextEnumChoice(BasicAwtColor.class);
173                
174                return true;
175        }
176
177}