blob: 94c06b79e158d2d0a9a902f98d11161abeeb6976 [file] [log] [blame]
/*
* 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.image;
import java.awt.Rectangle;
import java.awt.image.Raster;
import java.awt.image.RenderedImage;
import java.awt.image.WritableRenderedImage;
import java.util.Objects;
import java.util.function.Consumer;
import javax.measure.Quantity;
import org.opengis.referencing.operation.MathTransform;
import org.apache.sis.coverage.privy.ImageLayout;
import org.apache.sis.coverage.privy.ImageUtilities;
import org.apache.sis.coverage.privy.TileOpExecutor;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.measure.Units;
/**
* Combines an arbitrary number of images into a single one.
* The combined images may use different coordinate systems if a resampling operation is specified.
* The workflow is as below:
*
* <ol>
* <li>Creates an {@code ImageCombiner} with the destination image where to write.</li>
* <li>Configure with methods such as {@link #setInterpolation setInterpolation(…)}.</li>
* <li>Invoke {@link #accept accept(…)} or {@link #resample resample(…)}
* methods for each image to combine.</li>
* <li>Get the combined image with {@link #result()}.</li>
* </ol>
*
* Images are combined in the order they are specified.
* If the same pixel is written by many images, then the final value is the pixel of the last image specified.
* In current implementation, the last pixel values win even if those pixels are transparent
* (i.e. {@code ImageCombiner} does not yet handle alpha values).
*
* <h2>Limitations</h2>
* Current implementation does not try to map source bands to target bands for the same colors.
* For example, it does not verify if band order needs to be reversed because an image is RGB and
* the other image is BVR. It is caller responsibility to ensure that bands are in the same order.
*
* <p>Current implementation does not expand the destination image for accommodating
* any area of a given image that appear outside the destination image bounds.
* Only the intersection of both images is used.</p>
*
* @author Martin Desruisseaux (Geomatys)
* @version 1.4
* @since 1.1
*/
public class ImageCombiner implements Consumer<RenderedImage> {
/**
* The image processor for resampling operation.
*/
private final ImageProcessor processor;
/**
* The destination image where to write the images given to this {@code ImageCombiner}.
*/
private final WritableRenderedImage destination;
/**
* Creates an image combiner which will write in the given image. That image is not cleared;
* pixels that are not overwritten by calls to the {@code accept(…)} or {@code resample(…)}
* methods will be left unchanged.
*
* @param destination the image where to combine images.
*/
public ImageCombiner(final WritableRenderedImage destination) {
this(destination, new ImageProcessor());
}
/**
* Creates an image combiner which will use the given processor for resampling operations.
* The given destination image is not cleared; pixels that are not overwritten by calls to
* the {@code accept(…)} or {@code resample(…)} methods will be left unchanged.
*
* @param destination the image where to combine images.
* @param processor the processor to use for resampling operations.
*
* @since 1.2
*/
public ImageCombiner(final WritableRenderedImage destination, final ImageProcessor processor) {
this.destination = Objects.requireNonNull(destination);
this.processor = Objects.requireNonNull(processor);
}
/**
* Returns the interpolation method to use during resample operations.
*
* @return interpolation method to use during resample operations.
*
* @see #resample(RenderedImage, Rectangle, MathTransform)
*/
public Interpolation getInterpolation() {
return processor.getInterpolation();
}
/**
* Sets the interpolation method to use during resample operations.
*
* @param method interpolation method to use during resample operations.
*
* @see #resample(RenderedImage, Rectangle, MathTransform)
*/
public void setInterpolation(final Interpolation method) {
processor.setInterpolation(method);
}
/**
* Returns hints about the desired positional accuracy, in "real world" units or in pixel units.
* If the returned array is non-empty and contains accuracies large enough,
* {@code ImageCombiner} may use some slightly faster algorithms at the expense of accuracy.
*
* @return desired accuracy in no particular order, or an empty array if none.
*
* @see ImageProcessor#getPositionalAccuracyHints()
*/
public Quantity<?>[] getPositionalAccuracyHints() {
return processor.getPositionalAccuracyHints();
}
/**
* Sets hints about desired positional accuracy, in "real world" units or in pixel units.
* Accuracy can be specified in real world units such as {@linkplain Units#METRE metres}
* or in {@linkplain Units#PIXEL pixel units}, which are converted to real world units depending
* on image resolution. If more than one value is applicable to a dimension
* (after unit conversion if needed), the smallest value is taken.
*
* @param hints desired accuracy in no particular order, or a {@code null} array if none.
* Null elements in the array are ignored.
*
* @see ImageProcessor#setPositionalAccuracyHints(Quantity...)
*/
public void setPositionalAccuracyHints(final Quantity<?>... hints) {
processor.setPositionalAccuracyHints(hints);
}
/**
* Returns the type of number used for representing the values of each band.
*
* @return the type of number capable to hold sample values of each band.
*
* @since 1.4
*/
public DataType getBandType() {
return DataType.forBands(destination);
}
/**
* Writes the given image on top of destination image. The given source image shall use the same pixel
* coordinate system than the destination image (but not necessarily the same tile indices).
* For every (<var>x</var>,<var>y</var>) pixel coordinates in the destination image:
*
* <ul>
* <li>If (<var>x</var>,<var>y</var>) are valid {@code source} pixel coordinates,
* then the source pixel values overwrite the destination pixel values.</li>
* <li>Otherwise the destination pixel is left unchanged.</li>
* </ul>
*
* Note that source pixels overwrite destination pixels even if they are transparent
* (i.e. {@code ImageCombiner} does not yet handle alpha values).
*
* @param source the image to write on top of destination image.
*/
@Override
public void accept(final RenderedImage source) {
ArgumentChecks.ensureNonNull("source", source);
@SuppressWarnings("LocalVariableHidesMemberVariable")
final WritableRenderedImage destination = this.destination;
final Rectangle bounds = ImageUtilities.getBounds(source);
ImageUtilities.clipBounds(destination, bounds);
if (!bounds.isEmpty()) {
final TileOpExecutor executor = new TileOpExecutor(source, bounds) {
@Override protected void readFrom(final Raster tile) {
destination.setData(tile);
}
};
executor.readFrom(processor.prefetch(source, bounds));
}
}
/**
* Combines the result of resampling the given image. The resampling operation is defined by a potentially
* non-linear transform from the <em>destination</em> image to the specified <em>source</em> image.
* That transform should map {@linkplain org.apache.sis.coverage.grid.PixelInCell#CELL_CENTER pixel centers}.
*
* <h4>Properties used</h4>
* This operation uses the following properties in addition to method parameters:
* <ul>
* <li>{@linkplain #getInterpolation() Interpolation method} (nearest neighbor, bilinear, <i>etc</i>).</li>
* <li>{@linkplain #getPositionalAccuracyHints() Positional accuracy hints}
* for enabling faster resampling at the cost of lower precision.</li>
* </ul>
*
* Contrarily to {@link ImageProcessor}, this method does not use {@linkplain ImageProcessor#getFillValues() fill values}.
* Destination pixels that cannot be mapped to source pixels are left unchanged.
*
* @param source the image to be resampled.
* @param bounds domain of pixel coordinates in the destination image, or {@code null} for the whole image.
* @param toSource conversion of pixel coordinates from destination image to {@code source} image.
*
* @see ImageProcessor#resample(RenderedImage, Rectangle, MathTransform)
*/
public void resample(final RenderedImage source, Rectangle bounds, final MathTransform toSource) {
ArgumentChecks.ensureNonNull("source", source);
ArgumentChecks.ensureNonNull("toSource", toSource);
if (bounds == null) {
bounds = ImageUtilities.getBounds(destination);
}
final int tileWidth = destination.getTileWidth();
final int tileHeight = destination.getTileHeight();
final long tileGridXOffset = destination.getTileGridXOffset();
final long tileGridYOffset = destination.getTileGridYOffset();
final int minTileX = Math.toIntExact(Math.floorDiv((bounds.x - tileGridXOffset), tileWidth));
final int minTileY = Math.toIntExact(Math.floorDiv((bounds.y - tileGridYOffset), tileHeight));
final int minX = Math.toIntExact(Math.multiplyFull(minTileX, tileWidth) + tileGridXOffset);
final int minY = Math.toIntExact(Math.multiplyFull(minTileY, tileHeight) + tileGridYOffset);
/*
* Expand the target bounds until it contains an integer number of tiles, computed using the size
* of destination tiles. We have to do that because the resample operation below is not free to
* choose a tile size suiting the given bounds.
*/
long maxX = (bounds.x + (long) bounds.width) - 1; // Inclusive.
long maxY = (bounds.y + (long) bounds.height) - 1;
maxX = ((maxX - tileGridXOffset) / tileWidth + 1) * tileWidth + tileGridXOffset; // Exclusive.
maxY = ((maxY - tileGridYOffset) / tileHeight + 1) * tileHeight + tileGridYOffset;
bounds = new Rectangle(minX, minY,
Math.toIntExact(maxX - minX),
Math.toIntExact(maxY - minY));
/*
* Values of (minTileX, minTileY) computed above will cause `ResampledImage.getTileGridOffset()`
* to return the exact same value as `destination.getTileGridOffset()`. This is a requirement
* of `setDestination(…)` method.
*/
final RenderedImage result;
synchronized (processor) {
final ImageLayout layout = ImageLayout.forDestination(destination, minTileX, minTileY);
final ImageLayout previous = processor.getImageLayout();
try {
processor.setImageLayout(layout);
result = processor.resample(source, bounds, toSource);
} finally {
processor.setImageLayout(previous);
}
}
/*
* Check if the result is writing directly in the destination image.
*/
if (result instanceof ComputedImage && ((ComputedImage) result).getDestination() == destination) {
processor.prefetch(result, ImageUtilities.getBounds(destination));
} else {
accept(result);
}
}
/**
* Returns the combination of destination image with all images specified to {@code ImageCombiner} methods.
* This may be the destination image specified at construction time, but may also be a larger image if the
* destination has been dynamically expanded for accommodating larger sources.
*
* <p><b>Note:</b> dynamic expansion is not yet implemented in current version.
* If a future version implements it, we shall guarantee that the coordinate of each pixel is unchanged
* (i.e. the image {@code minX} and {@code minY} may become negative, but the pixel identified by
* coordinates (0,0) for instance will stay the same pixel.)</p>
*
* @return the combination of destination image with all source images.
*/
public RenderedImage result() {
return destination;
}
}