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