/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2009-2012, Open Source Geospatial Foundation (OSGeo)
* (C) 2009-2012, Geomatys
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotoolkit.image.io.mosaic;
import java.util.Random;
import java.awt.Dimension;
import java.awt.Rectangle;
import java.io.IOException;
import org.apache.sis.math.Statistics;
import org.geotoolkit.resources.Errors;
import static org.apache.sis.util.ArgumentChecks.ensureStrictlyPositive;
/**
* Profiles the loading of random rectangular regions using a given {@link TileManager}.
* The images to load while have random locations and random sizes, constrained between
* a {@linkplain #getMinSize() minimal} and {@linkplain #getMaxSize() maximal size}. A
* random subsampling will be requested, constrained between a {@linkplain #getMinSubsampling()
* minimum} and {@linkplain #getMaxSubsampling() maximum subsampling}.
* <p>
* More details on the algorithm used by this class are defined in the following methods:
* <p>
* <ul>
* <li>{@link #estimateEfficiency(int)}</li>
* </ul>
*
* The example below profiles a mosaic for different subsampling values.
* See {@link MosaicImageReadParam} for an explanation about why invoking
* <code>{@linkplain #setSubsamplingChangeAllowed setSubsamplingChangeAllowed}(true)</code>
* is strongly recommended.
*
* {@preformat java
* MosaicProfiler profiler = new MosaicProfiler(mosaic);
* profiler.setSubsamplingChangeAllowed(true); // STRONGLY RECOMMANDED
* profiler.setMaxSubsampling(1);
* for (int i=1; i<20; i++) {
* profiler.setMinSubsampling(i);
* double efficiency = profiler.estimateEfficiency(100);
* System.out.println("Subsampling=" + i + ", estimated efficiency=" + efficiency);
* }
* }
*
* @author Martin Desruisseaux (Geomatys)
* @version 3.00
*
* @since 3.00
* @module
*/
public class MosaicProfiler {
/**
* The default minimal tile size.
*/
private static final int DEFAULT_MIN = 512;
/**
* The default maximal tile size.
*/
private static final int DEFAULT_MAX = 1024;
/**
* The mosaic to profile.
*/
public final TileManager mosaic;
/**
* The area where to create random rectangle. <strong>Do not modify</strong>.
* On construction, this is set to a rectangle which may be a shared instance.
*/
private Rectangle region;
/**
* The minimal tile size, inclusive. Shall not be greater than {@link #maxSize}.
*/
private final Dimension minSize;
/**
* The maximal tile size, inclusive. Shall not be greater than the mosaic size.
*/
private final Dimension maxSize;
/**
* The minimal subsampling, inclusive. Shall not be greater than {@link #maxSubsampling}.
*/
private final Dimension minSubsampling;
/**
* The maximal subsampling, inclusive.
*/
private final Dimension maxSubsampling;
/**
* If {@code true}, the mosaic will be allowed to change the specified
* subsampling to some lower but more efficient subsampling.
*
* @see #isSubsamplingChangeAllowed
*/
private boolean subsamplingChangeAllowed;
/**
* The random number generator.
*/
private final Random random = new Random();
/**
* Creates a profiler for the given mosaic.
*
* @param mosaic The mosaic to profile.
* @throws IOException If it was necessary to fetch an image dimension from its
* {@linkplain Tile#getImageReader reader} and this operation failed.
*/
public MosaicProfiler(final TileManager mosaic) throws IOException {
this.mosaic = mosaic;
region = mosaic.getRegion(); // Shared instance - do not modify!
maxSize = new Dimension(Math.min(DEFAULT_MAX, region .width), Math.min(DEFAULT_MAX, region .height));
minSize = new Dimension(Math.min(DEFAULT_MIN, maxSize.width), Math.min(DEFAULT_MIN, maxSize.height));
maxSubsampling = new Dimension(region.width / minSize.width, region.height / minSize.height);
minSubsampling = new Dimension(1, 1);
}
/**
* Sets the seed of the random number generator. If this profiler is set to the same seed
* than a previous profiling session and if the properties (minimum and maximal tile size
* and subsampling) have not been changed, then the generated random rectangular regions
* while be the same than the previous profiling session.
*
* @param seed The seed to be given to the random number generator.
*/
public synchronized void setSeed(final long seed) {
random.setSeed(seed);
}
/**
* Copies the given source dimension to the given target dimension. This method
* ensures that the source dimension is not empty before to perform the copy.
*/
private static void setSize(final Dimension source, final Dimension target)
throws IllegalArgumentException
{
final int width, height;
ensureStrictlyPositive("width", width = source.width);
ensureStrictlyPositive("height", height = source.height);
target.setSize(width, height);
}
/**
* If the minimum size is greater than the maximum size, ajust one of the size in order
* to keep them ordered.
*
* @param min The minimum size.
* @param max The maximum size.
* @param a If {@code true}, the maximum size may be augmented. If {@code false}, the
* minimum size may be reduced.
*/
private static void adjust(final Dimension min, final Dimension max, final boolean a) {
if (min.width > max.width) {
if (a) max.width = min.width;
else min.width = max.width;
}
if (min.height > max.height) {
if (a) max.height = min.height;
else min.height = max.height;
}
}
/**
* Returns the region in which random rectangles will be calculated. On
* {@code MosaicProfiler} construction, this is set to the region covered
* by the whole mosaic.
*
* @return The region in which random rectangles will be calculated.
*/
public synchronized Rectangle getQueryRegion() {
return (Rectangle) region.clone();
}
/**
* Sets the region in which random rectangles will be calculated. This method computes
* the intersection of the given rectangle with the mosaic bounds, and the result must
* be non-empty.
*
* @param bounds The region in which random rectangles will be calculated.
* @throws IOException If it was necessary to fetch an image dimension from its
* {@linkplain Tile#getImageReader reader} and this operation failed.
*/
public synchronized void setQueryRegion(final Rectangle bounds) throws IOException {
final Rectangle old = region;
if ((region = mosaic.getRegion().intersection(bounds)).isEmpty()) {
region = old;
throw new IllegalArgumentException(Errors.format(Errors.Keys.EmptyRectangle_1, bounds));
}
}
/**
* Returns the size of the mosaic given to the constructor.
*
* @return The mosaic size.
* @throws IOException If it was necessary to fetch an image dimension from its
* {@linkplain Tile#getImageReader reader} and this operation failed.
*/
public synchronized Dimension getMosaicSize() throws IOException {
return mosaic.getRegion().getSize();
}
/**
* Returns the minimal size of the images to load.
*
* @return The minimal size of the images to load.
*/
public synchronized Dimension getMinSize() {
return (Dimension) minSize.clone();
}
/**
* Sets the minimal size of the images to load. If the given size is greater
* than the maximal size, then the maximal size will be increased accordingly.
*
* @param size The minimal size of the images to load.
*/
public synchronized void setMinSize(final Dimension size) {
setSize(size, minSize);
adjust(minSize, maxSize, true);
}
/**
* Convenience method setting the minimal size to the same value along <var>x</var>
* and <var>y</var> axis.
*
* @param size The minimal width and height of the images to load.
*/
public synchronized void setMinSize(final int size) {
ensureStrictlyPositive("size", size);
minSize.setSize(size, size);
adjust(minSize, maxSize, true);
}
/**
* Returns the maximal size of the images to load.
*
* @return The maximal size of the images to load.
*/
public synchronized Dimension getMaxSize() {
return (Dimension) maxSize.clone();
}
/**
* Sets the maximal size of the images to load. If the given size is smaller
* than the minimal size, then the minimal size will be reduced accordingly.
*
* @param size The maximal size of the images to load.
*/
public synchronized void setMaxSize(final Dimension size) {
setSize(size, maxSize);
adjust(minSize, maxSize, false);
}
/**
* Convenience method setting the maximal size to the same value along <var>x</var>
* and <var>y</var> axis.
*
* @param size The maximal width and height of the images to load.
*/
public synchronized void setMaxSize(final int size) {
ensureStrictlyPositive("size", size);
maxSize.setSize(size, size);
adjust(minSize, maxSize, false);
}
/**
* Returns the minimal subsampling of the random rectangular regions to load.
*
* @return The minimal subsampling of the regions to load.
*/
public synchronized Dimension getMinSubsampling() {
return (Dimension) minSubsampling.clone();
}
/**
* Sets the minimal subsampling of the random rectangular regions to load. If the given
* subsampling is greater than the maximal subsampling, then the maximal subsampling will
* be increased accordingly.
*
* @param subsampling The minimal subsampling of the regions to load.
*/
public synchronized void setMinSubsampling(final Dimension subsampling) {
setSize(subsampling, minSubsampling);
adjust(minSubsampling, maxSubsampling, true);
}
/**
* Convenience method setting the minimal subsampling to the same value along <var>x</var>
* and <var>y</var> axis.
*
* @param subsampling The minimal subsampling along <var>x</var> and <var>y</var> axis.
*/
public synchronized void setMinSubsampling(final int subsampling) {
ensureStrictlyPositive("subsampling", subsampling);
minSubsampling.setSize(subsampling, subsampling);
adjust(minSubsampling, maxSubsampling, true);
}
/**
* Returns the maximal subsampling of the random rectangular regions to load.
*
* @return The maximal subsampling of the regions to load.
*/
public synchronized Dimension getMaxSubsampling() {
return (Dimension) maxSubsampling.clone();
}
/**
* Sets the maximal subsampling of the random rectangular regions to load. If the given
* subsampling is smaller than the minimal subsampling, then the minimal subsampling will
* be reduced accordingly.
*
* @param subsampling The maximal subsampling of the regions to load.
*/
public synchronized void setMaxSubsampling(final Dimension subsampling) {
setSize(subsampling, maxSubsampling);
adjust(minSubsampling, maxSubsampling, false);
}
/**
* Convenience method setting the maximal subsampling to the same value along <var>x</var>
* and <var>y</var> axis.
*
* @param subsampling The maximal subsampling along <var>x</var> and <var>y</var> axis.
*/
public synchronized void setMaxSubsampling(final int subsampling) {
ensureStrictlyPositive("subsampling", subsampling);
maxSubsampling.setSize(subsampling, subsampling);
adjust(minSubsampling, maxSubsampling, false);
}
/**
* Returns {@code true} if the mosaic is allowed to change the subsampling to some more
* efficient value. The default value is {@code false}, which means that the mosaic will
* use exactly the given subsampling and may leads to very slow reading.
*
* @return {@code true} if the mosaic is allowed to change the subsampling.
*
* @see MosaicImageReadParam#isSubsamplingChangeAllowed
*/
public synchronized boolean isSubsamplingChangeAllowed() {
return subsamplingChangeAllowed;
}
/**
* Sets whatever the mosaic will be allowed to change the subsampling to some more efficient
* value. <strong>Users are strongly encouraged to set this value to {@code true}</strong>,
* which is not the default because doing so would violate the {@link javax.imageio.ImageReader}
* contract.
*
* @param allowed {@code true} if the mosaic is allowed to change the subsampling.
*
* @see MosaicImageReadParam#setSubsamplingChangeAllowed
*/
public synchronized void setSubsamplingChangeAllowed(final boolean allowed) {
subsamplingChangeAllowed = allowed;
}
/**
* Returns an empirical estimation of the efficiency of loading images using the mosaic. This
* method creates the given amount of random rectangles in the area of the mosaic, then
* estimates the theorical cost that loading those images would have. This is a only a
* guess - the images are not really loaded.
* <p>
* The highest value that this method can return is 1, which means that <cite>optimal
* loading</cite> (defined below) would occur. Values lower than 1 are the average time
* of image loadings compared to the optimal case. For example a value of 0.5 means that the
* <cite>theorical image loading time</cite> (defined below) is on average two time greater
* than it would be if it was possible to read all images optimally.
*
* {@section Definition of terms}
* The "<cite>theorical image loading time</cite>" is defined as proportional to the amount of
* pixels to read, assuming that each tile is read in a "<cite>all or nothing</cite>" fashion.
* This is only a gross approximation of the reality since image compression induces non-linear
* relationship between the amount of pixels and the loading time, and some image formats like
* TIFF and JPEG2000 can be tiled - thus breaking the "all or nothing" assumption.
* <p>
* The "<cite>optimal loading</cite>" case is defined as the case where every pixels to be
* traversed while reading tiles are useful to the requested image, with no pixel to discart
* because of croping or subsampling.
*
* @param numSamples The number of rectangular region to simulate loading.
* @return The estimated efficiency of loading images, as a value between 0 and 1 inclusve.
* Higher values are better.
* @throws IOException If it was necessary to fetch an image dimension from its
* {@linkplain Tile#getImageReader reader} and this operation failed.
*/
public synchronized Statistics estimateEfficiency(int numSamples) throws IOException {
final int dsx = maxSubsampling.width - minSubsampling.width + 1;
final int dsy = maxSubsampling.height - minSubsampling.height + 1;
final int dw = maxSize.width - minSize.width + 1;
final int dh = maxSize.height - minSize.height + 1;
final Rectangle region = this.region; // Shared instance - do not modify!
final Rectangle sample = new Rectangle();
final Dimension subsampling = new Dimension();
final Statistics stats = new Statistics(null);
while (--numSamples >= 0) {
final int sx = minSubsampling.width + random.nextInt(dsx);
final int sy = minSubsampling.height + random.nextInt(dsy);
final int width = Math.min((minSize.width + random.nextInt(dw)) * sx, region.width);
final int height = Math.min((minSize.height + random.nextInt(dh)) * sy, region.height);
final int x = region.x + random.nextInt(region.width - width + 1);
final int y = region.y + random.nextInt(region.height - height + 1);
sample.setBounds(x, y, width, height);
assert region.contains(sample) : sample;
subsampling.setSize(sx, sy);
long cost = 0;
for (final Tile tile : mosaic.getTiles(sample, subsampling, subsamplingChangeAllowed)) {
cost += tile.countUnwantedPixelsFromAbsolute(sample, subsampling);
}
final long area = (long) width * (long) height / (subsampling.width * subsampling.height);
stats.accept(1 / ((double) cost / (double) area + 1));
}
return stats;
}
}