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 Ch12_Ransac_Hough;
010
011import Ch12_Ransac_Hough.settings.RansacDrawSettings;
012import ij.IJ;
013import ij.ImagePlus;
014import ij.gui.GenericDialog;
015import ij.plugin.ImagesToStack;
016import ij.plugin.filter.PlugInFilter;
017import ij.process.ByteProcessor;
018import ij.process.ImageProcessor;
019import imagingbook.common.geometry.basic.Pnt2d;
020import imagingbook.common.geometry.ellipse.GeometricEllipse;
021import imagingbook.common.ij.DialogUtils;
022import imagingbook.common.ij.IjUtils;
023import imagingbook.common.ij.overlay.ColoredStroke;
024import imagingbook.common.ij.overlay.ShapeOverlayAdapter;
025import imagingbook.common.ransac.RansacCircleDetector;
026import imagingbook.common.ransac.RansacEllipseDetector;
027import imagingbook.common.ransac.RansacResult;
028import imagingbook.core.jdoc.JavaDocHelp;
029import imagingbook.sampleimages.GeneralSampleImage;
030
031import java.util.ArrayList;
032import java.util.List;
033
034import static imagingbook.common.ij.DialogUtils.addToDialog;
035import static imagingbook.common.ij.DialogUtils.getFromDialog;
036import static imagingbook.common.ij.IjUtils.noCurrentImage;
037
038/**
039 * <p>
040 * RANSAC ellipse detection implemented with imagingbook library class {@link RansacCircleDetector} (see Sec. 12.1.5 of
041 * [1] for details). If no image is currently open, the plugin optionally loads a suitable sample image.
042 * </p>
043 * <p>
044 * [1] W. Burger, M.J. Burge, <em>Digital Image Processing &ndash; An Algorithmic Introduction</em>, 3rd ed, Springer
045 * (2022).
046 * </p>
047 *
048 * @author WB
049 * @version 2022/10/03
050 */
051public class Ransac_Ellipse_Detect implements PlugInFilter, RansacDrawSettings, JavaDocHelp {
052
053        private static RansacEllipseDetector.Parameters params = new RansacEllipseDetector.Parameters();
054        static {
055                params.randomSeed = 17;
056        }
057        
058        private static int MaxEllipseCount = 3;
059        
060        private int W, H;
061        private ImagePlus im;
062        private String title;
063
064        
065        /**
066         * Constructor, asks to open a predefined sample image if no other image
067         * is currently open.
068         */
069        public Ransac_Ellipse_Detect() {
070                if (noCurrentImage()) {
071                        DialogUtils.askForSampleImage(GeneralSampleImage.NoisyEllipses);
072                }
073        }
074        
075        // -------------------------------------------------------------------------
076
077        
078        @Override
079        public int setup(String arg, ImagePlus im) {
080                this.im = im;
081                return DOES_8G + NO_CHANGES;
082        }
083
084        @Override
085        public void run(ImageProcessor ip) {
086                
087                title = "Ellipses from " + im.getTitle();
088                W = ip.getWidth();
089                H = ip.getHeight();     
090                
091                if (!runDialog()) {
092                        return;
093                }
094        
095                Pnt2d[] points = IjUtils.collectNonzeroPoints(ip);
096                List<RansacResult<GeometricEllipse>> ellipses = new ArrayList<>();
097
098                // ---------------------------------------------------------------------
099                RansacEllipseDetector detector = new RansacEllipseDetector(params);
100                // ---------------------------------------------------------------------
101                
102                List<ImagePlus> resultImages = new ArrayList<>();
103                int cnt = 0;
104
105                RansacResult<GeometricEllipse> sol = detector.detectNext(points);
106                while (sol != null && cnt < MaxEllipseCount) {
107                        ellipses.add(sol);
108                        cnt = cnt + 1;
109                        
110                        ImagePlus imSnap = new ImagePlus("circle-"+cnt, showPointSet(points));
111                        ShapeOverlayAdapter ola = new ShapeOverlayAdapter();
112
113                        {       // draw inliers (points)
114                                ColoredStroke stroke = new ColoredStroke(LineStrokeWidth, InlierColor, 0);
115                                stroke.setFillColor(InlierColor);
116                                for (Pnt2d p : sol.getInliers()) {
117                                        ola.addShape(p.getShape(InlierRadius), stroke);
118                                }
119                        }
120        
121                        {       // draw initial circle
122                                GeometricEllipse ellipse = sol.getPrimitiveInit();
123                                ColoredStroke stroke = new ColoredStroke(LineStrokeWidth, InitialFitColor, 0);
124                                ola.addShape(ellipse.getShape(), stroke);
125                        }
126        
127                        {       // draw final circle
128                                GeometricEllipse ellipse = sol.getPrimitiveFinal();
129                                ColoredStroke stroke = new ColoredStroke(LineStrokeWidth, FinalFitColor, 0);
130                                ola.addShape(ellipse.getShape(), stroke);
131                        }
132        
133                        {       // draw the 5 random points used
134                                ColoredStroke stroke = new ColoredStroke(LineStrokeWidth, RandomDrawDotColor, 0);
135                                stroke.setFillColor(RandomDrawDotColor);
136                                for (Pnt2d p : sol.getDraw()) {
137                                        ola.addShape(p.getShape(RandoDrawDotRadius), stroke);
138                                }
139                        }
140
141                        imSnap.setOverlay(ola.getOverlay());
142                        resultImages.add(imSnap);
143                        sol = detector.detectNext(points);
144                }
145
146                // combine all result images to a stack:
147                if (resultImages.isEmpty()) {
148                        IJ.error("No items detected!");
149                }
150                else {
151                        ImagePlus stack = ImagesToStack.run(resultImages.toArray(new ImagePlus[0]));
152                        stack.setTitle(title);
153                        stack.show();
154                }
155        }
156
157        // ------------------------------------------------------
158        
159        private ByteProcessor showPointSet(Pnt2d[] points) {
160                ByteProcessor bp = new ByteProcessor(W, H);
161                IjUtils.drawPoints(bp, points, 255);
162                bp.invertLut();
163                return bp;
164        }
165        
166        private boolean runDialog() {
167                GenericDialog gd = new GenericDialog(this.getClass().getSimpleName());
168                gd.addHelp(getJavaDocUrl());
169                addToDialog(params, gd);
170                gd.addNumericField("Max. number of ellipses", MaxEllipseCount);
171                
172                gd.addStringField("Output title", title, 16);
173                
174                gd.showDialog();
175                if (gd.wasCanceled())
176                        return false;
177                
178                getFromDialog(params, gd);
179                MaxEllipseCount = (int) gd.getNextNumber();
180                title = gd.getNextString();
181                
182                return params.validate();
183        }
184}