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 Ch25_SIFT;
011
012import ij.IJ;
013import ij.ImagePlus;
014import ij.gui.GenericDialog;
015import ij.plugin.filter.PlugInFilter;
016import ij.process.FloatProcessor;
017import ij.process.ImageProcessor;
018import imagingbook.common.color.sets.BasicAwtColor;
019import imagingbook.common.geometry.basic.Pnt2d;
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.math.VectorNorm.NormType;
025import imagingbook.common.sift.SiftColors;
026import imagingbook.common.sift.SiftDescriptor;
027import imagingbook.common.sift.SiftDetector;
028import imagingbook.common.sift.SiftMatch;
029import imagingbook.common.sift.SiftMatcher;
030import imagingbook.common.sift.SiftParameters;
031import imagingbook.core.jdoc.JavaDocHelp;
032import imagingbook.sampleimages.GeneralSampleImage;
033
034import java.awt.Color;
035import java.awt.Font;
036import java.awt.Shape;
037import java.awt.geom.AffineTransform;
038import java.awt.geom.Line2D;
039import java.awt.geom.QuadCurve2D;
040import java.util.List;
041
042import static imagingbook.common.color.sets.ColorEnumeration.getColors;
043import static imagingbook.common.ij.IjUtils.noCurrentImage;
044
045/**
046 * <p>
047 * This ImageJ plugin demonstrates the use of the SIFT detection and matching framework. See Sec. 25.5 of [1] for
048 * details.
049 * </p>
050 * <p>
051 * The plugin takes a single image, which is assumed to be composed of a left and right frame. The input image is split
052 * horizontally, then SIFT detection and matching is applied to the two sub-images. The input image is always converted
053 * to grayscale (and normalized to [0,1]) before SIFT feature detection is performed. The result is displayed as a
054 * graphic overlay by connecting and annotating the best-matching features. When saved as a TIFF image the overlay is
055 * preserved.
056 * </p>
057 * <p>
058 * [1] W. Burger, M.J. Burge, <em>Digital Image Processing &ndash; An Algorithmic Introduction</em>, 3rd ed, Springer
059 * (2022).
060 * </p>
061 *
062 * @author WB
063 * @version 2022/11/20
064 */
065public class SIFT_Matching_Demo implements PlugInFilter, JavaDocHelp {
066
067        // matching parameters:
068        private static NormType DistanceNormType = SiftMatcher.DefaultNormType;
069        private static double MaxDistanceRatio = SiftMatcher.DefaultRMax;
070
071        // display parameters:
072        private static int NumberOfMatchesToShow = 25;
073        private static double FeatureScale = 1.0;
074        private static boolean ShowFeatureLabels = true;
075
076        private static double MatchLineCurvature = 0.25;
077        private static double FeatureStrokewidth = 1.0;
078
079        private static BasicAwtColor MatchLineColor = BasicAwtColor.Magenta;
080        private static BasicAwtColor LabelColor = BasicAwtColor.Yellow;
081        private static Font LabelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 12);
082        
083        private static Color[] ScaleLevelColors = getColors(SiftColors.class);
084        
085        private ImagePlus im = null;
086
087        /**
088         * Constructor, asks to open a predefined sample image if no other image is currently open.
089         */
090        public SIFT_Matching_Demo() {
091                if (noCurrentImage()) {
092                        DialogUtils.askForSampleImage(GeneralSampleImage.RamsesSmall);
093                }
094        }
095        
096        @Override
097        public int setup(String arg0, ImagePlus im) {
098                this.im = im;
099                return DOES_ALL;
100        }
101
102        @Override
103        public void run(ImageProcessor ip) {            
104                if (!runDialog()) {
105                        return;
106                }
107
108                final int w = ip.getWidth();
109                final int h = ip.getHeight();
110                final int w2 = w / 2;
111                
112                FloatProcessor Ia = IjUtils.crop(ip, 0, 0, w2, h).convertToFloatProcessor();
113                FloatProcessor Ib = IjUtils.crop(ip, w2, 0, w2, h).convertToFloatProcessor();
114                
115                SiftParameters siftParams = new SiftParameters();
116                // modify SIFT parameters here if needed
117                
118                // we use the same parameters on left and right image
119                SiftDetector sdA = new SiftDetector(Ia, siftParams);
120                SiftDetector sdB = new SiftDetector(Ib, siftParams);
121                
122                List<SiftDescriptor> fsA = sdA.getSiftFeatures();
123                List<SiftDescriptor> fsB = sdB.getSiftFeatures();
124
125                IJ.log("SIFT features found in left image: "  + fsA.size());
126                IJ.log("SIFT features found in right image: " + fsB.size());
127
128                // create a SIFT matcher and perform matching:
129                SiftMatcher sm = new SiftMatcher(DistanceNormType, MaxDistanceRatio);
130                List<SiftMatch> matches = sm.match(fsA, fsB);   // matches are sorted by decreasing quality
131                
132                IJ.log("Matches found: " + matches.size());
133                if (matches.isEmpty()) {
134                        return;
135                }
136
137                // --------------------------------------------------
138                
139                ColoredStroke matchLineStroke = new ColoredStroke(0.2, MatchLineColor.getColor());
140                ColoredStroke[] fStrokes = new ColoredStroke[ScaleLevelColors.length];
141                for (int i = 0; i < ScaleLevelColors.length; i++) {
142                        fStrokes[i] = new ColoredStroke(FeatureStrokewidth, ScaleLevelColors[i]);
143                }
144                
145                ShapeOverlayAdapter ola = new ShapeOverlayAdapter();
146                ola.setTextColor(LabelColor.getColor());
147                ola.setFont(LabelFont);
148                
149                AffineTransform trans = AffineTransform.getTranslateInstance(w2, 0);
150                
151                // add vertical separator between left and right image:
152                ola.addShape(new Line2D.Double(w2 - 0.5, 0, w2 - 0.5, h), 
153                                new ColoredStroke(0.2, Color.green, 5)); 
154                
155                // add SIFT markers, connecting lines and number labels
156                int n = 1;
157                for (SiftMatch m : matches) {
158                        SiftDescriptor dA = m.getDescriptor1();
159                        SiftDescriptor dB = m.getDescriptor2();
160                        
161                        // draw the matched SIFT markers:
162                        Shape sA = dA.getShape(FeatureScale);
163                        Shape sB = trans.createTransformedShape(dB.getShape(FeatureScale));
164                        ola.addShape(sA, fStrokes[dA.getScaleLevel() % fStrokes.length]);
165                        ola.addShape(sB, fStrokes[dB.getScaleLevel() % fStrokes.length]);
166                        
167                        // draw the connecting lines:
168                        Shape cAB = makeConnectingShape(dA, dB.plus(w2, 0));
169                        ola.addShape(cAB, matchLineStroke);
170                        
171                        // draw the numeric feature labels on both sides:
172                        if (ShowFeatureLabels) {
173                                String label = Integer.toString(n);     
174                                ola.addText(dA.getX(), dA.getY(), label);
175                                ola.addText(dB.getX() + w2, dB.getY(), label);
176                        }
177                        if (++n > NumberOfMatchesToShow) break;
178                }
179
180                im.setOverlay(ola.getOverlay());
181        }
182        
183        // -------------------------
184        
185        private Shape makeConnectingShape(Pnt2d p1, Pnt2d p2) {
186                double x1 = p1.getX(); 
187                double y1 = p1.getY();
188                double x2 = p2.getX(); 
189                double y2 = p2.getY();
190                double dx = x2 - x1;
191                double dy = y2 - y1;
192                double ctrlx = (x1 + x2) / 2 - MatchLineCurvature * dy;
193                double ctrly = (y1 + y2) / 2 + MatchLineCurvature * dx;
194                return new QuadCurve2D.Double(x1, y1, ctrlx, ctrly, x2, y2);
195        }
196        
197        // -------------------------
198        
199        private boolean runDialog() {
200                GenericDialog gd = new GenericDialog(this.getClass().getSimpleName());
201                gd.addHelp(getJavaDocUrl());
202                gd.addMessage("This plugin expects a single image composed of a left and right frame.");
203
204                gd.addMessage("SIFT matching parameters:");
205                gd.addEnumChoice("Distance norm type", DistanceNormType);
206                gd.addNumericField("Max. ratio between 1st/2nd match (rMax)", MaxDistanceRatio, 2);
207                
208                gd.addMessage("Display parameters:");
209                gd.addNumericField("Number of matches to show", NumberOfMatchesToShow, 0);
210                gd.addNumericField("Feature scale", FeatureScale, 2);
211                gd.addNumericField("Match line curvature", MatchLineCurvature, 2);
212                gd.addEnumChoice("Match line color", MatchLineColor);
213                gd.addEnumChoice("Label color", LabelColor);
214                gd.addCheckbox("Show feature labels", ShowFeatureLabels);
215                
216                gd.showDialog();
217                if (gd.wasCanceled()) {
218                        return false;
219                }
220                
221                DistanceNormType = gd.getNextEnumChoice(NormType.class);
222                MaxDistanceRatio = gd.getNextNumber();
223                NumberOfMatchesToShow = (int) gd.getNextNumber();
224                FeatureScale = gd.getNextNumber();
225                MatchLineCurvature = gd.getNextNumber();
226                MatchLineColor = gd.getNextEnumChoice(BasicAwtColor.class);
227                LabelColor = gd.getNextEnumChoice(BasicAwtColor.class);
228                ShowFeatureLabels = gd.getNextBoolean();
229                return true;
230        }
231
232}