You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2021/06/08 10:45:19 UTC

[sis] branch GeoTIFF updated: Bug fix. Testing the reading of random sub-regions now pass.

This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a commit to branch GeoTIFF
in repository https://gitbox.apache.org/repos/asf/sis.git


The following commit(s) were added to refs/heads/GeoTIFF by this push:
     new e1bd0d9  Bug fix. Testing the reading of random sub-regions now pass.
e1bd0d9 is described below

commit e1bd0d9503b8b241663cea34a4517e868992f37e
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Tue Jun 8 12:44:53 2021 +0200

    Bug fix. Testing the reading of random sub-regions now pass.
---
 .../java/org/apache/sis/image/PlanarImage.java     |   8 +-
 .../sis/internal/coverage/j2d/TiledImage.java      |  37 ++-
 .../org/apache/sis/storage/geotiff/DataCube.java   |  10 +-
 .../org/apache/sis/storage/geotiff/DataSubset.java | 270 ++++++++++++---------
 .../apache/sis/storage/geotiff/PixelDecoder.java   |  41 ++--
 .../sis/test/storage/CoverageReadConsistency.java  |  12 +-
 6 files changed, 224 insertions(+), 154 deletions(-)

diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/PlanarImage.java b/core/sis-feature/src/main/java/org/apache/sis/image/PlanarImage.java
index 397dec2..0dd5392 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/PlanarImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/PlanarImage.java
@@ -551,17 +551,17 @@ public abstract class PlanarImage implements RenderedImage {
             if (sm.getHeight() < tileHeight) return "tileHeight";
         }
         long size = getWidth();
-        long remainder = size - JDK9.multiplyFull(getNumXTiles(), tileWidth);
+        long remainder = JDK9.multiplyFull(getNumXTiles(), tileWidth) - size;
         if (remainder != 0) {
             return (remainder >= 0 && remainder < size) ? "width" : "numXTiles";
         }
         size = getHeight();
-        remainder = size - JDK9.multiplyFull(getNumYTiles(), tileHeight);
+        remainder = JDK9.multiplyFull(getNumYTiles(), tileHeight) - size;
         if (remainder != 0) {
             return (remainder >= 0 && remainder < size) ? "height" : "numYTiles";
         }
