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 Ch08_Binary_Regions;
010
011import ij.IJ;
012import ij.ImagePlus;
013import ij.gui.GenericDialog;
014import ij.plugin.filter.PlugInFilter;
015import ij.process.ByteProcessor;
016import ij.process.ImageProcessor;
017import imagingbook.common.geometry.basic.NeighborhoodType2D;
018import imagingbook.common.geometry.basic.Pnt2d;
019import imagingbook.common.geometry.ellipse.GeometricEllipse;
020import imagingbook.common.ij.DialogUtils;
021import imagingbook.common.ij.IjUtils;
022import imagingbook.common.ij.overlay.ColoredStroke;
023import imagingbook.common.ij.overlay.ShapeOverlayAdapter;
024import imagingbook.common.regions.BinaryRegion;
025import imagingbook.common.regions.RegionContourSegmentation;
026import imagingbook.core.plugin.IjPluginName;
027import imagingbook.core.jdoc.JavaDocHelp;
028import imagingbook.sampleimages.GeneralSampleImage;
029
030import java.awt.Color;
031import java.awt.geom.Line2D;
032import java.util.List;
033
034import static imagingbook.common.ij.IjUtils.noCurrentImage;
035import static imagingbook.common.math.Arithmetic.sqr;
036import static java.lang.Math.sqrt;
037
038/**
039 * <p>
040 * Performs binary region segmentation, then displays each region's major axis (scaled by eccentricity) and equivalent
041 * ellipse as a vector overlay. See Sec. 8.6.2 and 8.6.3 of [1] for additional details. This plugin expects a binary
042 * (black and white) image with background = 0 and foreground &gt; 0. Display lookup tables (LUTs) are not considered.
043 * Eccentricity values are limited to {@link #MaxEccentricity}, axes are marked red if exceeded. Axes for regions with
044 * {@code NaN} eccentricity value (single-pixel regions) are not displayed. Axis and ellipse parameters are calculated
045 * from the region's central moments. If no image is currently open, the plugin optionally loads a suitable sample
046 * image.
047 * </p>
048 * <p>
049 * [1] W. Burger, M.J. Burge, <em>Digital Image Processing &ndash; An Algorithmic Introduction</em>, 3rd ed, Springer
050 * (2022).
051 * </p>
052 *
053 * @author WB
054 * @version 2022/12/08
055 */
056@IjPluginName("Region Eccentricity/Ellipse Demo")
057public class Region_Eccentricity_Ellipse_Demo implements PlugInFilter, JavaDocHelp {
058        
059        /** Neighborhood type used for region segmentation (4- or 8-neighborhood). */
060        public static NeighborhoodType2D Neighborhood = NeighborhoodType2D.N4;
061        
062        /** Eccentricity scale factor applied to the length of the region's major axis. */
063        public static double    AxisEccentricityScale = 1.0;
064        /** Minimum region size, smaller regions are ignored. */
065        public static int               MinRegionSize = 10;
066        /** Maximum eccentricity, greater eccentricity values are clipped. */
067        public static double    MaxEccentricity = 100;
068        
069        /** Color used for drawing the major axis. */
070        public static Color     AxisColor = Color.magenta;
071        /** Color used for drawing the major axis if maximum eccentricity exceeded. */
072        public static Color     AxisColorCLipped = Color.red;
073        /** Color used for drawing the region's center. */
074        public static Color             CenterColor = Color.orange;
075        /** Color used for drawing the region's equivalent ellipse. */
076        public static Color             EllipseColor = Color.green;
077        /** Line width used for drawing the region's axes. */
078        public static double    AxisLineWidth = 1.5;
079        /** Size (radius) of the region's center mark. */
080        public static double    CenterMarkSize = 3;
081        /** Line width used for drawing the region's center. */
082        public static double    CenterLineWidth = 0.75;
083        
084        /** Set true to show the regions's centroid. */
085        public static boolean   ShowCenterMark = true;
086        /** Set true to show the region's major axis (length scaled by eccentricity). */
087        public static boolean   ShowMajorAxis = true;
088        /** Set true to show the region's equivalent ellipse. */
089        public static boolean   ShowEllipse = true;
090        
091        private ImagePlus im = null;
092        
093        /**
094         * Constructor, asks to open a predefined sample image if no other image is currently open.
095         */
096        public Region_Eccentricity_Ellipse_Demo() {
097                if (noCurrentImage()) {
098                        DialogUtils.askForSampleImage(GeneralSampleImage.ToolsSmall);
099                }
100        }
101
102        @Override
103        public int setup(String arg, ImagePlus im) {
104                this.im = im;
105                return DOES_8G;
106        }
107
108        @Override
109        public void run(ImageProcessor ip) {
110                if (!IjUtils.isBinary(ip)) {
111                        IJ.showMessage("Plugin requires a binary image!");
112                        return;
113                }
114                
115                if (!runDialog()) {
116                        return;
117                }
118                
119                int w = ip.getWidth();
120                int h = ip.getHeight();
121                double unitLength = sqrt(w * h) * 0.005 * AxisEccentricityScale;                        
122
123                ShapeOverlayAdapter ola = new ShapeOverlayAdapter();
124                
125                // perform region segmentation:
126                RegionContourSegmentation segmenter = new RegionContourSegmentation((ByteProcessor) ip, Neighborhood);
127                List<BinaryRegion> regions = segmenter.getRegions();
128
129                for (BinaryRegion r : regions) {
130                        int n = r.getSize();
131                        if (n < MinRegionSize) {
132                                continue;
133                        }
134                        
135                        Pnt2d ctr = r.getCenter();
136                        
137                        if (ShowCenterMark) {
138                                ola.setStroke(new ColoredStroke(CenterLineWidth, CenterColor));
139                                // ola.addShape(makeCenterMark(xc, yc));
140                                ola.addShape(ctr.getShape(CenterMarkSize));
141                        }
142                        
143                        double[] mu = r.getCentralMoments();    // = (mu10, mu01, mu20, mu02, mu11)
144                        double mu20 = mu[0];
145                        double mu02 = mu[1];
146                        double mu11 = mu[2];
147                        
148                        double theta = 0.5 * Math.atan2(2 * mu11, mu20 - mu02); // axis angle   
149                                
150                        // calculate eccentricity from 2nd-order region moments:
151                        double A = mu20 + mu02;
152                        double B = sqr(mu20 - mu02) + 4 * sqr(mu11);
153                        if (B < 0) {
154                                throw new RuntimeException("negative value in eccentricity calculation: B = " + B); // this should never happen
155                        }               
156                        double a1 = A + sqrt(B);                // see book 2nd ed, eq. 10.34
157                        double a2 = A - sqrt(B);
158                        double ecc = a1 / a2;           
159                        
160                        if (ShowMajorAxis && !Double.isNaN(ecc)) {
161                                Color axisCol = AxisColor;                      // default color
162                                if (ecc > MaxEccentricity) {            // limit eccentricity (may be infinite)
163                                        ecc = MaxEccentricity;
164                                        axisCol = AxisColorCLipped;             // mark as beyond maximum
165                                }
166                                double len = ecc * unitLength;
167                                double dx = Math.cos(theta) * len;
168                                double dy = Math.sin(theta) * len;;
169                                double xc = ctr.getX();
170                                double yc = ctr.getY();
171                                ola.setStroke(new ColoredStroke(AxisLineWidth, axisCol));
172                                ola.addShape(new Line2D.Double(xc, yc, xc + dx, yc + dy));
173                        }
174                        
175                        if (ShowEllipse) {
176                                GeometricEllipse ellipse = r.getEquivalentEllipse();
177                                if (ellipse != null) {
178                                        ola.setStroke(new ColoredStroke(AxisLineWidth, EllipseColor));
179                                        ola.addShape(ellipse.getShape());
180                                }
181                        }       
182                }
183                
184                im.setOverlay(ola.getOverlay());
185        }
186        
187        // -----------------------------------------------------------------
188        
189        private boolean runDialog() {
190                GenericDialog gd = new GenericDialog(this.getClass().getSimpleName());
191                gd.addHelp(getJavaDocUrl());
192                gd.addEnumChoice("Neighborhood type", Neighborhood);
193                gd.addNumericField("Min. region size (pixels)", MinRegionSize, 0);
194                gd.addNumericField("Max. eccentricity", MaxEccentricity, 0);
195                gd.addNumericField("Axis scale", AxisEccentricityScale, 1);
196                gd.addCheckbox("Show center marks", ShowCenterMark);
197                gd.addCheckbox("Show major axes", ShowMajorAxis);
198                gd.addCheckbox("Show ellipses", ShowEllipse);
199                                
200                gd.showDialog();
201                if (gd.wasCanceled())
202                        return false;
203
204                Neighborhood = gd.getNextEnumChoice(NeighborhoodType2D.class);
205                MinRegionSize = (int) gd.getNextNumber();
206                MaxEccentricity = gd.getNextNumber();
207                AxisEccentricityScale = gd.getNextNumber();
208                
209                ShowCenterMark = gd.getNextBoolean();
210                ShowMajorAxis = gd.getNextBoolean();
211                ShowEllipse = gd.getNextBoolean();
212                return true;
213        }
214
215}