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 > 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 – 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}