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 Ch26_MSER; 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.ByteProcessor; 018import ij.process.ColorProcessor; 019import ij.process.ImageProcessor; 020import imagingbook.common.geometry.basic.Pnt2d; 021import imagingbook.common.geometry.ellipse.GeometricEllipse; 022import imagingbook.common.ij.DialogUtils; 023import imagingbook.common.ij.GuiTools; 024import imagingbook.common.ij.overlay.ColoredStroke; 025import imagingbook.common.ij.overlay.ShapeOverlayAdapter; 026import imagingbook.common.mser.MserColors; 027import imagingbook.common.mser.MserData; 028import imagingbook.common.mser.MserDetector; 029import imagingbook.common.mser.MserParameters; 030import imagingbook.common.mser.components.Component; 031import imagingbook.common.mser.components.PixelMap.Pixel; 032import imagingbook.core.jdoc.JavaDocHelp; 033import imagingbook.core.resource.ImageResource; 034import imagingbook.sampleimages.GeneralSampleImage; 035 036import java.awt.Color; 037import java.awt.Font; 038import java.util.List; 039 040import static imagingbook.common.ij.DialogUtils.addToDialog; 041import static imagingbook.common.ij.DialogUtils.getFromDialog; 042import static imagingbook.common.ij.IjUtils.noCurrentImage; 043 044/** 045 * <p> 046 * ImageJ plugin which runs MSER detection [1] on the current image and shows the result as a vector overlay in a new 047 * image. If the option {@code MarkMserPixels} is selected, the output is a color image with pixels belonging to MSER 048 * components marked in different colors. The input image is always converted to grayscale before MSER detection is 049 * performed. See Ch. 26 of [2] for details. If no image is currently open, the user is asked to load a predefined 050 * sample image. 051 * </p> 052 * <p> 053 * [1] J. Matas, O. Chum, M. Urban, and T. Pajdla. Robust widebaseline stereo from maximally stable extremal regions. 054 * Image and Vision Computing 22(10), 761–767 (2004). <br> [2] W. Burger, M.J. Burge, <em>Digital Image Processing 055 * – An Algorithmic Introduction</em>, 3rd ed, Springer (2022). 056 * </p> 057 * 058 * @author WB 059 * @version 2022/11/24 060 * @see MserDetector 061 */ 062public class MSER_Detection_Demo implements PlugInFilter, JavaDocHelp { 063 064 private static ImageResource SampleImage = GeneralSampleImage.MortarSmall; 065 066 private static boolean ShowMserCount = true; 067 private static boolean ShowEllipses = true; 068 private static boolean MarkMserPixels = false; 069 private static boolean ShowColorPalette = false; 070 private static boolean ShowMserLabels = false; 071 private static boolean ShowElapsedTime = false; 072 073 // processing direction 074 private static boolean BlackToWhite = true; // detect on original image (default) 075 private static boolean WhiteToBlack = false; // detect on inverted image 076 077 private static boolean UseTwoColorsOnly = false; // detect on inverted image 078 private static Color BlackToWhiteColor = MserColors.Yellow.getColor(); 079 private static Color WhiteToBlackColor = MserColors.Cyan.getColor(); 080 private static int MinDisplayWidth = 300; 081 082 private static MserParameters params = new MserParameters(); // MSER parameters 083 084 private ImagePlus im = null; 085 private Color[] colors = null; 086 private double ellipseStrokeWidth = 0; // set dynamically 087 private int labelFontSize = 0; // set dynamically 088 private Font labelFont = null; // set dynamically 089 private Roi roi = null; 090 private ByteProcessor bp = null; 091 private ImageProcessor ip2 = null; 092 private Overlay oly = null; 093 094 /** 095 * Constructor, asks to open a predefined sample image if no other image is currently open. 096 */ 097 public MSER_Detection_Demo() { 098 if (noCurrentImage()) { 099 DialogUtils.askForSampleImage(SampleImage); 100 } 101 } 102 103 @Override 104 public int setup(String arg0, ImagePlus im) { 105 this.im = im; 106 return DOES_8G + NO_CHANGES; 107 } 108 109 @Override 110 public void run(ImageProcessor ip) { 111 roi = im.getRoi(); 112 ellipseStrokeWidth = Math.max(0.25, ip.getWidth() * 0.1 / 100); 113 labelFontSize = Math.max(3, (int) Math.round(ip.getWidth() / 100.0)); 114 115 if (!runDialog(params)) { 116 return; 117 } 118 119 bp = ip.convertToByteProcessor(); 120 ip2 = (MarkMserPixels) ? ip.convertToColorProcessor() : ip.convertToByteProcessor(); 121 oly = new Overlay(); 122 String title = im.getShortTitle() + "-MSER"; 123 124 if (BlackToWhite || WhiteToBlack) title = title + "-"; 125 if (BlackToWhite) title = title + "B"; 126 if (WhiteToBlack) title = title + "W"; 127 if (UseTwoColorsOnly) title = title + "-2c"; 128 129 Color[] palette = MserColors.LevelColors; 130 labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, labelFontSize); 131 132 List<Component<MserData>> msersB = null; 133 List<Component<MserData>> msersW = null; 134// List<Component<MserData>> msersAll = new ArrayList<>(); 135 136 double elapsedTime = 0; 137 138 if (BlackToWhite) { 139 MserDetector detector = new MserDetector(bp, params); 140 msersB = detector.getMserFeatures(); 141 if (ShowMserCount) { 142 IJ.log("Found MSERs (BlackToWhite): " + msersB.size()); 143 } 144 if (UseTwoColorsOnly) 145 makeColorsBlackToWhite(); 146 else 147 makeColors(palette); 148 drawToOverlay(msersB); 149 elapsedTime += detector.getElapsedTime(); 150 } 151 152 if (WhiteToBlack) { 153 bp.invert(); 154 MserDetector detector = new MserDetector(bp, params); 155 msersW = detector.getMserFeatures(); 156 if (ShowMserCount) { 157 IJ.log("Found MSERs (WhiteToBlack): " + msersW.size()); 158 } 159 if (UseTwoColorsOnly) 160 makeColorsWhiteToBlack(); 161 else 162 makeColors(palette); 163 drawToOverlay(msersW); 164 elapsedTime += detector.getElapsedTime(); 165 } 166 167 if (ShowMserCount && BlackToWhite && WhiteToBlack) { 168 IJ.log("Found MSERs total: " + (msersB.size() + msersW.size())); 169 } 170 171 if (ShowElapsedTime) { 172 IJ.log(String.format("Algorithm %s: time elapsed %.0fms", params.method, elapsedTime)); 173 } 174 175 if (ShowColorPalette) { 176 if (UseTwoColorsOnly) 177 showTwoColors(); 178 else 179 showColorPalette(title + "-palette"); 180 } 181 182 // ---------------------------------------------------------------------- 183 184// Component.sortBySize(msersAll); // optional 185// drawToOverlay(msersAll); 186 187 ImagePlus cimp = new ImagePlus(title, ip2); 188 setMserImageProps(cimp, params); 189 cimp.setOverlay(oly); 190 cimp.show(); 191 192 // zoom result image if too small 193 if (cimp.getWidth() < MinDisplayWidth) { 194 int zoomFac = (int) Math.ceil((double) MinDisplayWidth / cimp.getWidth()); 195 GuiTools.zoomExact(cimp, zoomFac); 196 } 197 } 198 199 private void drawToOverlay(List<Component<MserData>> msers) { 200 ShapeOverlayAdapter ola = new ShapeOverlayAdapter(oly); 201 ola.setFont(labelFont); 202 203 for (Component<MserData> c : msers) { 204 205 // ignore MSERs outside the current ROI (if specified) 206 Pnt2d ctr = c.getProperties().getCenter(); 207 if (roi != null && !roi.containsPoint(ctr.getX(), ctr.getY())) { 208 continue; 209 } 210 211 Color vecCol = getColor(c.getLevel()); 212 213 // draw contained points 214 if (MarkMserPixels) { 215 float[] hsb = Color.RGBtoHSB(vecCol.getRed(), vecCol.getGreen(), vecCol.getBlue(), null); 216 Color pixCol = Color.getHSBColor(hsb[0], 0.5f, 0.5f); 217 ip2.setColor(pixCol); 218 for (Pixel pnt : c.getAllPixels()) { 219 ip2.drawDot(pnt.x, pnt.y); 220 } 221 } 222 223 if (ShowEllipses) { 224 // suggests 0.2% of image width as stroke width (but at least 0.5) 225 GeometricEllipse ellipse = c.getProperties().getEllipse(); 226 ola.addShape(ellipse.getShape(), new ColoredStroke(ellipseStrokeWidth, vecCol)); 227 228 if (ShowMserLabels) { 229 ola.setTextColor(vecCol); 230 ola.addText(ellipse.xc, ellipse.yc, Integer.toString(c.ID)); 231 } 232 } 233 } 234 } 235 236 // -------------------------------------------- 237 238 private boolean runDialog(MserParameters params) { 239 GenericDialog gd = new GenericDialog(this.getClass().getSimpleName()); 240 gd.addHelp(getJavaDocUrl()); 241 addToDialog(params, gd); 242 gd.addCheckbox("BLACK -> WHITE", BlackToWhite); 243 gd.addCheckbox("WHITE -> BLACK", WhiteToBlack); 244 gd.addCheckbox("Use 2 colors only", UseTwoColorsOnly); 245 246 gd.addMessage("Output parameters:"); 247 gd.addCheckbox("Show MSER count", ShowMserCount); 248 gd.addCheckbox("Show elapsed time", ShowElapsedTime); 249 gd.addCheckbox("Show ellipses", ShowEllipses); 250 gd.addNumericField("Ellipse stroke width", ellipseStrokeWidth, 2); 251 gd.addCheckbox("Show MSER labels", ShowMserLabels); 252 gd.addNumericField("Label font size", labelFontSize, 0); 253 gd.addCheckbox("Mark MSER pixels", MarkMserPixels); 254 gd.addCheckbox("Show color palette", ShowColorPalette); 255 256 gd.showDialog(); 257 if (gd.wasCanceled()) 258 return false; 259 260 getFromDialog(params, gd); 261 262 BlackToWhite = gd.getNextBoolean(); 263 WhiteToBlack = gd.getNextBoolean(); 264 UseTwoColorsOnly = gd.getNextBoolean(); 265 // Output: 266 ShowMserCount = gd.getNextBoolean(); 267 ShowElapsedTime = gd.getNextBoolean(); 268 ShowEllipses = gd.getNextBoolean(); 269 ellipseStrokeWidth = gd.getNextNumber(); 270 ShowMserLabels = gd.getNextBoolean(); 271 labelFontSize = (int) gd.getNextNumber(); 272 MarkMserPixels = gd.getNextBoolean(); 273 ShowColorPalette = gd.getNextBoolean(); 274 275// // Debugging: 276// params.validateComponentTree = gd.getNextBoolean(); 277 278 return true; 279 } 280 281 // -------------------------------------------- 282 283 private void makeColors(Color[] palette) { 284 final int n = palette.length; 285 colors = new Color[256]; 286 for (int level = 0; level < 256; level++) { 287 int k = (int) (n * level / 256.0); // k in [0,n-1] 288 colors[level] = palette[k]; 289 } 290 } 291 292 private void makeColorsBlackToWhite() { 293 colors = new Color[256]; 294 for (int level = 0; level < 256; level++) { 295 colors[level] = BlackToWhiteColor; 296 } 297 } 298 299 private void makeColorsWhiteToBlack() { 300 colors = new Color[256]; 301 for (int level = 0; level < 256; level++) { 302 colors[level] = WhiteToBlackColor; 303 } 304 } 305 306 private Color getColor(int level) { 307 // level is in 0,..,255 308 if (level < 0) 309 level = 0; 310 if (level >= colors.length) 311 level = colors.length - 1; 312 return colors[level]; 313 } 314 315 private void showColorPalette(String title) { 316 int W = 20; 317 int H = 256; 318 ColorProcessor cp = new ColorProcessor(W, H); 319 for (int level = 0; level < H; level++) { 320 cp.setColor(getColor(level)); 321 for (int i = 0; i < W; i++) { 322 cp.drawPixel(i, level); 323 } 324 } 325 cp.flipVertical(); 326 new ImagePlus(title, cp).show(); 327 } 328 329 private void showTwoColors() { 330 { 331 ColorProcessor cp = new ColorProcessor(20, 64); 332 cp.setColor(BlackToWhiteColor); 333 cp.fill(); 334 new ImagePlus("color-BW", cp).show(); 335 } 336 { 337 ColorProcessor cp = new ColorProcessor(20, 64); 338 cp.setColor(WhiteToBlackColor); 339 cp.fill(); 340 new ImagePlus("color-WB", cp).show(); 341 } 342 } 343 344 // -------------------------------------------- 345 346 private void setMserImageProps(ImagePlus imp, MserParameters params) { 347 imp.setProp("MSER-delta", params.delta); 348 imp.setProp("MSER-maxVariation", params.maxSizeVariation); 349 imp.setProp("MSER-minDiversity", params.minDiversity); 350 imp.setProp("MSER-maxRelArea", params.maxRelCompSize); 351 imp.setProp("MSER-minRelArea", params.minRelCompSize); 352 imp.setProp("MSER-minAbsArea", params.minAbsComponentArea); 353 imp.setProp("MSER-method", params.method.toString()); 354 } 355 356}