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-2025 Wilhelm Burger, Mark J. Burge. All rights reserved. 007 * Visit https://imagingbook.com for additional details. 008 ******************************************************************************/ 009 010package imagingbook.common.image; 011 012import ij.process.ImageProcessor; 013 014/** 015 * <p> 016 * Instances of this class perform the transformation between 2D image coordinates and indexes into the associated 1D 017 * pixel array and vice versa. As usual images are assumed to be stored in row-major order. Instances of this class do 018 * not hold any image data themselves, they just perform the indexing task. This is used, for example, by class 019 * {@link PixelPack}, which provides a universal image data container which uses {@link GridIndexer2D} internally. 020 * </p> 021 * <p> 022 * The (abstract) method {@link #getIndex(int, int)} returns the 1D array index for a pair of 2D image coordinates. It 023 * is implemented by the inner subclasses {@link DefaultValueIndexer}, {@link MirrorImageIndexer} and 024 * {@link NearestBorderIndexer}. They exhibit different behaviors when accessing out-of-image coordinates (see 025 * {@link OutOfBoundsStrategy}). 026 * </p> 027 * 028 * @author WB 029 * @version 2022/09/17 030 * @see OutOfBoundsStrategy 031 */ 032public abstract class GridIndexer2D { 033 034 public static final OutOfBoundsStrategy DefaultOutOfBoundsStrategy = OutOfBoundsStrategy.NearestBorder; 035 036 /** 037 * Creates and returns a new {@link GridIndexer2D} with the specified size and {@link OutOfBoundsStrategy}. 038 * 039 * @param width grid size (horizontal) 040 * @param height grid size (vertical) 041 * @param obs out-of-bounds strategy 042 * @return a new {@link GridIndexer2D} 043 */ 044 public static GridIndexer2D create(int width, int height, OutOfBoundsStrategy obs) { 045 if (obs == null) { 046 obs = DefaultOutOfBoundsStrategy; 047 } 048 switch (obs) { 049 case DefaultValue: return new DefaultValueIndexer(width, height); 050 case NearestBorder : return new NearestBorderIndexer(width, height); 051 case MirrorImage : return new MirrorImageIndexer(width, height); 052 case PeriodicImage : return new PeriodicImageIndexer(width, height); 053 case ThrowException : return new ExceptionIndexer(width, height); 054 default: 055 throw new IllegalStateException("Unexpected value: " + obs); 056 } 057 } 058 059 /** 060 * Creates and returns a new {@link GridIndexer2D} for the specified image and {@link OutOfBoundsStrategy}. 061 * @param ip the image to be associated with the returned indexer 062 * @param obs out-of-bounds strategy 063 * @return a new {@link GridIndexer2D} 064 */ 065 public static GridIndexer2D create(ImageProcessor ip, OutOfBoundsStrategy obs) { 066 return create(ip.getWidth(), ip.getHeight(), obs); 067 } 068 069 final int width; 070 final int height; 071 final OutOfBoundsStrategy obs; 072 073 private GridIndexer2D(int width, int height, OutOfBoundsStrategy obs) { 074 this.width = width; 075 this.height = height; 076 this.obs = obs; 077 } 078 079 /** 080 * Returns the 1D array index for a given pair of image coordinates. For u, v coordinates outside the image, the 081 * returned index depends on the implementing subclass of {@link GridIndexer2D}. As a general rule, this method either 082 * returns a valid 1D array index or throws an exception. 083 * Subclasses implement (override) this method. 084 * 085 * @param u x-coordinate 086 * @param v y-coordinate 087 * @return 1D array index 088 */ 089 public abstract int getIndex(int u, int v); 090 091 /** 092 * Returns the 1D array index for a given pair of image coordinates, assuming that the specified position is 093 * inside the image. 094 * 095 * @param u x-coordinate 096 * @param v y-coordinate 097 * @return the associated 1D index 098 */ 099 protected int getWithinBoundsIndex(int u, int v) { 100 return width * v + u; 101 } 102 103 /** 104 * Returns the width of the associated image. 105 * @return the image width 106 */ 107 public int getWidth() { 108 return this.width; 109 } 110 111 /** 112 * Returns the height of the associated image. 113 * @return the image height 114 */ 115 public int getHeight() { 116 return this.height; 117 } 118 119 /** 120 * Returns the out-of-bounds strategy (see {qlink OutOfBoundsStrategy} used by this indexer. 121 * 122 * @return the out-of-bounds strategy 123 */ 124 public OutOfBoundsStrategy getOutOfBoundsStrategy() { 125 return this.obs; 126 } 127 128 // --------------------------------------------------------- 129 130 /** 131 * This indexer returns the closest border pixel for coordinates outside the image bounds. This is the most common 132 * method. There is no public constructor. To instantiate use method 133 * {@link GridIndexer2D#create(int, int, OutOfBoundsStrategy)} with {@link OutOfBoundsStrategy#NearestBorder}. 134 */ 135 public static class NearestBorderIndexer extends GridIndexer2D { 136 137 NearestBorderIndexer(int width, int height) { 138 super(width, height, OutOfBoundsStrategy.NearestBorder); 139 } 140 141 @Override 142 public int getIndex(int u, int v) { 143 if (u < 0) { 144 u = 0; 145 } 146 else if (u >= width) { 147 u = width - 1; 148 } 149 if (v < 0) { 150 v = 0; 151 } 152 else if (v >= height) { 153 v = height - 1; 154 } 155 return getWithinBoundsIndex(u, v); 156 } 157 } 158 159 /** 160 * This indexer returns mirrored image values for coordinates outside the image bounds. There is no public 161 * constructor. To instantiate use method {@link GridIndexer2D#create(int, int, OutOfBoundsStrategy)} with 162 * {@link OutOfBoundsStrategy#MirrorImage}. 163 */ 164 public static class MirrorImageIndexer extends GridIndexer2D { 165 private final int width2, height2; 166 167 MirrorImageIndexer(int width, int height) { 168 super(width, height, OutOfBoundsStrategy.MirrorImage); 169 this.width2 = 2 * width; 170 this.height2 = 2 * height; 171 } 172 173 @Override 174 public int getIndex(int u, int v) { 175 int u2 = Math.floorMod(u, width2); 176 if (u2 >= width) 177 u2 = (width2) - u2 - 1; 178 179 int v2 = Math.floorMod(v, height2); 180 if (v2 >= height) 181 v2 = (height2) - v2 - 1; 182 183 return getWithinBoundsIndex(u2, v2); 184 } 185 } 186 187 /** 188 * This indexer returns repetitive image values for coordinates outside the image bounds. There is no public 189 * constructor. To instantiate use method {@link GridIndexer2D#create(int, int, OutOfBoundsStrategy)} with 190 * {@link OutOfBoundsStrategy#PeriodicImage}. 191 */ 192 public static class PeriodicImageIndexer extends GridIndexer2D { 193 194 PeriodicImageIndexer(int width, int height) { 195 super(width, height, OutOfBoundsStrategy.PeriodicImage); 196 } 197 198 @Override 199 public int getIndex(int u, int v) { 200 u = Math.floorMod(u, width); 201 v = Math.floorMod(v, height); 202 return getWithinBoundsIndex(u, v); 203 } 204 } 205 206 /** 207 * This indexer returns -1 for coordinates outside the image bounds, indicating that a (predefined) default pixel value 208 * should be used. There is no public constructor. To instantiate use method 209 * {@link GridIndexer2D#create(int, int, OutOfBoundsStrategy)} with {@link OutOfBoundsStrategy#DefaultValue}. 210 */ 211 public static class DefaultValueIndexer extends GridIndexer2D { 212 213 DefaultValueIndexer(int width, int height) { 214 super(width, height, OutOfBoundsStrategy.DefaultValue); 215 } 216 217 @Override 218 public int getIndex(int u, int v) { 219 if (u < 0 || u >= width || v < 0 || v >= height) { 220 return -1; 221 } 222 else { 223 return getWithinBoundsIndex(u, v); 224 } 225 } 226 } 227 228 /** 229 * This indexer throws an exception if coordinates outside image bounds are accessed. There is no public 230 * constructor. To instantiate use method {@link GridIndexer2D#create(int, int, OutOfBoundsStrategy)} with 231 * {@link OutOfBoundsStrategy#ThrowException}. 232 */ 233 public static class ExceptionIndexer extends GridIndexer2D { 234 235 ExceptionIndexer(int width, int height) { 236 super(width, height, OutOfBoundsStrategy.ThrowException); 237 } 238 239 @Override 240 public int getIndex(int u, int v) throws OutOfImageException { 241 if (u < 0 || u >= width || v < 0 || v >= height) { 242 throw new OutOfImageException( 243 String.format("out-of-image position [%d,%d]", u, v)); 244 } 245 else 246 return getWithinBoundsIndex(u, v); 247 } 248 } 249 250 // ----------------------------------------------------------- 251 252 /** 253 * Exception to be thrown by {@link ExceptionIndexer}. 254 */ 255 public static class OutOfImageException extends RuntimeException { 256 private static final long serialVersionUID = 1L; 257 258 public OutOfImageException(String message) { 259 super(message); 260 } 261 } 262 263} 264 265