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 ******************************************************************************/ 009 010package More_; 011 012import ij.IJ; 013import ij.ImagePlus; 014import ij.gui.GenericDialog; 015import ij.plugin.filter.PlugInFilter; 016import ij.process.ByteProcessor; 017import ij.process.ImageProcessor; 018import imagingbook.common.color.iterate.CssColorSequencer; 019import imagingbook.common.geometry.basic.Pnt2d; 020import imagingbook.common.geometry.fd.FourierDescriptor; 021import imagingbook.common.geometry.fd.FourierDescriptorUniform; 022import imagingbook.common.ij.DialogUtils; 023import imagingbook.common.ij.overlay.ColoredStroke; 024import imagingbook.common.ij.overlay.ShapeOverlayAdapter; 025import imagingbook.common.math.Complex; 026import imagingbook.common.regions.Contour; 027import imagingbook.common.regions.ContourTracer; 028import imagingbook.common.regions.RegionContourSegmentation; 029import imagingbook.core.jdoc.JavaDocHelp; 030import imagingbook.sampleimages.GeneralSampleImage; 031 032import java.awt.Color; 033import java.awt.Font; 034import java.awt.Shape; 035import java.awt.geom.AffineTransform; 036import java.awt.geom.Path2D; 037import java.util.List; 038import java.util.Locale; 039 040import static imagingbook.common.ij.IjUtils.noCurrentImage; 041 042/** 043 * <p> 044 * This ImageJ plugin visualizes the composition of 2D shapes by superposition of nested ellipses, corresponding to 045 * complex coefficient pairs of elliptic Fourier descriptors. 046 * </p> 047 * <p> 048 * The plugin assumes that the input image is binary (of type {@link ByteProcessor}). It is segmented and the outer 049 * contour of the largest connected component is used to calculate a Fourier descriptor (of type 050 * {@link FourierDescriptorUniform}) with a user-defined number of coefficient pairs. The plugin then displays a 051 * sequence of frames illustrating the reconstruction of the shape by superposition of nested ellipses as the path 052 * parameter (t) runs from 0 to 1. See Sec. 26.3.6 (esp. Fig. 26.12) of [1] for details. 053 * </p> 054 * <p> 055 * [1] W. Burger, M.J. Burge, <em>Digital Image Processing – An Algorithmic Introduction Using Java</em>, 2nd ed, 056 * Springer (2016). 057 * </p> 058 * 059 * @author WB 060 * @version 2022/10/28 061 */ 062public class Fourier_Descriptor_Animation implements PlugInFilter, JavaDocHelp { 063 // TODO: add key event to stop animation 064 private static int FourierCoefficientPairs = 3; 065 066 // visualization-related settings 067 private static int StepsPerFullRevolution = 500; 068 private static int StepsPerSecond = 10; 069 070 private static boolean ShowOriginalContour = true; 071 private static boolean ShowFullReconstruction = true; 072 private static boolean ShowEllipseTree = true; 073 private static boolean ShowPathParameter = true; 074 075 private static int ReconstructionPoints = 100; 076 077 private static Color ContourColor = new Color(0, 60, 255); 078 private static double ContourStrokeWidth = 0.25; 079 private static double ReconstructionMarkerRadius = 3.0; 080 private static Color ReconstructionColor = new Color(0, 185, 15); 081 private static double ReconstructionStrokeWidth = 0.5; 082 083 private static Font PathParameterFont = new Font(Font.MONOSPACED, Font.PLAIN, 10); 084 085 private ImagePlus im; 086 087 // ---------------------------------------------------------------- 088 089 /** 090 * Constructor, asks to open a predefined sample image if no other image is currently open. 091 */ 092 public Fourier_Descriptor_Animation() { 093 if (noCurrentImage()) { 094 DialogUtils.askForSampleImage(GeneralSampleImage.MapleLeafSmall); 095 } 096 } 097 098 // ---------------------------------------------------------------- 099 100 @Override 101 public int setup(String arg, ImagePlus im) { 102 this.im = im; 103 return DOES_8G + NO_CHANGES; 104 } 105 106 @Override 107 public void run(ImageProcessor ip) { 108 109 if (!runDialog()) { 110 return; 111 } 112 113 ByteProcessor bp = (ByteProcessor) ip; 114 Pnt2d[] contr = getLargestRegionContour(bp); 115 FourierDescriptor fd = FourierDescriptorUniform.from(FourierDescriptor.toComplexArray(contr), FourierCoefficientPairs); 116 Complex ctr = fd.getCoefficient(0); 117 118 ImagePlus imA = makeBackgroundImage(); 119 imA.show(); 120 121 ColoredStroke contourStroke = new ColoredStroke(ContourStrokeWidth, ContourColor); 122 ColoredStroke reconstructionStroke = new ColoredStroke(ReconstructionStrokeWidth, ReconstructionColor); 123 124 long frameTimeMs = Math.round(1000.0/StepsPerSecond); 125 boolean done = false; 126 127 while (!done) { 128 for (int k = 0; k < StepsPerFullRevolution; k++) { 129 if (IJ.escapePressed() || imA.getWindow() == null || imA.getWindow().isClosed()) { 130 done = true; break; 131 } 132 133 double t = (double) k / StepsPerFullRevolution; // t in [0,1] 134 long startTime = System.currentTimeMillis(); 135 136 ShapeOverlayAdapter ola = new ShapeOverlayAdapter(); 137 138 if (ShowPathParameter) { 139 ola.addText(5, 0, String.format(Locale.US, "t=%.4f", t), PathParameterFont, Color.black); 140 } 141 142 if (ShowOriginalContour) { 143 ola.addShape(toClosedPath(contr), contourStroke); 144 ola.addShape(Pnt2d.from(ctr.re, ctr.im).getShape()); 145 } 146 147 if (ShowFullReconstruction) { // draw the shape reconstructed from all FD-pairs 148 Path2D rec = FourierDescriptor.toPath(fd.getShapeFull(ReconstructionPoints)); 149 ola.addShape(rec, reconstructionStroke); 150 } 151 152 if (ShowEllipseTree) { 153 Complex cc = ctr; // current ellipse center 154 CssColorSequencer csq = new CssColorSequencer(); 155 csq.setRandomSeed(17); 156 for (int m = 1; m <= FourierCoefficientPairs; m++) { 157 Color ellcol = csq.next(); 158 ColoredStroke ellipseStroke = new ColoredStroke(ReconstructionStrokeWidth/2, ellcol); 159 160 // draw the ellipse for FD pair m: 161 Shape ellipse = fd.getEllipse(m); 162 AffineTransform trans = AffineTransform.getTranslateInstance(cc.re, cc.im); 163 Shape shape = trans.createTransformedShape(ellipse); 164 ola.addShape(shape, ellipseStroke); 165 166 // show marker for current path position for t 167 Complex cNext = cc.add(fd.getShapePointPair(m, t)); 168 ellipseStroke.setFillColor(ellcol); 169 ola.addShape(Pnt2d.from(cNext.toArray()).getShape(ReconstructionMarkerRadius), ellipseStroke); 170 171 // make current point the center of the next ellipse 172 cc = cc.add(cNext); 173 cc = cNext; 174 } 175 } 176 177 imA.setOverlay(ola.getOverlay()); 178 imA.updateAndDraw(); 179 180 // check and wait if needed: 181 long elapsedTime = System.currentTimeMillis() - startTime; 182 int remainingTime = (int) (frameTimeMs - elapsedTime); 183 if (remainingTime > 0) { 184 IJ.wait(remainingTime); 185 } 186 } 187 } 188 } 189 190 private void brighten(ByteProcessor ip, int minGray) { 191 float scale = (255 - minGray) / 255f; 192 int[] table = new int[256]; 193 for (int i=0; i<256; i++) { 194 table[i] = Math.round(minGray + scale * i); 195 196 } 197 ip.applyTable(table); 198 } 199 200 // ------------------------------------------------------------- 201 202 private Pnt2d[] getLargestRegionContour(ByteProcessor ip) { 203 // label regions and trace contours 204 ContourTracer tracer = new RegionContourSegmentation(ip); 205 List<? extends Contour> outerContours = tracer.getOuterContours(true); 206 207 if (outerContours.isEmpty()) { 208 IJ.error("no regions found"); 209 return null; 210 } 211 212 return outerContours.get(0).getPointArray(); // contour of largest region 213 } 214 215 private ImagePlus makeBackgroundImage() { 216 ByteProcessor bp = im.getProcessor().convertToByteProcessor(); 217 String title = String.format("%s-partial-%03d", im.getShortTitle(), FourierCoefficientPairs); 218 219 if (bp.isInvertedLut()) { 220 bp.invert(); 221 bp.invertLut(); 222 } 223 brighten(bp, 220); 224 return new ImagePlus(title, bp); 225 } 226 227 private Path2D toClosedPath(Pnt2d[] pnts) { 228 Path2D path = new Path2D.Float(); 229 path.moveTo(pnts[0].getX(), pnts[0].getY()); 230 for (int i = 1; i < pnts.length; i++) { 231 path.lineTo(pnts[i].getX(), pnts[i].getY()); 232 } 233 path.closePath(); 234 return path; 235 } 236 237 // ------------------------------------------------------------- 238 239 private boolean runDialog() { 240 GenericDialog gd = new GenericDialog(this.getClass().getSimpleName()); 241 gd.addHelp(getJavaDocUrl()); 242 gd.addMessage("Fourier decomposition parameters:"); 243 gd.addNumericField("Number of FD pairs (min. 1)", FourierCoefficientPairs, 0); 244 gd.addNumericField("Reconstruction Points", ReconstructionPoints, 0); 245 246 gd.addMessage("Animation parameters:"); 247 gd.addNumericField("Steps per full revolution", StepsPerFullRevolution, 0); 248 gd.addNumericField("Steps per second", StepsPerSecond, 0); 249 250 gd.addCheckbox("Show Original Contour", ShowOriginalContour); 251 gd.addCheckbox("Show Full Reconstruction", ShowFullReconstruction); 252 gd.addCheckbox("Show Ellipse Tree", ShowEllipseTree); 253 gd.addCheckbox("Show Path Parameter (t)", ShowPathParameter); 254 255 gd.showDialog(); 256 if(gd.wasCanceled()) 257 return false; 258 259 FourierCoefficientPairs = Math.max(1, (int) gd.getNextNumber()); 260 ReconstructionPoints = (int) gd.getNextNumber(); 261 262 StepsPerFullRevolution = (int) gd.getNextNumber(); 263 StepsPerSecond = (int) gd.getNextNumber(); 264 265 ShowOriginalContour = gd.getNextBoolean(); 266 ShowFullReconstruction = gd.getNextBoolean(); 267 ShowEllipseTree = gd.getNextBoolean(); 268 ShowPathParameter = gd.getNextBoolean(); 269 270 gd.addMessage("Press ESC or close window to stop animation."); 271 return true; 272 } 273 274}