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 Ch21_Geometric_Operations; 010 011import ij.IJ; 012import ij.ImagePlus; 013import ij.gui.GenericDialog; 014import ij.gui.PolygonRoi; 015import ij.gui.Roi; 016import ij.plugin.filter.PlugInFilter; 017import ij.process.ImageProcessor; 018import imagingbook.common.geometry.basic.Pnt2d; 019import imagingbook.common.geometry.basic.Pnt2d.PntInt; 020import imagingbook.common.geometry.mappings.linear.LinearMapping2D; 021import imagingbook.common.geometry.mappings.linear.ProjectiveMapping2D; 022import imagingbook.common.ij.DialogUtils; 023import imagingbook.common.ij.IjUtils; 024import imagingbook.common.ij.RoiUtils; 025import imagingbook.common.image.ImageMapper; 026import imagingbook.common.image.interpolation.InterpolationMethod; 027import imagingbook.common.math.PrintPrecision; 028import imagingbook.core.jdoc.JavaDocHelp; 029import imagingbook.sampleimages.GeneralSampleImage; 030 031/** 032 * <p> 033 * ImageJ plugin, performs 4-point projective mapping from a selected polygon ROI to the specified paper proportions (A4 034 * or Letter, in portrait format). See Sec. 21.1 (Exercise 21.5 and Fig. 21.17) of [1] for more details. 035 * </p> 036 * <p> 037 * [1] W. Burger, M.J. Burge, <em>Digital Image Processing – An Algorithmic Introduction</em>, 3rd ed, Springer 038 * (2022). 039 * </p> 040 * 041 * @author W. Burger 042 * @version 2022/11/28 043 * @see ProjectiveMapping2D 044 * @see LinearMapping2D 045 * @see ImageMapper 046 */ 047public class Rectify_Quad_Selection implements PlugInFilter, JavaDocHelp { 048 049 private static PaperFormatType PaperFormat = PaperFormatType.A4; 050 private static double OutputPixelSize = 0.5; // pixel size in mm 051 private static InterpolationMethod IPM = InterpolationMethod.Bilinear; 052 private static boolean ListTransformationMatrix = true; 053 054 private ImagePlus im = null; 055 056 /** 057 * Constructor, asks to create sample image if no other image is currently open. 058 */ 059 public Rectify_Quad_Selection() { 060 if (IjUtils.noCurrentImage() && DialogUtils.askForSampleImage()) { 061 ImagePlus imp = GeneralSampleImage.PostalPackageSmall.getImagePlus(); 062 float[] xpts = {22, 330, 981, 756}; // manually selected! 063 float[] ypts = {347, 71, 207, 591}; 064 Roi roi = new PolygonRoi(xpts, ypts, Roi.POLYGON); 065 imp.setRoi(roi); 066 imp.show(); 067 } 068 } 069 070 // ----------------------------------------------- 071 072 @Override 073 public int setup(String arg0, ImagePlus im) { 074 this.im = im; 075 return DOES_ALL + NO_CHANGES + ROI_REQUIRED; 076 } 077 078 @Override 079 public void run(ImageProcessor source) { 080 Roi roi = im.getRoi(); 081 if (!(roi instanceof PolygonRoi)) { 082 IJ.error("Polygon selection required!"); 083 return; 084 } 085 086 Pnt2d[] sourceCorners = RoiUtils.getOutlinePointsFloat(roi); 087 if (sourceCorners.length < 4) { 088 IJ.error("At least 4 points must be selected!"); 089 return; 090 } 091 092 if (!runDialog()) { 093 return; 094 } 095 096 double targetWidth = PaperFormat.width; // in millimeters 097 double targetHeight = PaperFormat.height; 098 099 int tWidth = (int) Math.round(targetWidth / OutputPixelSize); // pixels 100 int tHeight = (int) Math.round(targetHeight / OutputPixelSize); 101 102 Pnt2d[] targetCorners = { 103 PntInt.from(0, 0), 104 PntInt.from(tWidth, 0), 105 PntInt.from(tWidth, tHeight), 106 PntInt.from(0, tHeight)}; 107 108 LinearMapping2D mp = // inverse mapping (target to source) 109 ProjectiveMapping2D.fromPoints(sourceCorners, targetCorners).getInverse(); 110 111 if (ListTransformationMatrix) { 112 PrintPrecision.set(6); 113 IJ.log("Inverse transformation (target to source): M = \n" + mp.toString()); 114 } 115 116 ImageProcessor target = source.createProcessor(tWidth, tHeight); 117 ImageMapper mapper = new ImageMapper(mp, null, IPM); 118 mapper.map(source, target); 119 new ImagePlus("target", target).show(); 120 } 121 122 // -------------------------------------------- 123 124 enum PaperFormatType { 125 A4(210, 297), 126 Letter(216, 279); 127 128 final double width, height; // paper dimensions in mm 129 130 PaperFormatType(double width, double height) { 131 this.width = width; 132 this.height = height; 133 } 134 } 135 136 // -------------------------------------------- 137 138 private boolean runDialog() { 139 GenericDialog gd = new GenericDialog(this.getClass().getSimpleName()); 140 gd.addHelp(getJavaDocUrl()); 141 gd.addMessage(DialogUtils.formatText(50, 142 "How to use: Select the four corners of the quad to be rectified", 143 "with a polygon ROI, in clockwise direction, starting at the point", 144 "that should map to the top-left corner (in portrait mode).", 145 "Note that only the first 4 points are used!")); 146 147 gd.addEnumChoice("Output paper format", PaperFormat); 148 gd.addNumericField("Output pixel size (mm)", OutputPixelSize, 2); 149 gd.addEnumChoice("Pixel interpolation method", IPM); 150 gd.addCheckbox("List transformation matrix", ListTransformationMatrix); 151 152 gd.showDialog(); 153 if (gd.wasCanceled()) 154 return false; 155 156 PaperFormat = gd.getNextEnumChoice(PaperFormatType.class); 157 OutputPixelSize = gd.getNextNumber(); 158 IPM = gd.getNextEnumChoice(InterpolationMethod.class); 159 ListTransformationMatrix = gd.getNextBoolean(); 160 161 return true; 162 } 163 164}