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 2020/01/02 19:06:18 UTC

[sis] 01/02: Add ImageLayout as a helper class for setting the tiling used by ComputedImage.

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

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

commit be0c05340245a9c5eca12f8bbc14ba46cae9efa4
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Thu Jan 2 19:47:57 2020 +0100

    Add ImageLayout as a helper class for setting the tiling used by ComputedImage.
---
 .../sis/internal/coverage/j2d/ComputedImage.java   |  96 ++++----
 .../sis/internal/coverage/j2d/ImageFactory.java    |  76 +++++++
 .../sis/internal/coverage/j2d/ImageLayout.java     | 242 +++++++++++++++++++++
 .../sis/internal/coverage/j2d/ImageUtilities.java  |  57 ++---
 .../sis/internal/coverage/j2d/TileCache.java       |   6 +-
 .../sis/coverage/grid/GridCoverage2DTest.java      |   4 +-
 .../java/org/apache/sis/math/MathFunctions.java    |  17 +-
 7 files changed, 389 insertions(+), 109 deletions(-)

diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ComputedImage.java b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ComputedImage.java
index 8c371bf..99b3e5c 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ComputedImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ComputedImage.java
@@ -44,15 +44,21 @@ import org.apache.sis.util.Disposable;
  *
  * <p>Subclasses need to implement at least the following methods:</p>
  * <ul>
