/*
* 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.jai;
import java.util.Map;
import java.util.Set;
import java.util.Vector;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Collections;
import java.awt.Color;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.image.Raster;
import java.awt.image.RenderedImage;
import java.awt.image.WritableRaster;
import java.awt.image.WritableRenderedImage;
import javax.media.jai.OpImage;
import javax.media.jai.ImageLayout;
import org.apache.sis.util.collection.IntegerList;
import org.geotoolkit.image.color.ColorUtilities;
import org.geotoolkit.resources.Errors;
import static org.apache.sis.util.collection.Containers.hashMapCapacity;
/**
* Performs the Flood Fill operation on the given raster.
*
* {@section Algorithm}
* This class implements a <cite>Scan line flood fill</cite> algorithm as
* <a href="http://en.wikipedia.org/wiki/Flood_fill">documented in Wikipedia</a>
* on June 2009, section <cite>Alternative implementations</cite> (queue-based)
* modified as described in <cite>Scanline fill</cite> section. The algorithm
* has been modified in order to work properly with tiled images.
*
* @author Martin Desruisseaux (Geomatys)
* @version 3.01
*
* @since 3.01
* @module
*
* @todo This class is abstract for now because not yet implemented as a JAI operation.
* However the static methods are ready for use.
*/
public abstract class FloodFill extends OpImage {
/**
* The name of this operation in the JAI registry.
* This is {@value}.
*/
public static final String OPERATION_NAME = "org.geotoolkit.FloodFill";
/**
* The old values in the source image.
*/
private final double[][] oldValues;
/**
* The new values in the source image. The array length must matches the number of bands.
*/
private final double[] newValues;
/**
* Constructs a new Flood Fill for the given image. While this constructor is public, it
* should usually not be invoked directly. You should use {@linkplain javax.media.jai.JAI}
* factory methods instead.
*
* @param source The source image.
* @param layout The image layout.
* @param configuration The image properties and rendering hints.
* @param oldValues The old values in the source images.
* @param newValues The new values in the source images.
*/
public FloodFill(final RenderedImage source, final ImageLayout layout,
final Map<?,?> configuration, double[][] oldValues, double[] newValues)
{
super(new Vector<>(Collections.singleton(source)), layout, configuration, false);
final int numBands = source.getSampleModel().getNumBands();
this.newValues = newValues = newValues.clone();
this.oldValues = oldValues = oldValues.clone();
for (int i=0; i<oldValues.length; i++) {
oldValues[i] = Arrays.copyOf(oldValues[i], numBands);
}
}
/**
* Returns the source images.
*/
@Override
@SuppressWarnings("unchecked")
public Vector<RenderedImage> getSources() {
return super.getSources();
}
/**
* Colors an area of connected pixels with the same set of color.
* The fill is performed in place in the given image.
* The operation is performed immediately; it is not deferred like usual JAI operations.
*
* @param image The image in which to colors an area.
* @param oldColors The colors to replace (usually only 1 color, but more are allowed).
* @param newColors The new colors replacing the old ones.
* @param points The coordinate of the starting point. There is usually only one
* such point, but more are allowed.
*/
public static void fill(final WritableRenderedImage image, final Color[] oldColors,
final Color newColors, final Point... points)
{
final int numBands = image.getSampleModel().getNumBands();
final double[][] oldValues = new double[oldColors.length][];
for (int i=0; i<oldValues.length; i++) {
oldValues[i] = ColorUtilities.toDoubleValues(oldColors[i], numBands);
}
final double[] newValues = ColorUtilities.toDoubleValues(newColors, numBands);
fill(image, oldValues, newValues, points);
}
/**
* Colors an area of connected pixels with the same set of color.
* The fill is performed in place in the given image.
* The operation is performed immediately; it is not deferred like usual JAI operations.
*
* @param image The image in which to colors an area.
* @param oldValues The colors to replace (usually only 1 color, but more are allowed).
* @param newValues The new colors replacing the old ones.
* @param points The coordinate of the starting point. There is usually only one
* such point, but more are allowed.
*/
public static void fill(final WritableRenderedImage image, final double[][] oldValues,
final double[] newValues, final Point... points)
{
/*
* Copies the old values in a set of SampleValues objects. The exact type of SampleValues
* will depend on the transfer type.
*/
final int transferType = image.getSampleModel().getTransferType();
final Set<SampleValues> oldSamples = new HashSet<>(hashMapCapacity(oldValues.length));
for (final double[] samples : oldValues) {
oldSamples.add(SampleValues.getInstance(transferType, samples));
}
final SampleValues newSamples = SampleValues.getInstance(transferType, newValues);
oldSamples.remove(newSamples); // Necessary for avoiding infinite loop.
if (oldSamples.isEmpty()) {
return;
}
/*
* Copies the points coordinates in an IntegerList. The content of that list will be
* (x,y) tupples translated in such a way that the upper left pixel is located at (0,0)
* by definition (this is always the case with BufferedImage, but not necessarily with
* other kind of RenderedImage). Later we will call the fill(WritableRaster...) method
* in a loop as long as the list is not empty. Note that the fill(WritableRaster...)
* method may itself push more points in that list (can occur only if the image is tiled).
*/
final int xmin = image.getMinX();
final int ymin = image.getMinY();
final int width = image.getWidth();
final int height = image.getHeight();
final Rectangle bounds = new Rectangle(xmin, ymin, width, height);
if (bounds.isEmpty()) {
return;
}
final IntegerList stack = new IntegerList(8, Math.max(width, height)-1);
for (final Point point : points) {
int x = point.x - xmin;
int y = point.y - ymin;
if (x < 0 || x >= width || y < 0 || y >= height) {
throw new IllegalArgumentException(Errors.format(Errors.Keys.PointOutsideCoverage_1,
new StringBuilder().append(point.x).append(',').append(point.y).toString()));
}
stack.addInt(x);
stack.addInt(y);
}
/*
* Now process to the filling tile by tile.
*
* TODO: need to process together every points on the same tile, in order to avoid
* loading and flushing tiles too often.
*/
final int tileXOff = image.getTileGridXOffset();
final int tileYOff = image.getTileGridYOffset();
final int tileWidth = image.getTileWidth();
final int tileHeight = image.getTileHeight();
final int minTileY = image.getMinTileY();
final int maxTileY = image.getNumXTiles() + minTileY - 1; // inclusive.
while (!stack.isEmpty()) {
final int y = stack.removeLast() + ymin;
final int x = stack.removeLast() + xmin;
final int tileX = XToTileX(x, tileXOff, tileWidth);
final int tileY = YToTileY(y, tileYOff, tileHeight);
final Raster top = (tileY != minTileY) ? image.getTile(tileX, tileY-1) : null;
final Raster bottom = (tileY != maxTileY) ? image.getTile(tileX, tileY+1) : null;
final WritableRaster raster = image.getWritableTile(tileX, tileY);
try {
fill(raster, top, bottom, bounds, x, y, oldSamples, newSamples, stack);
} finally {
image.releaseWritableTile(tileX, tileY);
}
}
}
/**
* Process to the Scan Line Flood Fill in one tile. This method will be invoked for
* every tiles to process, and may be invoked more than once for the same tile.
* <p>
* Every access on {@code globalStack} will be performed in a synchronized block.
* This allow multi-threading, using a different thread for different tiles.
*
* @param raster The tile in which to apply the Scan Line Flood Fill.
* @param rasterTop The tile just above the given {@code raster}, or null if none.
* @param rasterBottom The tile just below the given {@code raster}, or null if none.
* @param imageBounds The bounds of the whole image (encompassing every tiles).
* @param x The <var>x</var> ordinate of the starting point.
* @param y The <var>y</var> ordinate of the starting point.
* @param oldValues The set of old colors to replace.
* @param newValues The new value to give to the filled pixels.
* @param globalStack Where to push the point needed further examination by other tiles.
*/
private static void fill(final WritableRaster raster, final Raster rasterTop,
final Raster rasterBottom, final Rectangle imageBounds, int x, int y,
final Set<SampleValues> oldValues, final SampleValues newValues,
final IntegerList globalStack)
{
final SampleValues buffer = newValues.instance();
if (!oldValues.contains(buffer.getPixel(raster, x, y))) {
return;
}
final int width = raster.getWidth();
final int height = raster.getHeight();
final int xmin = raster.getMinX();
final int ymin = raster.getMinY();
final int xmax = xmin + width - 1; // Inclusive
final int ymax = ymin + height - 1; // Inclusive
/*
* Prepares a stack of coordinates to be processed in successive passes of
* the loop below. Note that the coordinates in this stack are relative to
* the tile upper left corner, i.e. (xmin,ymin) must be subtracted. This
* is for allowing IntegerList to do its job (pack the data).
*/
final IntegerList stack = new IntegerList(128, Math.max(width, height)-1);
do {
/*
* Scans the current line toward the left. After this loop,
* (x,y) will be the location of the leftmost pixel to replace.
*/
assert x >= xmin && x <= xmax : x;
assert y >= ymin && y <= ymax : y;
do if (--x < xmin) {
// We have reached the left border. The inspection will need to continue in
// the tile at the left, if any. It will be caller's responsibility to use
// the information that we put in 'globalStack'.
if (imageBounds.contains(x,y)) {
synchronized (globalStack) {
globalStack.addInt(x - imageBounds.x);
globalStack.addInt(y - imageBounds.y);
}
}
break;
} while (oldValues.contains(buffer.getPixel(raster, x, y)));
x++;
/*
* The loop below scans toward the right as long as there is pixels to replace.
*/
boolean omitTopCheck = false;
boolean omitBottomCheck = false;
do {
newValues.setPixel(raster, x, y);
/*
* The do ... while loop below is executed exactly 2 times, for checking
* the pixel on top and on bottom of the (x,y) location.
*
* 1: (checkingTop == true ) checks the pixel at the (x, y-1) location.
* 2: (checkingTop == false) checks the pixel at the (x, y+1) location.
*/
boolean checkingTop = true;
boolean omitCheck = omitTopCheck;
int checkAt = y - 1;
Raster border = (y == ymin) ? rasterTop : raster;
while (true) {
/*
* Checks the pixel at the top or the bottom (depending on 'checkingTop' value)
* of the current (x,y) location. For every sequences of consecutive pixels to
* replace, push in the stack the location of the first pixel in that sequence.
*/
if (border != null && oldValues.contains(buffer.getPixel(border, x, checkAt)) != omitCheck) {
if ((omitCheck = !omitCheck) == true) {
if (border == raster) {
// Found a point which need further examination in this tile.
stack.addInt(x - xmin);
stack.addInt(checkAt - ymin);
} else if (imageBounds.contains(x, checkAt)) {
// Found a point which need further examination in an other tile.
synchronized (globalStack) {
globalStack.addInt(x - imageBounds.x);
globalStack.addInt(checkAt - imageBounds.y);
}
}
}
}
if (checkingTop) {
/*
* Just finished the first execution of the loop (checking the top pixel).
* Saves the "omitCheck" state and prepare the loop for the check of the
* bottom pixel.
*/
checkingTop = false;
omitTopCheck = omitCheck;
omitCheck = omitBottomCheck;
checkAt = y + 1;
border = (y == ymax) ? rasterBottom : raster;
continue;
}
/*
* Just finished the second execution of the loop (checking the bottom pixel).
* Saves the "omitCheck" state and we are done.
*/
omitBottomCheck = omitCheck;
break;
}
/*
* If we have not yet reached the right border, move one pixel to the right and
* continue the inspection of current row. If we have reached the right border,
* then the inspection will need to continue in the tile at the right, if any
* This is the same processing than we did for the left border.
*/
if (x++ == xmax) {
if (imageBounds.contains(x,y)) {
synchronized (globalStack) {
globalStack.addInt(x - imageBounds.x);
globalStack.addInt(y - imageBounds.y);
}
}
break;
}
} while (oldValues.contains(buffer.getPixel(raster, x, y)));
/*
* The above loop may have pushed additional points to process on the stack.
* If this is the case, extract the point on the top of the stack an continue.
* Otherwise we are done.
*/
if (stack.isEmpty()) {
break;
}
y = stack.removeLast() + ymin;
x = stack.removeLast() + xmin;
} while (true);
}
}