001/* 002 * GeoAPI - Java interfaces for OGC/ISO standards 003 * Copyright © 2012-2024 Open Geospatial Consortium, Inc. 004 * http://www.geoapi.org 005 * 006 * Licensed under the Apache License, Version 2.0 (the "License"); 007 * you may not use this file except in compliance with the License. 008 * You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018package org.opengis.test.coverage.image; 019 020import java.awt.Rectangle; 021import java.awt.image.DataBuffer; 022import java.awt.image.Raster; 023import java.awt.image.RenderedImage; 024import java.awt.image.SampleModel; 025 026import static java.lang.Double.doubleToLongBits; 027import static java.lang.Float.floatToIntBits; 028import static java.lang.StrictMath.*; 029import static org.junit.jupiter.api.Assertions.*; 030 031 032/** 033 * A row-major iterator over sample values in a {@link Raster} or {@link RenderedImage}. 034 * For any image (tiled or not), this class iterates first over the <em>bands</em>, then 035 * over the <em>columns</em> and finally over the <em>rows</em>. If the image is tiled, 036 * then this iterator will perform the necessary calls to the {@link RenderedImage#getTile(int, int)} 037 * method for each row in order to perform the iteration as if the image was untiled. 038 * 039 * <p>On creation, this iterator is positioned <em>before</em> the first sample value. 040 * To use this iterator, invoke the {@link #next()} method in a {@code while} loop 041 * as below:</p> 042 * 043 * {@snippet lang="java" : 044 * PixelIterator it = new PixelIterator(image); 045 * while (it.next()) { 046 * float value = it.getSampleFloat(); 047 * // Do some processing with the value here... 048 * }} 049 * 050 * @see org.opengis.test.Assertions#assertSampleValuesEqual(RenderedImage, RenderedImage, double, String) 051 * 052 * @author Rémi Marechal (Geomatys) 053 * @author Martin Desruisseaux (Geomatys) 054 * @version 3.1 055 * @since 3.1 056 */ 057@SuppressWarnings("strictfp") // Because we still target Java 11. 058public strictfp class PixelIterator { 059 /** 060 * The image in which to iterate. 061 */ 062 private final RenderedImage image; 063 064 /** 065 * Current raster in which to iterate. 066 */ 067 private Raster raster; 068 069 /** 070 * The bands to iterate over, or {@code null} if none. 071 */ 072 private final int[] sourceBands; 073 074 /** 075 * The subsampling to apply during the iteration. 076 */ 077 private final int xSubsampling, ySubsampling; 078 079 /** 080 * Number of bands to iterate over. 081 */ 082 private final int numBands; 083 084 /** 085 * The iteration bounds in the image, in pixel coordinates. 086 * This rectangle may span an arbitrary number of tiles. 087 */ 088 private final int minX, maxX, maxY; 089 090 /** 091 * The iteration bounds in the image, in tile coordinates. 092 */ 093 private final int minTileX, maxTileX, maxTileY; 094 095 /** 096 * The intersection of the iteration bounds together with the current 097 * {@linkplain #raster} bounds. 098 */ 099 private int currentMaxX, currentMaxY; 100 101 /** 102 * Current band and pixel position in the current {@linkplain #raster}. 103 */ 104 private int band, x, y; 105 106 /** 107 * Current raster position in the {@linkplain #image}. 108 */ 109 private int tileX, tileY; 110 111 /** 112 * Creates an iterator for the whole area of the given raster. 113 * 114 * @param raster The raster for which to create an iterator. 115 */ 116 public PixelIterator(final Raster raster) { 117 this(new RasterImage(raster)); 118 } 119 120 /** 121 * Creates an iterator for the whole area of the given image. 122 * 123 * @param image The image for which to create an iterator. 124 */ 125 public PixelIterator(final RenderedImage image) { 126 this(image, null, 1, 1, null); 127 } 128 129 /** 130 * Creates an iterator for a sub-area of the given raster. 131 * 132 * @param raster the raster to iterate over. 133 * @param subArea rectangle which represent raster sub area iteration, or {@code null} if none. 134 * @param xSubsampling the iteration step when moving to the next pixel. 135 * @param ySubsampling the iteration step when moving to the next scan line. 136 * @param sourceBands the source bands, or {@code null} if none. 137 */ 138 public PixelIterator(final Raster raster, final Rectangle subArea, 139 final int xSubsampling, final int ySubsampling, final int[] sourceBands) 140 { 141 this(new RasterImage(raster), subArea, xSubsampling, ySubsampling, sourceBands); 142 } 143 144 /** 145 * Creates an iterator for a sub-area of the given image. 146 * 147 * @param image the image to iterate over. 148 * @param subArea rectangle which represent image sub area iteration, or {@code null} if none. 149 * @param xSubsampling the iteration step when moving to the next pixel. 150 * @param ySubsampling the iteration step when moving to the next scan line. 151 * @param sourceBands the source bands, or {@code null} if none. 152 */ 153 public PixelIterator(final RenderedImage image, final Rectangle subArea, 154 final int xSubsampling, final int ySubsampling, final int[] sourceBands) 155 { 156 this.image = image; 157 this.numBands = (sourceBands != null) ? sourceBands.length : image.getSampleModel().getNumBands(); 158 this.sourceBands = sourceBands; 159 this.xSubsampling = xSubsampling; 160 this.ySubsampling = ySubsampling; 161 162 int minX = image.getMinX(); 163 int minY = image.getMinY(); 164 int maxX = image.getWidth() + minX; 165 int maxY = image.getHeight() + minY; 166 if (subArea != null) { 167 minX = max(minX, subArea.x); 168 minY = max(minY, subArea.y); 169 maxX = min(maxX, subArea.x + subArea.width); 170 maxY = min(maxY, subArea.y + subArea.height); 171 } 172 this.minX = minX; 173 this.maxX = maxX; 174 this.maxY = maxY; 175 176 final int gridXOffset = image.getTileGridXOffset(); 177 final int gridYOffset = image.getTileGridYOffset(); 178 final int tileWidth = image.getTileWidth(); 179 final int tileHeight = image.getTileHeight(); 180 181 final int minTileY; 182 minTileX = divide(minX - gridXOffset, tileWidth, false); 183 minTileY = divide(minY - gridYOffset, tileHeight, false); 184 maxTileX = divide(maxX - gridXOffset, tileWidth, true); 185 maxTileY = divide(maxY - gridYOffset, tileHeight, true); 186 187 // Initialize attributes to first iteration. 188 x = minX; 189 y = minY; 190 band = -1; 191 tileX = minTileX; 192 tileY = minTileY; 193 updateRaster(); 194 } 195 196 /** 197 * Rounds the given numbers, rounding toward floor or ceil depending on the value 198 * of the {@code ceil} argument. This method works for negative numerator too. 199 * 200 * @param numerator the value to divide. 201 * @param denominator the divisor. 202 * @param ceil whether to round toward up. 203 * @return division result rounded toward the specified direction. 204 */ 205 private static int divide(final int numerator, final int denominator, final boolean ceil) { 206 assertTrue(denominator > 0, "Require a non-negative denominator."); 207 int div = numerator / denominator; 208 if (ceil) { 209 if (numerator > 0 && (numerator % denominator) != 0) { 210 div++; 211 } 212 } else { 213 if (numerator < 0 && (numerator % denominator) != 0) { 214 div--; 215 } 216 } 217 return div; 218 } 219 220 /** 221 * Updates the {@linkplain #raster} and related fields for the current 222 * {@link #tileX} and {@link #tileY} values. 223 */ 224 private void updateRaster() { 225 raster = image.getTile(tileX, tileY); 226 currentMaxX = min(maxX, raster.getMinX() + raster.getWidth()); 227 currentMaxY = min(maxY, raster.getMinY() + raster.getHeight()); 228 } 229 230 /** 231 * Moves to the next sample values and returns {@code true} if the iteration has more pixels. 232 * 233 * @return {@code true} if the next sample value exist. 234 */ 235 public boolean next() { 236 if (++band == numBands) { 237 if ((x += xSubsampling) >= currentMaxX) { 238 int nextTile = tileX + 1; // Needed only when the iteration stops before the maxX of the last tile in a row. 239 tileX = divide(x - image.getTileGridXOffset(), image.getTileWidth(), false); 240 if (max(nextTile, tileX) >= maxTileX) { 241 if ((y += ySubsampling) >= currentMaxY) { 242 nextTile = tileY + 1; // Needed only when the iteration stops before the maxY of the last row of tiles. 243 tileY = divide(y - image.getTileGridYOffset(), image.getTileHeight(), false); 244 if (max(nextTile, tileY) >= maxTileY) { 245 return false; 246 } 247 } 248 x = minX; 249 tileX = minTileX; 250 } 251 updateRaster(); 252 } 253 band = 0; 254 } 255 return true; 256 } 257 258 /** 259 * Returns the current <var>x</var> coordinate. The coordinate values range from 260 * {@linkplain RenderedImage#getMinX() image X minimum} (inclusive) to that minimum 261 * plus the {@linkplain RenderedImage#getWidth() image width} (exclusive). 262 * 263 * @return the current <var>x</var> coordinate. 264 * 265 * @see RenderedImage#getMinX() 266 * @see RenderedImage#getWidth() 267 */ 268 public int getX() { 269 return x; 270 } 271 272 /** 273 * Returns the current <var>y</var> coordinate. The coordinate values range from 274 * {@linkplain RenderedImage#getMinY() image Y minimum} (inclusive) to that minimum 275 * plus the {@linkplain RenderedImage#getHeight() image height} (exclusive). 276 * 277 * @return the current <var>y</var> coordinate. 278 * 279 * @see RenderedImage#getMinY() 280 * @see RenderedImage#getHeight() 281 */ 282 public int getY() { 283 return y; 284 } 285 286 /** 287 * Returns the current band index. The index values range from 0 (inclusive) to 288 * the {@linkplain SampleModel#getNumBands() number of bands} (exclusive), or to 289 * the {@code sourceBands} array length (exclusive) if the array given to the 290 * constructor was non-null. 291 * 292 * @return the current band index. 293 * 294 * @see SampleModel#getNumBands() 295 */ 296 public int getBand() { 297 return (sourceBands != null) ? sourceBands[band] : band; 298 } 299 300 /** 301 * Returns the type of the sample values, as one of the {@code TYPE_*} constants 302 * defined in the {@link DataBuffer} class. 303 * 304 * @return the type of the sample values. 305 * 306 * @see SampleModel#getDataType() 307 * @see DataBuffer#TYPE_BYTE 308 * @see DataBuffer#TYPE_SHORT 309 * @see DataBuffer#TYPE_USHORT 310 * @see DataBuffer#TYPE_INT 311 * @see DataBuffer#TYPE_FLOAT 312 * @see DataBuffer#TYPE_DOUBLE 313 */ 314 public int getDataType() { 315 return image.getSampleModel().getDataType(); 316 } 317 318 /** 319 * Returns the sample value at the current position, as an integer. 320 * This method is appropriate for the 321 * {@linkplain DataBuffer#TYPE_BYTE byte}, 322 * {@linkplain DataBuffer#TYPE_SHORT short}, 323 * {@linkplain DataBuffer#TYPE_USHORT unsigned short} and 324 * {@linkplain DataBuffer#TYPE_INT integer} 325 * {@linkplain #getDataType() datatypes}. 326 * 327 * @return the sample value at the current position. 328 * 329 * @see Raster#getSample(int, int, int) 330 * @see DataBuffer#TYPE_BYTE 331 * @see DataBuffer#TYPE_SHORT 332 * @see DataBuffer#TYPE_USHORT 333 * @see DataBuffer#TYPE_INT 334 */ 335 public int getSample() { 336 return raster.getSample(x, y, getBand()); 337 } 338 339 /** 340 * Returns the sample value at the current position, as a floating point number. 341 * 342 * @return the sample value at the current position. 343 * 344 * @see Raster#getSampleFloat(int, int, int) 345 * @see DataBuffer#TYPE_FLOAT 346 */ 347 public float getSampleFloat() { 348 return raster.getSampleFloat(x, y, getBand()); 349 } 350 351 /** 352 * Returns the sample value at the current position, as a double-precision floating point number. 353 * 354 * @return the sample value at the current position. 355 * 356 * @see Raster#getSampleDouble(int, int, int) 357 * @see DataBuffer#TYPE_DOUBLE 358 */ 359 public double getSampleDouble() { 360 return raster.getSampleDouble(x, y, getBand()); 361 } 362 363 /** 364 * Compares all sample values iterated by this {@code PixelIterator} with the sample values 365 * iterated by the given iterator. If a mismatch is found, then an {@link AssertionError} is 366 * thrown with a detailed error message. 367 * 368 * <p>This method does not verify the image sizes, number of tiles, number of bands, color 369 * model or datatype. Consequently, this method is robust to the following differences:</p> 370 * 371 * <ul> 372 * <li>Differences in the ({@linkplain RenderedImage#getMinX() x}, 373 * {@linkplain RenderedImage#getMinY() y}) origin;</li> 374 * <li>Differences in tile layout (images are compared as if they were untiled);</li> 375 * <li>Differences in the datatype (values are compared using the widest of this iterator 376 * {@linkplain #getDataType() datatype} and the datatype of the given iterator).</li> 377 * </ul> 378 * 379 * If the images have different sizes, then an <q>Unexpected end of iteration</q> 380 * exception will be thrown when the first iterator reaches the iteration end. 381 * 382 * @param actual the iterator that contains the actual values to be compared with the "expected" sample values. 383 * @param tolerance the tolerance threshold for floating point comparison. This threshold does not apply to integer types. 384 * @throws AssertionError if a value in this iterator is not equals to a value in the given iterator with the given 385 * tolerance threshold. 386 */ 387 public void assertSampleValuesEqual(final PixelIterator actual, final double tolerance) throws AssertionError { 388 final int dataType = Math.max(getDataType(), actual.getDataType()); 389 while (next()) { 390 assertTrue(actual.next(), "Unexpected end of pixel iteration."); 391 switch (dataType) { 392 case DataBuffer.TYPE_DOUBLE: { 393 final double a = actual.getSampleDouble(); 394 final double e = this. getSampleDouble(); 395 if (doubleToLongBits(a) == doubleToLongBits(e)) { 396 continue; // All variants of NaN values are considered equal. 397 } 398 if (abs(a-e) <= tolerance) { 399 continue; // Negative and positive zeros are considered equal. 400 } 401 break; 402 } 403 case DataBuffer.TYPE_FLOAT: { 404 final float a = actual.getSampleFloat(); 405 final float e = this. getSampleFloat(); 406 if (floatToIntBits(a) == floatToIntBits(e)) { 407 continue; // All variants of NaN values are considered equal. 408 } 409 if (abs(a-e) <= tolerance) { 410 continue; // Negative and positive zeros are considered equal. 411 } 412 break; 413 } 414 default: { 415 if (actual.getSample() == getSample()) continue; 416 break; 417 } 418 } 419 /* 420 * Remainder of this block is for formatting the error message. 421 */ 422 final Number ev, av; 423 switch (dataType) { 424 case DataBuffer.TYPE_DOUBLE: ev = getSampleDouble(); av = actual.getSampleDouble(); break; 425 case DataBuffer.TYPE_FLOAT: ev = getSampleFloat(); av = actual.getSampleFloat(); break; 426 default: ev = getSample(); av = actual.getSample(); break; 427 } 428 final String lineSeparator = System.getProperty("line.separator", "\n"); 429 final StringBuilder buffer = new StringBuilder(1024); 430 buffer.append("Mismatched sample value: expected ").append(ev).append(" but got ").append(av).append(lineSeparator); 431 buffer.append("Pixel coordinate in the complete image: "); position(buffer); buffer.append(lineSeparator); 432 buffer.append("Pixel coordinate in the compared image: "); actual.position(buffer); buffer.append(lineSeparator); 433 actual.completeComparisonFailureMessage(buffer, lineSeparator); 434 fail(buffer.toString()); 435 } 436 assertFalse(actual.next(), "Expected end of pixel iteration, but found more values."); 437 } 438 439 /** 440 * Invoked when a sample value mismatch has been found, for allowing {@link PixelIteratorForIO} 441 * to append to the error message the I/O parameters used for the reading or writing process. 442 * 443 * @param buffer buffer where to write the message. 444 * @param lineSeparator value of {@link System#lineSeparator()}. 445 */ 446 void completeComparisonFailureMessage(final StringBuilder buffer, final String lineSeparator) { 447 } 448 449 /** 450 * Formats the current position of this iterator in the given buffer. 451 * 452 * @param buffer buffer where to write the position. 453 */ 454 private void position(final StringBuilder buffer) { 455 buffer.append('(').append(getX()).append(", ").append(getY()).append(") band ").append(getBand()); 456 } 457 458 /** 459 * Returns a string representation of this iterator position for debugging purpose. 460 * 461 * @return a string representation if this iterator position. 462 */ 463 @Override 464 public String toString() { 465 final StringBuilder buffer = new StringBuilder(48); 466 position(buffer.append(getClass().getSimpleName()).append('[')); 467 return buffer.append(']').toString(); 468 } 469}