-        if (JDK9.multiplyFull(getMinTileX(),  tileWidth)  + getTileGridXOffset() != getMinX()) return "tileX";
-        if (JDK9.multiplyFull(getMinTileY(),  tileHeight) + getTileGridYOffset() != getMinY()) return "tileY";
+        if (JDK9.multiplyFull(getMinTileX(), tileWidth)  + getTileGridXOffset() != getMinX()) return "tileX";
+        if (JDK9.multiplyFull(getMinTileY(), tileHeight) + getTileGridYOffset() != getMinY()) return "tileY";
         return null;
     }
 
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/TiledImage.java b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/TiledImage.java
index 8cce878..abecce8 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/TiledImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/TiledImage.java
@@ -80,8 +80,7 @@ public class TiledImage extends PlanarImage {
      * @param height      number of pixels along Y axis in the whole rendered image.
      * @param minTileX    minimum tile index in the X direction.
      * @param minTileY    minimum tile index in the Y direction.
-     * @param tiles       the tiles. Must contains at least one element.
-     *                    This array is not cloned.
+     * @param tiles       the tiles. Must contains at least one element. This array is not cloned.
      */
     public TiledImage(final Map<String,Object> properties, final ColorModel colorModel,
                       final int width, final int height, final int minTileX, final int minTileY,
@@ -100,6 +99,40 @@ public class TiledImage extends PlanarImage {
     }
 
     /**
+     * Verifies whether image layout information and tile coordinates are consistent.
+     * This method verifies the size and minimum pixel coordinates of all tiles,
+     * in addition to the verifications documented in the super-class.
+     *
+     * @return {@code null} if image layout information are consistent,
+     *         or the name of inconsistent attribute if a problem is found.
+     */
+    @Override
+    public String verify() {
+        final int minX       = getMinX();
+        final int minY       = getMinY();
+        final int numXTiles  = getNumXTiles();
+        final int tileWidth  = getTileWidth();
+        final int tileHeight = getTileHeight();
+        for (int i=0; i < tiles.length; i++) {
+            final Raster tile = tiles[i];
+            final int tx = i % numXTiles;
+            final int ty = i / numXTiles;
+            if (tile.getWidth()  != tileWidth)              return property(tx, ty, "width");
+            if (tile.getHeight() != tileHeight)             return property(tx, ty, "height");
+            if (tile.getMinX()   != tileWidth  * tx + minX) return property(tx, ty, "x");
+            if (tile.getMinY()   != tileHeight * ty + minY) return property(tx, ty, "y");
+        }
+        return super.verify();
+    }
+
+    /**
+     * Label returned by {@link #verify()} for identifying an error in a specified tile.
+     */
+    private static String property(final int tx, final int ty, final String name) {
+        return "tiles[" + tx + ", " + ty + "]." + name;
+    }
+
+    /**
      * Returns the color model, or {@code null} if none.
      */
     @Override
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataCube.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataCube.java
index a709d6a..649b0ec 100644
--- a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataCube.java
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataCube.java
@@ -62,7 +62,9 @@ abstract class DataCube extends AbstractGridResource implements ResourceOnFileSy
      * All tiles loaded by any {@link GridCoverage} created from this resource.
      * Keys are the same indices than the ones used by {@link #getTileArrayInfo()}.
      * For each value, the {@link WritableRaster#getMinX()} and {@code minY} values
-     * shall be the pixel coordinates of the raster upper-left corner relative to the full image.
+     * can be anything, depending which {@link DataSubset} was first to load the tile.
+     *
+     * @see DataSubset#rasters
      */
     private final WeakValueHashMap<Integer,WritableRaster> rasters;
 
@@ -170,7 +172,7 @@ abstract class DataCube extends AbstractGridResource implements ResourceOnFileSy
         final GridExtent     fullSize     = gridGeometry.getExtent();
         boolean              sharedCache  = true;
         final int[]          subsamplings;
-        GridExtent           areaOfInterest;
+        final GridExtent     areaOfInterest;
         if (domain == null) {
             domain         = gridGeometry;
             areaOfInterest = fullSize;
@@ -181,8 +183,8 @@ abstract class DataCube extends AbstractGridResource implements ResourceOnFileSy
              * If an area of interest has been specified, we may need to expand it to an integer amount of tiles.
              * But we do not need to do that if the image is untiled; it is okay to read only a sub-region of the
              * single tile. We disable the "integer amount of tiles" restriction by setting the tile size to 1.
-             * Note that we can disable this restriction in only one dimension, typically the X one when the TIFF
-             * image use strips instead of tiles.
+             * Note that it is possible to disable this restriction in a single dimension, typically the X one
+             * when the TIFF image use strips instead of tiles.
              */
             int tileWidth  = sampleModel.getWidth();
             int tileHeight = sampleModel.getHeight();
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataSubset.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataSubset.java
index 250cebf..308869f 100644
--- a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataSubset.java
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataSubset.java
@@ -56,6 +56,12 @@ import org.opengis.coverage.CannotEvaluateException;
  */
 final class DataSubset extends GridCoverage {
     /**
+     * Number of dimensions in a rendered image.
+     * Used for identifying codes where a two-dimensional slice is assumed.
+     */
+    static final int BIDIMENSIONAL = 2;
+
+    /**
      * The resource which contain this {@code DataSubset}.
      * Used for fetching information like the input channel and where to report warnings.
      */
@@ -83,7 +89,7 @@ final class DataSubset extends GridCoverage {
     /**
      * Number of tiles along each dimension, inside the domain specified by the user.
      * The array length is 2 for a standard TIFF file, but this class accepts a higher number of
-     * dimensions if 3- or 4-dimensional data are stored in a GeoTIFF file using some convention.
+     * dimensions if 3- or 4-dimensional data are stored in a TIFF file using some convention.
      * This is the number of tiles in the domain specified at construction time, not necessarily
      * the number of tiles of the image stored in the TIFF file. Tile indices starts at (0, 0, …),
      * not at the indices of the corresponding tile in the TIFF file, for avoiding integer overflow.
@@ -103,31 +109,38 @@ final class DataSubset extends GridCoverage {
     private final int indexOfFirstTile;
 
     /**
-     * Tile coordinates of the tile in the upper-left corner of this {@code DataSubset}.
-     * The upper-left corner of the image stored in the TIFF file has coordinates (0,0).
+     * Pixel coordinates of the upper-left corner of the first tile in this {@code DataSubset}.
+     * Those coordinates are relative to the the upper-left corner of the image stored in the
+     * TIFF file, which has coordinates (0,0) by definition. This is the origin of this subset.
      */
-    private final int firstTileX, firstTileY;
+    private final long[] origin;
 
     /**
      * The area to read in unit of image pixels (before subsampling).
      * Lower values are inclusive and upper values are exclusive.
+     * Coordinates are relative to this {@code DataSubset}, i.e. coordinates (0,0) are at the position
+     * identified by the lower coordinates of the {@code areaOfInterest} argument given to constructor.
+     * Note that {@code areaOfInterest} has usually been rounded to integer amount of tile size.
      *
-     * <p>In the usual case where tiles are read fully, {@code lowerX|Y} are (0,0) and {@code upperX|Y} are
+     * <p>In the usual case where tiles are read fully, {@code lowers} are (0,0) and {@code uppers} are
      * multiples of the tile size. But those values may identify a smaller region if this {@link DataSubset}
      * contains only one (potentially large) tile along at least one dimension. In such case, those values
      * allow to read only a sub-region of tiles.</p>
      */
-    private final long lowerX, lowerY, upperX, upperY;
+    private final long[] relativeLower, relativeUpper;
 
     /**
-     * The method for transferring pixel values from GeoTIFF file to this {@code Raster}.
+     * The method for transferring pixel values from TIFF file to this {@code Raster}.
      * There is different subtypes depending on decompression methods.
      */
     private final PixelDecoder decoder;
 
     /**
      * Cache of rasters read by this {@code DataSubset}. This cache may be shared with other {@code DataSubset}
-     * instances created for the same {@link DataCube} resource.
+     * instances created for the same {@link DataCube} resource. For each value, the raster {@code minX} and
+     * {@code minY} values can be anything, depending which {@code DataSubset} was first to load the tile.
+     *
+     * @see DataCube#rasters
      */
     private final Map<Integer,WritableRaster> rasters;
 
@@ -136,7 +149,7 @@ final class DataSubset extends GridCoverage {
      * by {@link ImageFileDirectory#validateMandatoryTags()} before this call.
      *
      * @param  owner           the resource which contain this {@code DataSubset}.
-     * @param  fullSize        the full size of the GeoTIFF image.
+     * @param  fullSize        the full size of the TIFF image.
      * @param  areaOfInterest  the area to read in unit of the full size image (without subsampling).
      * @param  domain          the sub-region extent, CRS and conversion from cell indices to CRS.
      * @param  ranges          sample dimensions for each image band.
@@ -162,8 +175,8 @@ final class DataSubset extends GridCoverage {
         tileStrides = new int[dimension];
         tileStrides[0] = 1;
         for (int i=0; i < dimension - 1; i++) {
-            final int numTile = Math.toIntExact(Numerics.ceilDiv(fullSize.getSize(i), decoder.getChunkSize(i)));
-            tileStrides[i+1] = Math.multiplyExact(numTile, tileStrides[i]);
+            final int tileCount = Math.toIntExact(Numerics.ceilDiv(fullSize.getSize(i), decoder.getChunkSize(i)));
+            tileStrides[i+1]  = Math.multiplyExact(tileCount, tileStrides[i]);
         }
         /*
          * If a sub-domain is requested, modify the tile indices in order to include only
@@ -171,32 +184,25 @@ final class DataSubset extends GridCoverage {
          * as the indices of the first tile, we need to have that tile at index 0 in the
          * `tileOffsets` and `tileByteCounts` vectors.
          */
-        numTiles = new int[dimension];
-        final long[] relativeLows  = new long[dimension];
-        final long[] relativeHighs = new long[dimension];
-        final long[] firstTilePos  = new long[dimension];
+        numTiles      = new int [dimension];
+        relativeLower = new long[dimension];        // Coordinates of first pixel relative to request.
+        relativeUpper = new long[dimension];        // Relative coordinates of last pixel, exclusive.
+        origin        = new long[dimension];        // Minimal pixel coordinates of first requested tile.
         long indexOfFirstTile = 0;
         for (int i=0; i<dimension; i++) {
-            final long low      = areaOfInterest.getLow(i);                 // Always ≥ 0.
-            final long high     = areaOfInterest.getHigh(i);                // Inclusive
-            final int  size     = decoder.getChunkSize(i);
-            final long tileLow  = low  / size;                              // Always ≥ 0.
-            final long tileHigh = high / size;                              // Inclusive.
-            final int  tileSpan = Math.toIntExact(tileHigh - tileLow);      // Number of tiles minus 1.
-            numTiles[i]         = Math.incrementExact(tileSpan);
-            relativeLows [i]    = (low  % size);
-            relativeHighs[i]    = (high % size) + (tileSpan * size);
-            firstTilePos [i]    = tileLow;
-            indexOfFirstTile    = Math.addExact(Math.multiplyExact(tileLow, tileStrides[i]), indexOfFirstTile);
+            final long low       = areaOfInterest.getLow(i);                // Always ≥ 0.
+            final long high      = areaOfInterest.getHigh(i);               // Inclusive
+            final int  tileSize  = decoder.getChunkSize(i);
+            final long tileLow   = low  / tileSize;                         // Always ≥ 0.
+            final long tileHigh  = high / tileSize;                         // Inclusive.
+            final int  tileCount = Math.toIntExact(tileHigh - tileLow);     // Number of tiles minus 1.
+            numTiles     [i]     = Math.incrementExact(tileCount);
+            relativeLower[i]     = (low % tileSize);
+            relativeUpper[i]     = Math.incrementExact((high % tileSize) + JDK9.multiplyFull(tileCount, tileSize));
+            origin       [i]     = Math.multiplyExact(tileLow, tileSize);
+            indexOfFirstTile     = Math.addExact(Math.multiplyExact(tileLow, tileStrides[i]), indexOfFirstTile);
         }
         this.indexOfFirstTile = Math.toIntExact(indexOfFirstTile);
-        lowerX = relativeLows[0];
-        lowerY = relativeLows[1];
-        upperX = Math.incrementExact(relativeHighs[0]);
-        upperY = Math.incrementExact(relativeHighs[1]);
-        firstTileX = Math.toIntExact(firstTilePos [0]);
-        firstTileY = Math.toIntExact(firstTilePos [1]);
-        // Values in higher dimensions are not needed.
     }
 
     /**
@@ -205,11 +211,11 @@ final class DataSubset extends GridCoverage {
      */
     private static final class Tile implements Comparable<Tile> {
         /**
-         * Index of the tile in the two first dimensions, relative to the enclosing {@link DataSubset}.
-         * Tile (0,0) is the tile in the upper-left corner of this {@link DataSubset}, not necessarily
-         * the tile in the upper-left corner of the image in the TIFF file.
+         * Tile Matrix Coordinates (MC) in the two first dimensions, relative to the enclosing {@link DataSubset}.
+         * Tile (0,0) is the tile in the upper-left corner of this {@link DataSubset}, not necessarily the tile in
+         * the upper-left corner of the image in the TIFF file.
          */
-        final int xInSubset, yInSubset;
+        final int[] tileInSubset;
 
         /**
          * Index of this tile in the query answered by {@link DataSubset#readTiles(int[], int[])}.
@@ -234,15 +240,21 @@ final class DataSubset extends GridCoverage {
         final long byteCount;
 
         /**
+         * Pixel coordinates (relative to the {@link DataSubset}) of the upper-left corner of the tile.
+         */
+        final int minX, minY;
+
+        /**
          * Stores information about a tile to be loaded.
          */
-        Tile(final DataSubset ds, final int[] tc, final int indexInQuery, final int indexInSource) {
-            this.xInSubset     = tc[0];
-            this.yInSubset     = tc[1];
+        Tile(final DataSubset ds, final int[] tc, final int indexInQuery, final int indexInSource, final int[] pixelCoord) {
+            this.tileInSubset  = tc.clone();
             this.indexInQuery  = indexInQuery;
             this.indexInSource = indexInSource;
             this.byteOffset    = ds.tileOffsets.longValue(indexInSource);
             this.byteCount     = ds.tileByteCounts.longValue(indexInSource);
+            this.minX          = pixelCoord[0];
+            this.minY          = pixelCoord[1];
         }
 
         /**
@@ -256,35 +268,37 @@ final class DataSubset extends GridCoverage {
     /**
      * Returns all tiles in the given ranges of tile indices. Given indices are relative to this {@code DataSubset}!
      * (0,0) is the tile in the upper-left corner of this {@code DataSubset} (not necessarily the upper-left corner
-     * of the image stored in the TIFF file). However the {@link WritableRaster#getMinX()} and {@code getMinY()}
-     * pixel coordinates are relative to the full image; callers will generally need to apply translations on the
-     * tiles returned by this method.
+     * of the image stored in the TIFF file). The {@link WritableRaster#getMinX()} and {@code getMinY()} coordinates
+     * of returned raster will start at the given {@code offsetAOI} values.
      *
      * <p>This method is thread-safe.</p>
      *
-     * @param  lower  indices (relative to this {@code DataSubset} of the upper-left tile to read.
-     * @param  upper  indices (relative to this {@code DataSubset} after the bottom-right tile to read.
-     * @return tiles decoded from the GeoTIFF file.
+     * @param  tileLower  indices (relative to this {@code DataSubset}) of the upper-left tile to read.
+     * @param  tileUpper  indices (relative to this {@code DataSubset}) after the bottom-right tile to read.
+     * @param  offsetAOI  pixel coordinates to assign to the upper-left corner of the region to read.
+     * @return tiles decoded from the TIFF file.
      * @throws IOException if an I/O error occurred.
      * @throws DataStoreContentException if the sample model is not supported.
      * @throws RuntimeException if the Java2D image can not be created for another reason
      *         (too many exception types to list them all).
      */
-    private WritableRaster[] readTiles(final int[] lower, final int[] upper) throws IOException, DataStoreContentException {
+    private WritableRaster[] readTiles(final int[] tileLower, final int[] tileUpper, final int[] offsetAOI)
+            throws IOException, DataStoreContentException
+    {
         /*
-         * Convert the `lower` coordinates to index in the `tileOffsets` and `tileByteCounts` vectors.
+         * Convert the `tileLower` coordinates to index in the `tileOffsets` and `tileByteCounts` vectors.
          * This loop does argument validation in same time.
          */
         int indexInSource = indexOfFirstTile;
         int countInQuery  = 1;
         for (int i=0; i < tileStrides.length; i++) {
-            final int lo = lower[i];
-            final int up = upper[i];
-            final int n  = numTiles[i];
-            ArgumentChecks.ensureBetween("lower", 0, n-1, lo);
-            ArgumentChecks.ensureBetween("upper", lo+1, n, up);
-            indexInSource = Math.addExact(indexInSource, Math.multiplyExact(tileStrides[i], lo));
-            countInQuery  = Math.multiplyExact(countInQuery, up - lo);
+            final int tlo = tileLower[i];
+            final int tup = tileUpper[i];
+            final int n   = numTiles [i];
+            ArgumentChecks.ensureBetween("tileLower", 0, n-1, tlo);
+            ArgumentChecks.ensureBetween("tileUpper", tlo+1, n, tup);
+            indexInSource = Math.addExact(indexInSource, Math.multiplyExact(tileStrides[i], tlo));
+            countInQuery  = Math.multiplyExact(countInQuery, tup - tlo);
         }
         /*
          * Prepare an array for all tiles to be returned. Tiles that are already in memory will be stored
@@ -293,24 +307,40 @@ final class DataSubset extends GridCoverage {
         final WritableRaster[] result = new WritableRaster[countInQuery];
         final Tile[] missings = new Tile[countInQuery];
         int numMissings = 0;
-        final int[] tc = lower.clone();
+        final int[] tc = tileLower.clone();     // Tile matrix coordinates relative to this subset.
+        final int[] pc = offsetAOI.clone();     // Pixel coordinates relative to Area Of Interest.
         synchronized (owner.reader.store) {
             /*
-             * Iterate over all tiles in the region specified to this method. This loop maintains 3 indices:
+             * Iterate over all tiles in the region specified to this method. This loop maintains 4 indices:
              *
              *   - `indexInQuery`  is the index in the `rasters` array to be returned.
              *   - `indexInSource` is the corresponding index in the `tileOffsets` vector.
-             *   - `tile`          contains the tile indices relative to this `DataSubset`.
+             *   - `tc[]`          contains the tile matrix coordinates relative to this `DataSubset`.
+             *   - `pc[]`          contains the oixel coordinates relative to the user-specified AOI.
              */
             for (int indexInQuery = 0; indexInQuery < countInQuery; indexInQuery++) {
-                if ((result[indexInQuery] = rasters.get(indexInSource)) == null) {
-                    missings[numMissings++] = new Tile(this, tc, indexInQuery, indexInSource);
+                WritableRaster tile = rasters.get(indexInSource);
+                if (tile != null) {
+                    // Found a tile. Make sure that the raster starts at the expected coordinates.
+                    final int x=pc[0], y=pc[1];
+                    if (tile.getMinX() != x || tile.getMinY() != y) {
+                        tile = tile.createWritableTranslatedChild(x, y);
+                    }
+                    result[indexInQuery] = tile;
+                } else {
+                    // Tile not yet loaded. Add to a queue of tiles to load later.
+                    missings[numMissings++] = new Tile(this, tc, indexInQuery, indexInSource, pc);
                 }
+                // Increment all indices other than `indexInQuery`.
                 for (int i=0; i<tc.length; i++) {
                     indexInSource += tileStrides[i];
-                    if (++tc[i] < upper[i]) break;
-                    indexInSource -= (tc[i] - lower[i]) * tileStrides[i];     // Rewind to lower[i].
-                    tc[i] = lower[i];
+                    if (++tc[i] < tileUpper[i]) {
+                        pc[i] = Math.addExact(pc[i], decoder.getChunkSize(i));
+                        break;
+                    }
+                    indexInSource -= (tc[i] - tileLower[i]) * tileStrides[i];       // Rewind to index for tileLower[i].
+                    tc[i] = tileLower[i];
+                    pc[i] = offsetAOI[i];
                 }
             }
             Arrays.sort(missings, 0, numMissings);
@@ -320,20 +350,21 @@ final class DataSubset extends GridCoverage {
              * TODO: Use `tile.byteCount` for checking if two tiles are consecutive in the TIFF file.
              * In such case we should send only one HTTP range request.
              */
-            final Point location = new Point();
-            for (int i=0; i < numMissings; i++) {
-                final Tile  tile   = missings[i];
-                final long  minX   = JDK9.multiplyFull(tile.xInSubset, decoder.tileWidth);
-                final long  minY   = JDK9.multiplyFull(tile.yInSubset, decoder.tileHeight);
-                final long  offset = Math.addExact(owner.reader.origin, tile.byteOffset);
-                location.x = Math.addExact(firstTileX, tile.xInSubset);
-                location.y = Math.addExact(firstTileY, tile.yInSubset);
-                final WritableRaster raster = decoder.readSlice(offset, tile.byteCount,
-                        Math.max(lowerX - minX, 0), Math.min(upperX - minX, decoder.tileWidth),
-                        Math.max(lowerY - minY, 0), Math.min(upperY - minY, decoder.tileHeight),
-                        location);
-
-                // The `existing` raster should always be null, but we check anyway by paranoia.
+            final Point location = new Point();             // Tile upper-left corner coordinates relative to the request.
+            final long[] lower = new long[BIDIMENSIONAL];   // Coordinates of the first pixel to read relative to the tile.
+            final long[] upper = new long[BIDIMENSIONAL];   // Coordinates after the last pixel to read relative to the tile.
+            for (int i=0; i<numMissings; i++) {
+                final Tile tile   = missings[i];
+                final long offset = Math.addExact(owner.reader.origin, tile.byteOffset);
+                for (int d=0; d<BIDIMENSIONAL; d++) {
+                    final int  tileSize     = decoder.getChunkSize(d);
+                    final long relativeBase = JDK9.multiplyFull(tile.tileInSubset[d], tileSize);
+                    lower[d] = Math.max(relativeLower[d] - relativeBase, 0);
+                    upper[d] = Math.min(relativeUpper[d] - relativeBase, tileSize);
+                }
+                location.x = tile.minX;
+                location.y = tile.minY;
+                final WritableRaster raster = decoder.readSlice(offset, tile.byteCount, lower, upper, location);
                 final WritableRaster existing = rasters.putIfAbsent(tile.indexInSource, raster);
                 result[tile.indexInQuery] = (existing != null) ? existing : raster;
             }
@@ -349,64 +380,65 @@ final class DataSubset extends GridCoverage {
      */
     @Override
     public RenderedImage render(final GridExtent sliceExtent) {
-        final int dimension = numTiles.length;
-        final int[] upper = numTiles.clone();
-        final int[] lower = new int[dimension];
-        final int[] translation = new int[dimension];
+        final int   dimension = numTiles.length;
+        final int[] tileUpper = numTiles.clone();       // Indices of last tile to read, exclusive.
+        final int[] tileLower = new int[dimension];     // Indices of first tile to read, inclusive.
+        final int[] offsetAOI = new int[dimension];     // Pixel offset compared to Area Of Interest.
+        final int[] imageSize = new int[dimension];
         if (sliceExtent != null) {
-            final int[] sd = sliceExtent.getSubspaceDimensions(2);
+            final int[] sd = sliceExtent.getSubspaceDimensions(BIDIMENSIONAL);
             if (sd[1] != 1) {
                 // TODO
-                throw new UnsupportedOperationException("Non-horizontal slice not yet implemented.");
+                throw new UnsupportedOperationException("Non-horizontal slices not yet implemented.");
             }
             for (int i = Math.min(sliceExtent.getDimension(), dimension); --i >= 0;) {
-                final int  s   = decoder.getChunkSize(i);
-                final long min = sliceExtent.getLow(i);
-                translation[i] = (int) Math.floorMod(min, s);       // TODO: remove the cast with JDK9.
-                final long lo  = Math.max(Math.floorDiv(min, s), 0);
-                final long up  = Math.min(Numerics.ceilDiv(sliceExtent.getHigh(i), s) + 1, numTiles[i]);
-                if (up <= lo) {
-                    throw new IllegalArgumentException(Errors.format(Errors.Keys.IllegalRange_2,
-                            sliceExtent.getLow(i), sliceExtent.getHigh(i)));
+                final long tilePos  = origin[i];
+                final int  tileSize = decoder.getChunkSize(i);
+                final long aoiMin   = sliceExtent.getLow  (i);
+                final long aoiMax   = sliceExtent.getHigh (i);
+                final long tLo      = Math.max((aoiMin - tilePos) / tileSize, 0);   // Tile M.C. relative to this subset.
+                final long tUp      = Math.min((aoiMax - tilePos) / tileSize + 1, numTiles[i]);
+                if (tUp <= tLo) {
+                    throw new IllegalArgumentException(Errors.format(Errors.Keys.IllegalRange_2, aoiMin, aoiMax));
                 }
-                lower[i] = Math.toIntExact(lo);
-                upper[i] = Math.toIntExact(up);
+                final long pLo = Math.max(tLo * tileSize, relativeLower[i]);        // Relative coordinate of first pixel.
+                final long pUp = Math.min(tUp * tileSize, relativeUpper[i]);        // Relative coordinate after last pixel.
+                tileLower[i]   = Math.toIntExact(tLo);
+                tileUpper[i]   = Math.toIntExact(tUp);
+                imageSize[i]   = Math.toIntExact(pUp - pLo);
+                offsetAOI[i]   = Math.toIntExact(Math.subtractExact(Math.addExact(pLo, origin[i]), aoiMin));
             }
         }
+        /*
+         * Get all tiles in the specified region. I/O operations, if needed, happen here.
+         */
         try {
-            /*
-             * Get all tiles in the specified region. I/O operations, if needed, happen here.
-             */
-            final WritableRaster[] result = readTiles(lower, upper);
-            /*
-             * The tiles returned by `readTiles(…)` have their coordinates relative to the image upper-left corner.
-             * We need to make them relative to the `sliceExtent` requested in this method call.
-             */
-            final int tx = Math.addExact(translation[0], firstTileX * decoder.tileWidth);
-            final int ty = Math.addExact(translation[1], firstTileY * decoder.tileHeight);
-            if ((tx | ty) != 0) {
-                for (int i=0; i<result.length; i++) {
-                    final WritableRaster tile = result[i];
-                    final int x = Math.subtractExact(tile.getMinX(), tx);
-                    final int y = Math.subtractExact(tile.getMinY(), ty);
-                    result[i] = tile.createWritableTranslatedChild(x, y);
-                }
-            }
-            /*
-             * Put the tiles in the image to be returned.
-             */
-            final int width  = Math.toIntExact(Math.min(JDK9.multiplyFull(upper[0] - lower[0], decoder.tileWidth),  upperX - lowerX));
-            final int height = Math.toIntExact(Math.min(JDK9.multiplyFull(upper[1] - lower[1], decoder.tileHeight), upperY - lowerY));
-            final GridGeometry gg = new ImageRenderer(this, sliceExtent).getImageGeometry(2);
+            final WritableRaster[] result = readTiles(tileLower, tileUpper, offsetAOI);
+            final GridGeometry gg = new ImageRenderer(this, sliceExtent).getImageGeometry(BIDIMENSIONAL);
             final ColorModel colorModel;
             synchronized (owner.reader.store) {
                 colorModel = owner.getColorModel();
             }
-            return new TiledImage(Collections.singletonMap(PlanarImage.GRID_GEOMETRY_KEY, gg),
-                                  colorModel, width, height, lower[0], lower[1], result);
+            final TiledImage image = new TiledImage(Collections.singletonMap(PlanarImage.GRID_GEOMETRY_KEY, gg),
+                                    colorModel, imageSize[0], imageSize[1], tileLower[0], tileLower[1], result);
+            assert validate(image);
+            return image;
         } catch (Exception e) {                 // Too many exception types for listing them all.
             throw new CannotEvaluateException(owner.reader.resources().getString(
                     Resources.Keys.CanNotRenderImage_1, owner.filename()), e);
         }
     }
+
+    /**
+     * Verifies if the given image is consistent. This method tolerates smaller width or height compared to
+     * what we would expect from an integer amount of tiles, because this may get smaller height from images
+     * encoded as strips.
+     */
+    private static boolean validate(final TiledImage image) {
+        final String error = image.verify();
+        if (error == null || error.equals("width") || error.equals("height")) {
+            return true;
+        }
+        throw new AssertionError(error);
+    }
 }
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/PixelDecoder.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/PixelDecoder.java
index ea4bc0f..3a3def5 100644
--- a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/PixelDecoder.java
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/PixelDecoder.java
@@ -16,6 +16,7 @@
  */
 package org.apache.sis.storage.geotiff;
 
+import java.util.Arrays;
 import java.nio.Buffer;
 import java.io.IOException;
 import java.awt.Point;
@@ -55,7 +56,7 @@ class PixelDecoder {
     /**
      * Size of all tiles in the GeoTIFF image (without clipping and subsampling).
      */
-    final int tileWidth, tileHeight;
+    private final int tileWidth, tileHeight;
 
     /**
      * Subsampling along each dimension. All values shall be greater than zero.
@@ -131,30 +132,26 @@ class PixelDecoder {
      * <p>The default implementation in this base class assumes uncompressed data.
      * Subclasses must override for handling decompression.</p>
      *
-     * @param  input      the channel to read.
      * @param  offset     position in the channel where tile data begins.
      * @param  byteCount  number of bytes for the compressed tile data.
-     * @param  lowerX     <var>x</var> coordinate of the first pixel to read relative to the tile.
-     * @param  lowerY     <var>y</var> coordinate of the first pixel to read relative to the tile.
-     * @param  upperX     <var>x</var> coordinate after the last pixel to read relative to the tile.
-     * @param  upperY     <var>y</var> coordinate after the last pixel to read relative to the tile.
-     * @param  location   coordinates of the tile. Note: values are overwritten by this method.
+     * @param  lowerX     (<var>x</var>,<var>y</var>) coordinates of the first pixel to read relative to the tile.
+     * @param  upperX     (<var>x</var>,<var>y</var>) coordinate after the last pixel to read relative to the tile.
+     * @param  location   pixel coordinates in the upper-left corner of the tile to return.
      * @return image decoded from the GeoTIFF file.
      * @throws IOException if an I/O error occurred.
      * @throws DataStoreContentException if the sample model is not supported.
      * @throws RuntimeException if the Java2D image can not be created for another reason
      *         (too many exception types to list them all).
      */
-    WritableRaster readSlice(final long offset, final long byteCount,
-            final long lowerX, final long upperX, final long lowerY, final long upperY, final Point location)
-            throws IOException, DataStoreContentException
+    WritableRaster readSlice(final long offset, final long byteCount, final long[] lower, final long[] upper,
+                             final Point location) throws IOException, DataStoreContentException
     {
         final int type = model.getDataType();
-        final long length = (upperX - lowerX) * (upperY - lowerY) * (DataBuffer.getDataTypeSize(type) / Byte.SIZE);
+        final long length = (upper[0] - lower[0]) * (upper[1] - lower[1]) * (DataBuffer.getDataTypeSize(type) / Byte.SIZE);
         if (length > byteCount) {
             // We may have less bytes to read if only a subregion is read.
-            throw new DataStoreContentException(reader.resources().getString(
-                    Resources.Keys.UnexpectedTileLength_4, location.x, location.y, length, byteCount));
+            throw new DataStoreContentException(reader.resources().getString(Resources.Keys.UnexpectedTileLength_4,
+                        location.x / tileWidth, location.y / tileHeight, length, byteCount));
         }
         final HyperRectangleReader hr = new HyperRectangleReader(ImageUtilities.toNumberEnum(type), reader.input, offset);
         /*
@@ -168,16 +165,16 @@ class PixelDecoder {
             numInterleaved = 1;
         }
         final long[]   size        = new long[] {JDK9.multiplyFull(numInterleaved, tileWidth), tileHeight, numBanks};
-        final int[]    subsample2D = new int[]  {subsamplings[0], subsamplings[1], 1};
-        final long[]   regionLower = new long[] {lowerX, lowerY, 0};
-        final long[]   regionUpper = new long[] {upperX, upperY, 1};
-        final Region   region      = new Region(size, regionLower, regionUpper, subsample2D);
-        final int      capacity    = Math.toIntExact(region.length);
+        final int[]    subsample2D = new int [] {subsamplings[0], subsamplings[1], 1};
+        final long[]   regionLower = Arrays.copyOf(lower, DataSubset.BIDIMENSIONAL + 1);
+        final long[]   regionUpper = Arrays.copyOf(upper, DataSubset.BIDIMENSIONAL + 1);
         final Buffer[] banks       = new Buffer[numBanks];
         for (int b=0; b<numBanks; b++) {
-            regionLower[2] = b;
-            regionUpper[2] = b + 1;
-            final Buffer buffer = hr.readAsBuffer(region, capacity);
+            regionLower[DataSubset.BIDIMENSIONAL] = b;
+            regionUpper[DataSubset.BIDIMENSIONAL] = b + 1;
+            final Region region   = new Region(size, regionLower, regionUpper, subsample2D);
+            final int    capacity = Math.toIntExact(region.length);
+            final Buffer buffer   = hr.readAsBuffer(region, capacity);
             /*
              * The buffer returned by `readAsBuffer(…)` may have less data than the buffer capacity
              * if the current tile is smaller than the expected tile size (e.g. truncated last tile).
@@ -192,8 +189,6 @@ class PixelDecoder {
             }
             banks[b] = buffer.limit(capacity);
         }
-        location.x *= tileWidth;
-        location.y *= tileHeight;
         final DataBuffer buffer = RasterFactory.wrap(DataType.forDataBufferType(model.getDataType()), banks);
         return WritableRaster.createWritableRaster(model, buffer, location);
     }
diff --git a/storage/sis-storage/src/test/java/org/apache/sis/test/storage/CoverageReadConsistency.java b/storage/sis-storage/src/test/java/org/apache/sis/test/storage/CoverageReadConsistency.java
index 877ad6d..49b22a1 100644
--- a/storage/sis-storage/src/test/java/org/apache/sis/test/storage/CoverageReadConsistency.java
+++ b/storage/sis-storage/src/test/java/org/apache/sis/test/storage/CoverageReadConsistency.java
@@ -16,7 +16,9 @@
  */
 package org.apache.sis.test.storage;
 
+import java.util.Arrays;
 import java.util.Random;
+import java.awt.Point;
 import java.awt.Rectangle;
 import org.opengis.referencing.datum.PixelInCell;
 import org.apache.sis.coverage.grid.GridCoverage;
@@ -105,7 +107,7 @@ public strictfp class CoverageReadConsistency extends TestCase {
         for (int i=0; i<NUM_ITERATIONS; i++) {
             for (int d=0; d<dimension; d++) {
                 final int span = StrictMath.toIntExact(ex.getSize(d));
-                final int rs   = span-1;//random.nextInt(span);              // Span of the sub-region - 1.
+                final int rs   = random.nextInt(span);              // Span of the sub-region - 1.
                 low [d]        = random.nextInt(span - rs);         // Note: (span - rs) > 0.
                 high[d]        = low[d] + rs;
             }
@@ -141,7 +143,13 @@ nextSlice:  for (;;) {
                     assertTrue(itc.next());
                     expected = itr.getPixel(expected);
                     actual   = itc.getPixel(actual);
-                    assertArrayEquals(expected, actual, STRICT);
+                    if (!Arrays.equals(expected, actual)) {
+                        final Point pr = itr.getPosition();
+                        final Point pc = itc.getPosition();
+                        assertArrayEquals("Mismatch at position (" + pr.x + ", " + pr.y + ") in full image " +
+                                          "and (" + pc.x + ", " + pc.y + ") in tested sub-image",
+                                          expected, actual, STRICT);
+                    }
                 }
                 assertFalse(itc.next());
                 /*