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.FloatProcessor;
017import ij.process.ImageProcessor;
018import imagingbook.common.color.sets.BasicAwtColor;
019import imagingbook.common.ij.DialogUtils;
020import imagingbook.common.ij.IjUtils;
021import imagingbook.common.ij.overlay.ColoredStroke;
022import imagingbook.common.ij.overlay.ShapeOverlayAdapter;
023import imagingbook.common.image.LocalMinMaxFinder;
024import imagingbook.common.image.LocalMinMaxFinder.ExtremalPoint;
025import imagingbook.common.image.matching.CorrCoeffMatcher;
026import imagingbook.core.jdoc.JavaDocHelp;
027import imagingbook.sampleimages.GeneralSampleImage;
028
029import java.awt.Font;
030import java.awt.Rectangle;
031import java.awt.geom.Rectangle2D;
032import java.util.Locale;
033import java.util.Random;
034
035/**
036 * <p>
037 * This ImageJ plugin demonstrates the use of the {@link CorrCoeffMatcher} class. The active (grayscale) image is taken
038 * as the search image. The reference (template) image is extracted from the required rectangular selection (ROI) in the
039 * search image and then corrupted by Gaussian noise with user-specified sigma. The correlation coefficient matcher is
040 * quite resistant to high noise levels. Detected matches are shown as graphic overlays on the input image. Also, the
041 * matching score surface and its local maxima are optionally displayed. See Sec. 23.1.1 (Alg. 23.1) of [1] for
042 * additional details on correlation coefficient matching.
043 * </p>
044 * <p>
045 * [1] W. Burger, M.J. Burge, <em>Digital Image Processing &ndash; An Algorithmic Introduction</em>, 3rd ed, Springer
046 * (2022).
047 * </p>
048 *
049 * @author WB
050 * @version 2022/12/16
051 * @see CorrCoeffMatcher
052 * @see LocalMinMaxFinder
053 */
054public class CorrelCoefficient_Matching implements PlugInFilter, JavaDocHelp {
055
056        private static boolean ShowReferenceImage = true;
057        private static boolean ShowScoreMap = true;
058
059        private static boolean ReferenceAddNoise = true;
060        private static double ReferenceNoiseSigma = 20.0;
061        private static double AcceptanceThreshold = 0.60;
062        private static boolean ShowScoreValues = true;
063        private static int ScoreValueFontSize = 5;
064
065        private static int MaxLocalMinimaCount = 10;
066        private static double MarkerSize = 2;
067        private static double MarkerStrokeWidth = 0.5;
068        private static BasicAwtColor BestMatchColor = BasicAwtColor.Green;
069        private static BasicAwtColor OtherMatchColor = BasicAwtColor.Orange;
070
071        private ImagePlus imgI;         // the search image
072
073        /**
074         * Constructor, asks to open a predefined sample image if no other image is currently open.
075         */
076        public CorrelCoefficient_Matching() {
077                if (IjUtils.noCurrentImage()) {
078                        if (DialogUtils.askForSampleImage(GeneralSampleImage.IrishManor)) {
079                                ImagePlus imp = IJ.getImage();
080                                imp.setRoi(190, 41, 30, 30);
081                        }
082                }
083        }
084
085        @Override
086    public int setup(String arg, ImagePlus imp) {
087        this.imgI = imp;
088        return DOES_8G + ROI_REQUIRED + NO_CHANGES;
089    }
090
091        @Override
092    public void run(ImageProcessor ipI) {
093
094                Rectangle roi = ipI.getRoi();
095                if (roi == null) {
096                        IJ.showMessage("Rectangular selection required!");
097                        return;
098                }
099
100                if (!runDialog()) {
101                        return;
102                }
103
104                ImageProcessor I = ipI;                 // search image
105        ImageProcessor R = ipI.crop();  // reference image
106                int wR = R.getWidth();
107                int hR = R.getHeight();
108
109                if (ReferenceAddNoise) {
110                        Random rg = new Random();
111                        for (int u = 0; u < wR; u++) {
112                                for (int v = 0; v < hR; v++) {
113                                        double x = R.get(u, v) + rg.nextGaussian() * ReferenceNoiseSigma;
114                                        R.putPixel(u, v, (int) Math.round(x));
115                                }
116                        }
117                }
118
119                if (ShowReferenceImage) {
120                        new ImagePlus("Reference image (R)", R).show();
121                }
122
123
124                CorrCoeffMatcher matcher = new CorrCoeffMatcher(I);
125                float[][] Q = matcher.getMatch(R);                              // 2D match score function
126                FloatProcessor fQ = new FloatProcessor(Q);
127
128                // find local minima in 2D score function Q:
129                LocalMinMaxFinder locator = new LocalMinMaxFinder(fQ);
130                ExtremalPoint[] maxima = locator.getMaxima(MaxLocalMinimaCount);
131
132                // show result on the search image:
133                {
134                        imgI.setRoi((Roi) null);        // remove ROI selection
135                        ShapeOverlayAdapter ola = new ShapeOverlayAdapter();
136                        ColoredStroke bestStroke = new ColoredStroke(MarkerStrokeWidth, BestMatchColor.getColor());
137                        ColoredStroke otherStroke = new ColoredStroke(MarkerStrokeWidth, OtherMatchColor.getColor());
138
139                        // mark matches in search image:
140                        ola.setStroke(bestStroke);
141                        ola.setTextColor(BestMatchColor.getColor());
142                        ola.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, ScoreValueFontSize));
143                        for (int i = 0; i < maxima.length; i++) {
144                                ExtremalPoint ep = maxima[i];
145                                if (ep.q < AcceptanceThreshold) {
146                                        break;
147                                }
148                                if (i > 0) {
149                                        ola.setStroke(otherStroke);
150                                        ola.setTextColor(OtherMatchColor.getColor());
151                                }
152                                ola.addShape(new Rectangle2D.Double(maxima[i].x, maxima[i].y, wR, hR));
153                                if (ShowScoreValues) {
154                                        ola.addText(ep.x + 1, ep.y, String.format(Locale.US, "%.3f", maxima[i].q));
155                                }
156                        }
157                        imgI.setOverlay(ola.getOverlay());
158                }
159
160                if (ShowScoreMap) {
161                        // create graphic overlay:
162                        ShapeOverlayAdapter ola = new ShapeOverlayAdapter();
163                        ColoredStroke bestStroke = new ColoredStroke(MarkerStrokeWidth, BestMatchColor.getColor());
164                        ColoredStroke otherStroke = new ColoredStroke(MarkerStrokeWidth, OtherMatchColor.getColor());
165
166                        // mark local minima
167                        ola.setStroke(bestStroke);
168                        ola.setTextColor(BestMatchColor.getColor());
169                        ola.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, ScoreValueFontSize));
170                        for (int i = 0; i < maxima.length; i++) {
171                                ExtremalPoint ep = maxima[i];
172                                if (ep.q < AcceptanceThreshold) {
173                                        break;
174                                }
175                                if (i > 0) {
176                                        ola.setStroke(otherStroke);
177                                        ola.setTextColor(OtherMatchColor.getColor());
178                                }
179                                ola.addShape(ep.getShape(MarkerSize));
180                                if (ShowScoreValues) {
181                                        ola.addText(ep.x + MarkerSize, ep.y, String.format(Locale.US, "%.3f", maxima[i].q));
182                                }
183                        }
184
185                        ImagePlus matchIm = new ImagePlus("Match of " + imgI.getTitle(), fQ);
186                        matchIm.setOverlay(ola.getOverlay());
187                        matchIm.show();
188                }
189    }
190
191        // -------------------------------------------------
192 
193    private boolean runDialog() {
194                GenericDialog gd = new GenericDialog(this.getClass().getSimpleName());
195                gd.addHelp(getJavaDocUrl());
196                gd.addCheckbox("Add noise to reference image", ReferenceAddNoise);
197                gd.addNumericField("Reference noise sigma", ReferenceNoiseSigma, 2);
198                gd.addNumericField("Acceptance threshold (< 1.0)", AcceptanceThreshold, 2);
199                gd.addNumericField("Max. local minima count", MaxLocalMinimaCount, 0);
200                gd.addEnumChoice("Best match color", BestMatchColor);
201                gd.addEnumChoice("Other match color", OtherMatchColor);
202                gd.addCheckbox("Show score values", ShowScoreValues);
203                gd.addNumericField("Score value font size", ScoreValueFontSize, 0);
204                gd.addCheckbox("Show reference image", ShowReferenceImage);
205                gd.addCheckbox("Show score map", ShowScoreMap);
206
207                gd.showDialog();
208                if (gd.wasCanceled())
209                        return false;
210
211                ReferenceAddNoise = gd.getNextBoolean();
212                ReferenceNoiseSigma = gd.getNextNumber();
213                AcceptanceThreshold = gd.getNextNumber();
214                MaxLocalMinimaCount = (int) gd.getNextNumber();
215                BestMatchColor = gd.getNextEnumChoice(BasicAwtColor.class);
216                OtherMatchColor = gd.getNextEnumChoice(BasicAwtColor.class);
217                ShowScoreValues = gd.getNextBoolean();
218                ScoreValueFontSize = (int) gd.getNextNumber();
219                ShowReferenceImage = gd.getNextBoolean();
220                ShowScoreMap = gd.getNextBoolean();
221                return true;
222    }
223}