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.ImageCanvas; 015import ij.gui.Overlay; 016import ij.plugin.filter.PlugInFilter; 017import ij.process.ImageProcessor; 018import imagingbook.common.color.sets.BasicAwtColor; 019import imagingbook.common.geometry.basic.Pnt2d; 020import imagingbook.common.geometry.mappings.Mapping2D; 021import imagingbook.common.geometry.mappings.nonlinear.LogPolarMapping2; 022import imagingbook.common.ij.DialogUtils; 023import imagingbook.common.ij.overlay.ColoredStroke; 024import imagingbook.common.ij.overlay.ShapeOverlayAdapter; 025import imagingbook.common.image.ImageMapper; 026import imagingbook.core.jdoc.JavaDocHelp; 027import imagingbook.sampleimages.GeneralSampleImage; 028 029import java.awt.Shape; 030import java.awt.event.MouseEvent; 031import java.awt.event.MouseListener; 032import java.awt.geom.Path2D; 033 034import static imagingbook.common.ij.IjUtils.noCurrentImage; 035 036/** 037 * <p> 038 * ImageJ plugin demonstrating the use of 2D log-polar mapping. Two mapping types are available (Version1, Version2). 039 * See Sec. 21.1.6 of [1] for details and examples. The plugin works interactively. Once started, the current mouse 040 * position specifies the transformation's center point. A new image with radius/angle coordinates is opened and 041 * continuously updated, until the plugin is terminated. The circular source grid is displayed as a graphic overlay. 042 * </p> 043 * <p> 044 * [1] W. Burger, M.J. Burge, <em>Digital Image Processing – An Algorithmic Introduction</em>, 3rd ed, Springer 045 * (2022). 046 * </p> 047 * 048 * @author WB 049 * @version 2022/11/16 050 * @see LogPolarMapping2 051 */ 052public class Map_LogPolar_Demo implements PlugInFilter, MouseListener, JavaDocHelp { 053 054 private static int P = 60; // number of radial steps 055 private static int Q = 100; // number of angular steps 056 private double rmin, rmax; // min/max radius (determined from image size) 057 058 private static boolean ShowSamplingGrid = true; 059 private static BasicAwtColor OverlayColorChoice = BasicAwtColor.Green; 060 private static float OverlayStrokeWidth = 0.2f; 061 062 private ImagePlus sourceIm; 063 private ImageCanvas sourceCv; 064 private ImageProcessor sourceIp; 065 private ImageProcessor targetIp; 066 private ImagePlus targetIm; 067 private Mapping2D mapping; 068 private String title; 069 070 /** 071 * Constructor, asks to open a predefined sample image if no other image is currently open. 072 */ 073 public Map_LogPolar_Demo() { 074 if (noCurrentImage()) { 075 DialogUtils.askForSampleImage(GeneralSampleImage.Flower); 076 } 077 } 078 079 @Override 080 public int setup(String arg, ImagePlus im) { 081 this.sourceIm = im; 082 return DOES_ALL; 083 } 084 085 @Override 086 public void run(ImageProcessor ip) { 087 rmax = Math.hypot(ip.getWidth(), ip.getHeight()) / 3; 088 rmin = rmax / 75; 089 090 if (!runDialog()) { 091 return; 092 } 093 094 this.sourceIp = ip; 095 this.targetIp = sourceIp.createProcessor(P, Q); 096 this.targetIm = new ImagePlus("Log Polar Image", targetIp); 097 this.sourceCv = sourceIm.getWindow().getCanvas(); 098 this.sourceCv.addMouseListener(this); 099 //sourceCv.addMouseMotionListener(this); 100 this.sourceIm.setOverlay(null); 101 this.title = sourceIm.getTitle(); 102 this.sourceIm.setTitle(title + " [RUNNING]"); 103 this.sourceIm.updateAndRepaintWindow(); 104 IJ.wait(100); 105 } 106 107 // ----------------------------------------------------------------- 108 109 private void mapAndUpdate(double xc, double yc) { 110 this.mapping = new LogPolarMapping2(xc, yc, P, Q, rmax, rmin).getInverse(); 111 new ImageMapper(mapping).map(sourceIp, targetIp); 112 targetIm.show(); 113 targetIm.updateAndDraw(); 114 if (ShowSamplingGrid) { 115 sourceIm.setOverlay(getSupportRegionOverlay(xc, yc)); 116 sourceIm.updateAndDraw(); 117 } 118 } 119 120 void finish() { 121 sourceIm.setTitle(title); 122 sourceCv.removeMouseListener(this); 123 } 124 125 // --------- generate source grid overlay --------------- 126 127 private Overlay getSupportRegionOverlay(double xc, double yc) { 128 ShapeOverlayAdapter ola = new ShapeOverlayAdapter(); 129 ColoredStroke stroke = new ColoredStroke(OverlayStrokeWidth, OverlayColorChoice.getColor()); 130 ola.setStroke(stroke); 131 132 for (int i = 0; i < P; i++) { 133 ola.addShape(makeCircle(xc, yc, i)); 134 } 135 for (int j = 0; j < Q; j++) { 136 ola.addShape(makeSpoke(xc, yc, j)); 137 } 138 139 return ola.getOverlay(); 140 } 141 142 143 private Shape makeCircle(double xc, double yc, int i) { 144 Path2D path = new Path2D.Double(); 145 Pnt2d start = mapping.applyTo(Pnt2d.from(i, 0)); 146 path.moveTo(start.getX(), start.getY()); 147 for (int j = 1; j <= Q; j++) { 148 Pnt2d pnt = mapping.applyTo(Pnt2d.from(i, j % Q)); 149 path.lineTo(pnt.getX(), pnt.getY()); 150 } 151 return path; 152 } 153 154 private Shape makeSpoke(double xc, double yc, int j) { 155 Path2D path = new Path2D.Double(); 156 Pnt2d outer = mapping.applyTo(Pnt2d.from(P - 1, j)); 157 Pnt2d inner = mapping.applyTo(Pnt2d.from(0, j)); 158 path.moveTo(outer.getX(), outer.getY()); 159 path.lineTo(inner.getX(), inner.getY()); 160 return path; 161 } 162 163 // --------- mouse event handling -------------------- 164 165 @Override 166 public void mouseClicked(MouseEvent e) { 167 if (e.isControlDown()) { 168 finish(); 169 } 170 else { 171 double xc = sourceCv.offScreenXD(e.getX()); 172 double yc = sourceCv.offScreenYD(e.getY()); 173 //IJ.log("Mouse pressed: " + xc +","+yc); 174 //IJ.log("Mag = " + canvas.getMagnification()); 175 mapAndUpdate(xc, yc); 176 } 177 } 178 179 @Override 180 public void mouseEntered(MouseEvent arg0) {} 181 @Override 182 public void mouseExited(MouseEvent arg0) {} 183 @Override 184 public void mousePressed(MouseEvent arg0) {} 185 @Override 186 public void mouseReleased(MouseEvent arg0) {} 187 188 // ---------------------------------------------------- 189 190 boolean runDialog() { 191 GenericDialog gd = new GenericDialog(this.getClass().getSimpleName()); 192 gd.addHelp(getJavaDocUrl()); 193 gd.addNumericField("Radial steps (P)", P, 0); 194 gd.addNumericField("Angular steps (Q) ", Q, 0); 195 gd.addNumericField("Max. radius (rmax)", rmax, 1); 196 gd.addNumericField("Min. radius (rmin)", rmin, 1); 197 gd.addCheckbox("Draw sampling grid", ShowSamplingGrid); 198 gd.addEnumChoice("Overlay color", OverlayColorChoice); 199 200 gd.addMessage("Click left in source image to start,\n" + 201 "click ctrl+left to terminate."); 202 203 gd.showDialog(); 204 if (gd.wasCanceled()) { 205 return false; 206 } 207 208 P = (int) gd.getNextNumber(); 209 Q = (int) gd.getNextNumber(); 210 rmax = gd.getNextNumber(); 211 rmin = gd.getNextNumber(); 212 ShowSamplingGrid = gd.getNextBoolean(); 213 OverlayColorChoice = gd.getNextEnumChoice(BasicAwtColor.class); 214 return true; 215 } 216}