- *   <li>{@link #getWidth()}</li>
- *   <li>{@link #getHeight()}</li>
- *   <li>{@link #getTileWidth()}</li>
- *   <li>{@link #getTileHeight()}</li>
- *   <li>{@link #computeTile(int,int)}</li>
+ *   <li>{@link #getWidth()}           — the image width in pixels.</li>
+ *   <li>{@link #getHeight()}          — the image height in pixels.</li>
+ *   <li>{@link #computeTile(int,int)} — invoked when a requested tile is not in the cache.</li>
  * </ul>
  *
- * <p>This class is thread-safe. Multiple tiles may be computed in
- * different background threads.</p>
+ * <p>If pixel coordinates or tile indices do not start at zero,
+ * then subclasses shall also override the following methods:</p>
+ * <ul>
+ *   <li>{@link #getMinX()}     — the minimum <var>x</var> coordinate (inclusive) of the image.</li>
+ *   <li>{@link #getMinY()}     — the minimum <var>y</var> coordinate (inclusive) of the image.</li>
+ *   <li>{@link #getMinTileX()} — the minimum tile index in the <var>x</var> direction.</li>
+ *   <li>{@link #getMinTileY()} — the minimum tile index in the <var>y</var> direction.</li>
+ * </ul>
+ *
+ * <p>This class is thread-safe: multiple tiles may be computed in different background threads.</p>
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.1
@@ -137,7 +143,13 @@ public abstract class ComputedImage extends PlanarImage {
 
     /**
      * Creates an initially empty image with the given sample model.
-     * The tile size will be the width and height of the given sample model.
+     * The default tile size will be the width and height of the given sample model.
+     * The {@link ImageLayout#createCompatibleSampleModel(RenderedImage)} convenience method
+     * may be used for getting a sample model of desired size.
+     *
+     * <div class="note"><b>Note:</b>
+     * the restriction about sample model size matching tile size is for reducing the amount
+     * of memory consumed by {@link #createTile(int, int)}.</div>
      *
      * @param  sampleModel  the sample model shared by all tiles in this image.
      * @param  sources      sources of this image (may be an empty array), or a null array if unknown.
@@ -147,56 +159,12 @@ public abstract class ComputedImage extends PlanarImage {
         this.sampleModel = sampleModel;
         if (sources != null) {
             sources = sources.clone();
-            this.sources = sources;
             for (int i=0; i<sources.length; i++) {
                 ArgumentChecks.ensureNonNullElement("sources", i, sources[i]);
             }
-        } else {
-            this.sources = null;
-        }
-        reference = new Cleaner(this);
-    }
-
-    /**
-     * Creates an initially empty image with a sample model derived from the given image.
-     * This constructor sets {@link #sampleModel} to a model compatible with the one used
-     * by the given image, but with {@linkplain SampleModel#getWidth() width} and
-     * {@linkplain SampleModel#getHeight() height} matching exactly the size of the tiles.
-     *
-     * <p>This constructor does <strong>not</strong> inherit other image properties.
-     * In particular pixel coordinates and tile indices in this image start at (0,0)
-     * unless subclass override {@link #getMinX()}, {@link #getMinY()}, {@link #getMinTileX()}
-     * and {@link #getMinTileY()}.</p>
-     *
-     * @param  image   the main image from which to get tile size.
-     * @param  others  additional sources, or {@code null} if none.
-     */
-    protected ComputedImage(final RenderedImage image, final RenderedImage... others) {
-        ArgumentChecks.ensureNonNull("image", image);
-        /*
-         * Get a sample model compatible with the given one, but with the tile width and height.
-         * We check if the given sample model can be used as-is and create a new one only if needed.
-         * This restriction about sample model size matching tile size is for reducing the amount
-         * of memory consumed by {@link #createTile(int, int)}.
-         */
-        final int width  = image.getTileWidth();
-        final int height = image.getTileHeight();
-        SampleModel sm   = image.getSampleModel();
-        if (sm.getWidth() != width || sm.getHeight() != height) {
-            sm = sm.createCompatibleSampleModel(width, height);
-        }
-        sampleModel = sm;
-        if (others == null) {
-            sources = new RenderedImage[] {image};
-        } else {
-            sources = new RenderedImage[others.length + 1];
-            sources[0] = image;
-            System.arraycopy(others, 0, sources, 1, others.length);
-            for (int i=1; i<sources.length; i++) {
-                ArgumentChecks.ensureNonNullElement("others", i-1, sources[i]);
-            }
         }
-        reference = new Cleaner(this);
+        this.sources = sources;             // Note: null value does not have same meaning than empty array.
+        reference = new Cleaner(this);      // Create cleaner last after all arguments have been validated.
     }
 
     /**
@@ -282,7 +250,7 @@ public abstract class ComputedImage extends PlanarImage {
      * @param  tileX  the column index of the tile to get.
      * @param  tileY  the row index of the tile to get.
      * @return the tile at the given index (never null).
-     * @throws IndexOutOfBoundsException if a given tile index is out of bounds.
+     * @throws IllegalArgumentException if a given tile index is out of bounds.
      * @throws ImagingOpException if an error occurred while computing the image.
      */
     @Override
@@ -291,13 +259,24 @@ public abstract class ComputedImage extends PlanarImage {
         final Cache<TileCache.Key,Raster> cache = TileCache.GLOBAL;
         Raster tile = cache.peek(key);
         if (tile == null) {
+            int min;
+            ArgumentChecks.ensureBetween("tileX", (min = getMinTileX()), min + getNumXTiles() - 1, tileX);
+            ArgumentChecks.ensureBetween("tileY", (min = getMinTileY()), min + getNumYTiles() - 1, tileY);
             final Cache.Handler<Raster> handler = cache.lock(key);
             try {
                 tile = handler.peek();
                 if (tile == null) {
-                    tile = computeTile(tileX, tileY);
+                    Exception cause = null;
+                    try {
+                        tile = computeTile(tileX, tileY);
+                    } catch (ImagingOpException e) {
+                        throw e;                            // Let that kind of exception propagate.
+                    } catch (Exception e) {
+                        cause = e;
+                    }
                     if (tile == null) {
-                        throw new ImagingOpException(Resources.format(Resources.Keys.CanNotComputeTile_2, tileX, tileY));
+                        throw (ImagingOpException) new ImagingOpException(Resources.format(
+                                Resources.Keys.CanNotComputeTile_2, tileX, tileY)).initCause(cause);
                     }
                 }
             } finally {
@@ -314,8 +293,9 @@ public abstract class ComputedImage extends PlanarImage {
      * @param  tileX  the column index of the tile to compute.
      * @param  tileY  the row index of the tile to compute.
      * @return computed tile for the given indices (can not be null).
+     * @throws Exception if an error occurred while computing the tile.
      */
-    protected abstract Raster computeTile(int tileX, int tileY);
+    protected abstract Raster computeTile(int tileX, int tileY) throws Exception;
 
     /**
      * Creates an initially empty tile at the given tile grid position.
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageFactory.java b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageFactory.java
new file mode 100644
index 0000000..8355838
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageFactory.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.coverage.j2d;
+
+import java.awt.image.BufferedImage;
+import java.awt.image.ColorModel;
+import java.awt.image.DataBuffer;
+import org.apache.sis.util.Static;
+
+
+/**
+ * Convenience methods for creating new images.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+public final class ImageFactory extends Static {
+    /**
+     * Do not allow instantiation of this class.
+     */
+    private ImageFactory() {
+    }
+
+    /**
+     * Creates an opaque image with a gray scale color model. The image can have an arbitrary
+     * number of bands, but in current implementation only one band is used.
+     *
+     * <p><b>Warning:</b> displaying this image is very slow, except in a few special cases.
+     * It should be used only when no standard color model can be used.</p>
+     *
+     * @param  dataType       the color model type as one of {@code DataBuffer.TYPE_*} constants.
+     * @param  width          the desired image width.
+     * @param  height         the desired image height.
+     * @param  numComponents  the number of components.
+     * @param  visibleBand    the band to use for computing colors.
+     * @param  minimum        the minimal sample value expected.
+     * @param  maximum        the maximal sample value expected.
+     * @return the color space for the given range of values.
+     */
+    public static BufferedImage createGrayScale(final int dataType, final int width, final int height,
+            final int numComponents, final int visibleBand, final double minimum, final double maximum)
+    {
+        switch (dataType) {
+            case DataBuffer.TYPE_BYTE: {
+                if (numComponents == 1 && minimum <= 0 && maximum >= 0xFF) {
+                    return new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY);
+                }
+                break;
+            }
+            case DataBuffer.TYPE_USHORT: {
+                if (numComponents == 1 && minimum <= 0 && maximum >= 0xFFFF) {
+                    return new BufferedImage(width, height, BufferedImage.TYPE_USHORT_GRAY);
+                }
+                break;
+            }
+        }
+        final ColorModel cm = ColorModelFactory.createGrayScale(DataBuffer.TYPE_INT, 1, 0, -10, 10);
+        return new BufferedImage(cm, cm.createCompatibleWritableRaster(width, height), false, null);
+    }
+}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageLayout.java b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageLayout.java
new file mode 100644
index 0000000..1e29459
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageLayout.java
@@ -0,0 +1,242 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.coverage.j2d;
+
+import java.util.Arrays;
+import java.awt.Dimension;
+import java.awt.image.ColorModel;
+import java.awt.image.IndexColorModel;
+import java.awt.image.RenderedImage;
+import java.awt.image.SampleModel;
+import org.apache.sis.math.MathFunctions;
+import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.internal.util.Strings;
+
+
+/**
+ * Derives information about image location, size and tile grid. {@code ImageLayout} does not store
+ * those information directly, but provides method for deriving those properties from a given image.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+public class ImageLayout {
+    /**
+     * The minimum tile size. The {@link #toTileSize(int, int, boolean)} method will not suggest tiles
+     * smaller than this size. This size must be smaller than {@link ImageUtilities#DEFAULT_TILE_SIZE}.
+     *
+     * <p>Tiles of 180×180 pixels consume about 127 kB, assuming 4 bytes per pixel. This is about half
+     * the consumption of tiles of 256×256 pixels. We select a size which is a multiple of 90 because
+     * images are often used with a resolution of e.g. ½° per pixel.</p>
+     */
+    private static final int MIN_TILE_SIZE = 180;
+
+    /**
+     * The default instance which will target {@value ImageUtilities#DEFAULT_TILE_SIZE} pixels as tile
+     * width and height.
+     */
+    public static final ImageLayout DEFAULT = new ImageLayout(null, true);
+
+    /**
+     * Preferred size for tiles.
+     *
+     * @see ImageUtilities#DEFAULT_TILE_SIZE
+     */
+    private final int preferredTileWidth, preferredTileHeight;
+
+    /**
+     * Whether this instance allow tiles that are only partially filled. A value of {@code true} implies that
+     * tiles in the last row or in the last column may contain empty pixels. A value of {@code false} implies
+     * that this class will be unable to subdivide large images in smaller tiles if the image size is a prime
+     * number.
+     */
+    private final boolean allowPartialTiles;
+
+    /**
+     * Creates a new image layout.
+     *
+     * @param  preferredTileSize  the preferred tile size, or {@code null} for the default size.
+     * @param  allowPartialTiles  whether this instance allow tiles that are only partially filled.
+     */
+    protected ImageLayout(final Dimension preferredTileSize, final boolean allowPartialTiles) {
+        if (preferredTileSize != null) {
+            preferredTileWidth  = preferredTileSize.width;
+            preferredTileHeight = preferredTileSize.height;
+        } else {
+            preferredTileWidth  = ImageUtilities.DEFAULT_TILE_SIZE;
+            preferredTileHeight = ImageUtilities.DEFAULT_TILE_SIZE;
+        }
+        this.allowPartialTiles = allowPartialTiles;
+    }
+
+    /**
+     * Suggests a tile size close to {@code tileSize} for the specified {@code imageSize}.
+     * First, this method tries to return a tile size which is a divisor of the image size.
+     * If no such divisor is found and {@code allowPartialTiles} is {@code true}, then this
+     * method returns a size that minimize the amount of empty pixels in the last tile.
+     *
+     * @param  imageSize          the image size (width or height).
+     * @param  preferredTileSize  the preferred tile size, which is often {@value ImageUtilities#DEFAULT_TILE_SIZE}.
+     * @param  allowPartialTiles  whether to allow tiles that are only partially filled.
+     * @return the suggested tile size, or {@code imageSize} if none.
+     */
+    private static int toTileSize(final int imageSize, final int preferredTileSize, final boolean allowPartialTiles) {
+        if (imageSize <= 2*preferredTileSize) {     // Factor 2 is arbitrary.
+            return imageSize;
+        }
+        int rmax = imageSize % preferredTileSize;
+        if (rmax == 0) return preferredTileSize;
+        /*
+         * Find tile sizes which are divisors of image size and select the one closest to desired size.
+         * Note: the (i >= 0) check is a paranoiac check redundant with (imageSize % tileSize == 0) check.
+         */
+        final int[] divisors = MathFunctions.divisors(imageSize);
+        int i = Arrays.binarySearch(divisors, preferredTileSize);
+        if (i >= 0) return divisors[i];
+        if ((i = ~i) < divisors.length) {
+            final int smaller = divisors[i];
+            final boolean tooSmall = (smaller < MIN_TILE_SIZE);
+            if (++i < divisors.length) {
+                final int larger = divisors[i];
+                if (larger < imageSize && (tooSmall || (larger - preferredTileSize) <= preferredTileSize - smaller)) {
+                    return larger;
+                }
+            }
+            if (!tooSmall) {
+                return smaller;
+            }
+        }
+        /*
+         * Found no exact divisor. If we are allowed to return an approximated size,
+         * search the divisor which will minimize the amount of empty pixels.
+         */
+        int best = preferredTileSize;
+        if (allowPartialTiles) {
+            for (i = imageSize/2; --i >= MIN_TILE_SIZE;) {
+                final int r = imageSize % i;
+                if (r == 0) return i;       // Should never happen since we checked divisors before, but be paranoiac.
+                if (r > rmax || (r == rmax && Math.abs(i - preferredTileSize) < Math.abs(best - preferredTileSize))) {
+                    rmax = r;
+                    best = i;
+                }
+            }
+        }
+        /*
+         * At this point `best` is an "optimal" tile size (the one that left as few empty pixels as possible),
+         * and `rmax` is the amount of non-empty pixels using this tile size. We will use that "optimal" size
+         * only if it fills at least 75% of the tile size. Otherwise, we arbitrarily consider that it doesn't
+         * worth to tile.
+         */
+        return (rmax >= preferredTileSize - preferredTileSize/4) ? best : imageSize;
+    }
+
+    /**
+     * Suggests a tile size for the specified image size. This method suggests a tile size which is a divisor
+     * of the given image size if possible, or a size that left as few empty pixels as possible otherwise.
+     *
+     * <p>The {@code allowPartialTile} argument should be {@code false} if the tiled image is opaque,
+     * or if the sample value for transparent pixels is different than zero. This restriction is for
+     * avoiding black or colored borders on the image left size and bottom size.</p>
+     *
+     * @param  imageWidth         the image width in pixels.
+     * @param  imageHeight        the image height in pixels.
+     * @param  allowPartialTiles  whether to allow tiles that are only partially filled.
+     * @return suggested tile size for the given image size.
+     */
+    public Dimension suggestTileSize(final int imageWidth, final int imageHeight, boolean allowPartialTiles) {
+        allowPartialTiles &= this.allowPartialTiles;
+        return new Dimension(toTileSize(imageWidth,  preferredTileWidth,  allowPartialTiles),
+                             toTileSize(imageHeight, preferredTileHeight, allowPartialTiles));
+    }
+
+    /**
+     * Suggests a tile size for operations derived from the given image.
+     * If the given image is null, then this method returns the preferred tile size.
+     * Otherwise if the given image is already tiled, then this method preserves the
+     * current tile size unless the tiles are too large, in which case they may be subdivided.
+     * Otherwise (untiled image) this method proposes a tile size.
+     *
+     * <p>This method also checks whether the color model supports transparency. If not, then this
+     * method will not return a size that may result in the creation of partially empty tiles.</p>
+     *
+     * @param  image  the image for which to derive a tile size, or {@code null}.
+     * @return suggested tile size for the given image.
+     */
+    public Dimension suggestTileSize(final RenderedImage image) {
+        if (image == null) {
+            return new Dimension(preferredTileWidth, preferredTileHeight);
+        }
+        boolean pt = allowPartialTiles;
+        if (pt) {
+            final ColorModel cm = image.getColorModel();
+            if (cm != null) {
+                if (cm instanceof IndexColorModel) {
+                    pt = ((IndexColorModel) cm).getTransparentPixel() == 0;
+                } else {
+                    pt = cm.hasAlpha();
+                }
+            }
+        }
+        /*
+         * If the image is already tiled, we may select smaller tiles if the original tiles are too large
+         * but those smaller tiles must be divisors of the original size. This is necessary because image
+         * operations may assume that a call to `source.getTile(…)` will return a tile covering fully the
+         * tile to compute.
+         */
+        final boolean singleXTile = image.getNumXTiles() <= 1;
+        final boolean singleYTile = image.getNumYTiles() <= 1;
+        int width  =  singleXTile ? image.getWidth()  : image.getTileWidth();
+        int height =  singleYTile ? image.getHeight() : image.getTileHeight();
+        return new Dimension(toTileSize(width,  preferredTileWidth,  pt & singleXTile),
+                             toTileSize(height, preferredTileHeight, pt & singleYTile));
+    }
+
+    /**
+     * Creates a sample model compatible with the sample model of the given image
+     * but with a size matching the preferred tile size. This method can be used
+     * for determining the {@code sampleModel} argument of {@link ComputedImage}
+     * constructor.
+     *
+     * @param  image  the image form which to get a sample model.
+     * @return image sample model with preferred tile size.
+     *
+     * @see ComputedImage#ComputedImage(SampleModel, RenderedImage...)
+     */
+    public SampleModel createCompatibleSampleModel(final RenderedImage image) {
+        ArgumentChecks.ensureNonNull("image", image);
+        final Dimension tile = suggestTileSize(image);
+        SampleModel sm = image.getSampleModel();
+        if (sm.getWidth() != tile.width || sm.getHeight() != tile.height) {
+            sm = sm.createCompatibleSampleModel(tile.width, tile.height);
+        }
+        return sm;
+    }
+
+    /**
+     * Returns a string representation for debugging purpose.
+     *
+     * @return a string representation for debugging purpose.
+     */
+    @Override
+    public String toString() {
+        return Strings.toString(getClass(),
+                "preferredTileSize", new StringBuilder().append(preferredTileWidth).append('×').append(preferredTileHeight),
+                "allowPartialTiles", allowPartialTiles);
+    }
+}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageUtilities.java b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageUtilities.java
index 6cd8807..9648cfc 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageUtilities.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageUtilities.java
@@ -19,7 +19,6 @@ package org.apache.sis.internal.coverage.j2d;
 import java.util.Arrays;
 import java.awt.Rectangle;
 import java.awt.color.ColorSpace;
-import java.awt.image.BufferedImage;
 import java.awt.image.ColorModel;
 import java.awt.image.DataBuffer;
 import java.awt.image.IndexColorModel;
@@ -31,6 +30,7 @@ import java.awt.image.SampleModel;
 import java.awt.image.SinglePixelPackedSampleModel;
 import org.apache.sis.internal.feature.Resources;
 import org.apache.sis.internal.system.Modules;
+import org.apache.sis.util.Static;
 import org.apache.sis.util.logging.Logging;
 import org.apache.sis.util.resources.Vocabulary;
 
@@ -43,7 +43,7 @@ import org.apache.sis.util.resources.Vocabulary;
  * @since   1.1
  * @module
  */
-public final class ImageUtilities {
+public final class ImageUtilities extends Static {
     /**
      * Default width and height of tiles, in pixels.
      */
@@ -65,43 +65,6 @@ public final class ImageUtilities {
     }
 
     /**
-     * Creates an opaque image with a gray scale color model. The image can have an arbitrary
-     * number of bands, but in current implementation only one band is used.
-     *
-     * <p><b>Warning:</b> displaying this image is very slow, except in a few special cases.
-     * It should be used only when no standard color model can be used.</p>
-     *
-     * @param  dataType       the color model type as one of {@code DataBuffer.TYPE_*} constants.
-     * @param  width          the desired image width.
-     * @param  height         the desired image height.
-     * @param  numComponents  the number of components.
-     * @param  visibleBand    the band to use for computing colors.
-     * @param  minimum        the minimal sample value expected.
-     * @param  maximum        the maximal sample value expected.
-     * @return the color space for the given range of values.
-     */
-    public static BufferedImage createGrayScale(final int dataType, final int width, final int height,
-            final int numComponents, final int visibleBand, final double minimum, final double maximum)
-    {
-        switch (dataType) {
-            case DataBuffer.TYPE_BYTE: {
-                if (numComponents == 1 && minimum <= 0 && maximum >= 0xFF) {
-                    return new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY);
-                }
-                break;
-            }
-            case DataBuffer.TYPE_USHORT: {
-                if (numComponents == 1 && minimum <= 0 && maximum >= 0xFFFF) {
-                    return new BufferedImage(width, height, BufferedImage.TYPE_USHORT_GRAY);
-                }
-                break;
-            }
-        }
-        final ColorModel cm = ColorModelFactory.createGrayScale(DataBuffer.TYPE_INT, 1, 0, -10, 10);
-        return new BufferedImage(cm, cm.createCompatibleWritableRaster(width, height), false, null);
-    }
-
-    /**
      * Returns the bounds of the given image as a new rectangle.
      *
      * @param  image  the image for which to get the bounds.
@@ -114,6 +77,22 @@ public final class ImageUtilities {
     }
 
     /**
+     * Returns the data type of the given raster.
+     *
+     * @param  raster  the raster for which to get the data type, or {@code null}.
+     * @return the data type, or {@link DataBuffer#TYPE_UNDEFINED} if unknown.
+     */
+    public static int getDataType(final Raster raster) {
+        if (raster != null) {
+            final DataBuffer buffer = raster.getDataBuffer();
+            if (buffer != null) {
+                return buffer.getDataType();
+            }
+        }
+        return DataBuffer.TYPE_UNDEFINED;
+    }
+
+    /**
      * Names of {@link DataBuffer} types.
      */
     private static final String[] TYPE_NAMES = new String[DataBuffer.TYPE_DOUBLE + 1];
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/TileCache.java b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/TileCache.java
index 41cd10e..6dbdb54 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/TileCache.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/TileCache.java
@@ -114,7 +114,11 @@ final class TileCache extends Cache<TileCache.Key, Raster> {
          */
         @Override
         public int hashCode() {
-            return Long.hashCode(System.identityHashCode(image) + tileX + 65563L * tileY);
+            /*
+             * Dispatch tileX and tileY on approximately two halves of 32 bits integer.
+             * 65563 is a prime number close to 65536, the capacity of 16 bits integers.
+             */
+            return System.identityHashCode(image) + tileX + 65563 * tileY;
         }
 
         /**
diff --git a/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/GridCoverage2DTest.java b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/GridCoverage2DTest.java
index d3a1d64..f89d81f 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/GridCoverage2DTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/GridCoverage2DTest.java
@@ -27,7 +27,7 @@ import org.opengis.referencing.operation.MathTransform1D;
 import org.opengis.referencing.datum.PixelInCell;
 import org.apache.sis.coverage.SampleDimension;
 import org.apache.sis.geometry.DirectPosition2D;
-import org.apache.sis.internal.coverage.j2d.ImageUtilities;
+import org.apache.sis.internal.coverage.j2d.ImageFactory;
 import org.apache.sis.measure.NumberRange;
 import org.apache.sis.measure.Units;
 import org.apache.sis.referencing.crs.HardCodedCRS;
@@ -70,7 +70,7 @@ public final strictfp class GridCoverage2DTest extends TestCase {
          * Create an image and set values directly as integers. We do not use one of the
          * BufferedImage.TYPE_* constant because this test uses some negative values.
          */
-        final BufferedImage  image  = ImageUtilities.createGrayScale(DataBuffer.TYPE_INT, size, size, 1, 0, -10, 10);
+        final BufferedImage  image  = ImageFactory.createGrayScale(DataBuffer.TYPE_INT, size, size, 1, 0, -10, 10);
         final WritableRaster raster = image.getRaster();
         raster.setSample(0, 0, 0,   2);
         raster.setSample(1, 0, 0,   5);
diff --git a/core/sis-utility/src/main/java/org/apache/sis/math/MathFunctions.java b/core/sis-utility/src/main/java/org/apache/sis/math/MathFunctions.java
index 51a7c96..fa69922 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/math/MathFunctions.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/math/MathFunctions.java
@@ -61,7 +61,7 @@ import static org.apache.sis.internal.util.Numerics.SIGNIFICAND_SIZE;
  *
  * @author  Martin Desruisseaux (MPO, IRD, Geomatys)
  * @author  Johann Sorel (Geomatys)
- * @version 1.0
+ * @version 1.1
  *
  * @see DecimalFunctions
  * @see org.apache.sis.util.Numbers
@@ -750,8 +750,8 @@ public final class MathFunctions extends Static {
              */
             return Double.NaN;
         }
-        exp -= (16383 - 1023);      //change from 15 bias to 11 bias
-        // Check cases where mantissa excess what double can support
+        exp -= (16383 - 1023);     // Change from 15 bias to 11 bias.
+        // Check cases where mantissa excess what double can support.
         if (exp < 0)    return Double.NEGATIVE_INFINITY;
         if (exp > 2046) return Double.POSITIVE_INFINITY;
 
@@ -852,12 +852,11 @@ testNextNumber:         while (true) {      // Simulate a "goto" statement (usua
         int count = 1;
         /*
          * Searches for the first divisors among the prime numbers. We stop the search at the
-         * square root of 'n' because every values above that point can be inferred from the
-         * values before that point, i.e. if n=p1*p2 and p2 is greater than 'sqrt', than p1
-         * most be lower than 'sqrt'.
+         * square root of `n` because every values above that point can be inferred from the
+         * values before that point, i.e. if n=p₁⋅p₂ and p₂ is greater than `sqrt`, than p₁
+         * must be lower than `sqrt`.
          */
-        final int sqrt = (int) sqrt(number);               // Really want rounding toward 0.
-        for (int p,i=0; (p=primeNumberAt(i)) <= sqrt; i++) {
+        for (int p,i=0; (p=primeNumberAt(i))*p <= number; i++) {
             if (number % p == 0) {
                 if (count == divisors.length) {
                     divisors = Arrays.copyOf(divisors, count*2);
@@ -866,7 +865,7 @@ testNextNumber:         while (true) {      // Simulate a "goto" statement (usua
             }
         }
         /*
-         * Completes the divisors past 'sqrt'. The numbers added here may or may not be prime
+         * Completes the divisors past `sqrt`. The numbers added here may or may not be prime
          * numbers. Side note: checking that they are prime numbers would be costly, but this
          * algorithm doesn't need that.
          */