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 Ch23_Image_Matching; 010 011import ij.IJ; 012import ij.ImagePlus; 013import ij.gui.GenericDialog; 014import ij.gui.Roi; 015import ij.plugin.filter.PlugInFilter; 016import ij.process.ByteProcessor; 017import ij.process.FloatProcessor; 018import ij.process.ImageProcessor; 019import imagingbook.common.color.sets.BasicAwtColor; 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.image.LocalMinMaxFinder; 025import imagingbook.common.image.LocalMinMaxFinder.ExtremalPoint; 026import imagingbook.common.image.matching.ChamferMatcher; 027import imagingbook.common.image.matching.DistanceTransform.DistanceType; 028import imagingbook.core.jdoc.JavaDocHelp; 029import imagingbook.sampleimages.GeneralSampleImage; 030 031import java.awt.Font; 032import java.awt.Rectangle; 033import java.awt.geom.Rectangle2D; 034import java.util.Random; 035 036/** 037 * <p> 038 * This ImageJ plugin demonstrates the use of the {@link ChamferMatcher} class. The active (search) image is assumed to 039 * be binary (checked). The reference (template) image is extracted from the required rectangular selection (ROI) in the 040 * search image and then corrupted with binary (salt-and-pepper) noise (i.e., a certain percentage of its pixels is 041 * randomly flipped). Increasing noise leads to poorer match results and matching eventually fails when the noise level 042 * is too high. Detected matches are shown as graphic overlays on the input image. Also, the matching score surface and 043 * its local minima are optionally displayed. See Sec. 23.2.3 (Alg. 23.3) of [1] for details on Chamfer matching. 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/12/16 052 * @see ChamferMatcher 053 * @see LocalMinMaxFinder 054 */ 055public class Chamfer_Matching implements PlugInFilter, JavaDocHelp { 056 057 private static boolean ShowReferenceImage = true; 058 private static boolean ShowScoreMap = true; 059 060 private static boolean ReferenceAddNoise = true; 061 private static double ReferenceNoiseLevel = 2.5 / 100; 062 private static boolean ShowScoreValues = true; 063 private static int ScoreValueFontSize = 5; 064 065 private static DistanceType distNorm = DistanceType.L2; 066 private static int MaxLocalMinimaCount = 5; 067 private static double MarkerSize = 2; 068 private static double MarkerStrokeWidth = 0.5; 069 private static BasicAwtColor BestMatchColor = BasicAwtColor.Green; 070 private static BasicAwtColor OtherMatchColor = BasicAwtColor.Orange; 071 072 private ImagePlus imgI; // the search image 073 074 /** 075 * Constructor, asks to open a predefined sample image if no other image is currently open. 076 */ 077 public Chamfer_Matching() { 078 if (IjUtils.noCurrentImage()) { 079 if (DialogUtils.askForSampleImage(GeneralSampleImage.CirclesSquares)) { 080 ImagePlus imp = IJ.getImage(); 081 imp.setRoi(39, 40, 58, 58); 082 } 083 } 084 } 085 086 @Override 087 public int setup(String arg, ImagePlus imp) { 088 this.imgI = imp; 089 return DOES_8G + ROI_REQUIRED + NO_CHANGES; 090 } 091 092 @Override 093 public void run(ImageProcessor ipI) { 094 if (!IjUtils.isBinary(ipI)) { 095 IJ.showMessage("Current image is not binary!"); 096 return; 097 } 098 099 Rectangle roi = ipI.getRoi(); 100 if (roi == null) { 101 IJ.showMessage("Rectangular selection required!"); 102 return; 103 } 104 105 if (!runDialog()) { 106 return; 107 } 108 109 ByteProcessor I = (ByteProcessor) ipI; // search image 110 ByteProcessor R = (ByteProcessor) ipI.crop(); // reference image 111 int wR = R.getWidth(); 112 int hR = R.getHeight(); 113 114 if (ReferenceAddNoise) { 115 Random rg = new Random(); 116 for (int u = 0; u < wR; u++) { 117 for (int v = 0; v < hR; v++) { 118 if (rg.nextDouble() < ReferenceNoiseLevel) { 119 R.set(u, v, (R.get(u, v) > 0) ? 0 : 255); 120 } 121 } 122 } 123 } 124 125 if (ShowReferenceImage) { 126 new ImagePlus("Reference image (R)", R).show(); 127 } 128 129 ChamferMatcher matcher = new ChamferMatcher(I, distNorm); 130 float[][] Q = matcher.getMatch(R); // 2D match score function 131 FloatProcessor fQ = new FloatProcessor(Q); 132 133 // find local minima in 2D score function Q: 134 LocalMinMaxFinder locator = new LocalMinMaxFinder(fQ); 135 ExtremalPoint[] minima = locator.getMinima(MaxLocalMinimaCount); 136 137 // show result on search image: 138 { 139 imgI.setRoi((Roi) null); // remove ROI selection 140 ShapeOverlayAdapter ola = new ShapeOverlayAdapter(); 141 ColoredStroke bestStroke = new ColoredStroke(MarkerStrokeWidth, BestMatchColor.getColor()); 142 ColoredStroke otherStroke = new ColoredStroke(MarkerStrokeWidth, OtherMatchColor.getColor()); 143 144 // mark matches in search image: 145 ola.setStroke(bestStroke); 146 ola.setTextColor(BestMatchColor.getColor()); 147 ola.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, ScoreValueFontSize)); 148 for (int i = 0; i < minima.length; i++) { 149 if (i > 0) { 150 ola.setStroke(otherStroke); 151 ola.setTextColor(OtherMatchColor.getColor()); 152 } 153 ExtremalPoint ep = minima[i]; 154 ola.addShape(new Rectangle2D.Double(minima[i].x, minima[i].y, wR, hR)); 155 if (ShowScoreValues) { 156 ola.addText(ep.x + 1, ep.y, String.format("%.0f", minima[i].q)); 157 } 158 } 159 imgI.setOverlay(ola.getOverlay()); 160 } 161 162 if (ShowScoreMap) { 163 // create graphic overlay: 164 ShapeOverlayAdapter ola = new ShapeOverlayAdapter(); 165 ColoredStroke bestStroke = new ColoredStroke(MarkerStrokeWidth, BestMatchColor.getColor()); 166 ColoredStroke otherStroke = new ColoredStroke(MarkerStrokeWidth, OtherMatchColor.getColor()); 167 168 // mark local minima 169 ola.setStroke(bestStroke); 170 ola.setTextColor(BestMatchColor.getColor()); 171 ola.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, ScoreValueFontSize)); 172 for (int i = 0; i < minima.length; i++) { 173 if (i > 0) { 174 ola.setStroke(otherStroke); 175 ola.setTextColor(OtherMatchColor.getColor()); 176 } 177 ExtremalPoint ep = minima[i]; 178 ola.addShape(ep.getShape(MarkerSize)); 179 if (ShowScoreValues) { 180 ola.addText(ep.x + MarkerSize, ep.y, String.format("%.0f", minima[i].q)); 181 } 182 } 183 184 ImagePlus matchIm = new ImagePlus("Match of " + imgI.getTitle(), fQ); 185 matchIm.setOverlay(ola.getOverlay()); 186 matchIm.show(); 187 } 188 } 189 190 // ------------------------------------------------- 191 192 private boolean runDialog() { 193 GenericDialog gd = new GenericDialog(this.getClass().getSimpleName()); 194 gd.addHelp(getJavaDocUrl()); 195 if (imgI.isInvertedLut()) { 196 gd.setInsets(0, 0, 0); 197 gd.addMessage("NOTE: Image has inverted LUT (0 = white)!"); 198 } 199 gd.addCheckbox("Add noise to reference image", ReferenceAddNoise); 200 gd.addNumericField("Noise level (%)", ReferenceNoiseLevel * 100, 1); 201 gd.addEnumChoice("Distance transform norm", distNorm); 202 gd.addNumericField("Max. local minima count", MaxLocalMinimaCount, 0); 203 gd.addEnumChoice("Best match color", BestMatchColor); 204 gd.addEnumChoice("Other match color", OtherMatchColor); 205 gd.addCheckbox("Show score values", ShowScoreValues); 206 gd.addNumericField("Score value font size", ScoreValueFontSize, 0); 207 gd.addCheckbox("Show reference image", ShowReferenceImage); 208 gd.addCheckbox("Show score map", ShowScoreMap); 209 210 gd.showDialog(); 211 if (gd.wasCanceled()) 212 return false; 213 214 ReferenceAddNoise = gd.getNextBoolean(); 215 ReferenceNoiseLevel = gd.getNextNumber() / 100; 216 distNorm = gd.getNextEnumChoice(DistanceType.class); 217 MaxLocalMinimaCount = (int) gd.getNextNumber(); 218 BestMatchColor = gd.getNextEnumChoice(BasicAwtColor.class); 219 OtherMatchColor = gd.getNextEnumChoice(BasicAwtColor.class); 220 ShowScoreValues = gd.getNextBoolean(); 221 ScoreValueFontSize = (int) gd.getNextNumber(); 222 ShowReferenceImage = gd.getNextBoolean(); 223 ShowScoreMap = gd.getNextBoolean(); 224 return true; 225 } 226 227}