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