/* * 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.awt.Rectangle; import java.awt.image.Raster; import java.awt.image.ColorModel; import java.awt.image.RenderedImage; import java.awt.image.WritableRaster; import javax.media.jai.ImageLayout; import javax.media.jai.UntiledOpImage; import org.geotoolkit.image.color.ColorUtilities; import org.geotoolkit.image.internal.ImageUtilities; import static javax.media.jai.ImageLayout.COLOR_MODEL_MASK; import static javax.media.jai.ImageLayout.SAMPLE_MODEL_MASK; /** * Creates the silhouette of an image that isolates its content from its background. * The background color is assumed to be uniform, with a know value which must be * specified as a parameter to this operation. * <p> * This operation can be used in order to build a mask for an image that has been rotated, * where pixels in the middle of the image should not have their value masked even if they * happen to have the same color than the background color. * <p> * Current implementation is rather simple and works for images in which the content * is inside a rotated rectangle. More complex outline are not guaranteed to work. * Example: * <p> * <table align="center" cellpadding="15" border="1"> * <tr><th>Input</th><th>output</th></tr><tr> * <td><img src="doc-files/sample-rgb.png" border="1"></td> * <td><img src="doc-files/silhouette.png" border="1"></td> * </tr></table> * * {@section Current algorithm} * This operator starts from a corner of the source image. If the pixel value in that corner is * equals to the specified background color, then the corresponding sample value in the target * image is set to -1 (for integer data type, this is equivalent to setting all bits to 1). * Otherwise the target sample value is set to 0. If and only if the pixel in the corner has * been set to -1, then its neighbors pixels are examined iteratively in the same way. This * operation is repeated for the 4 image corners. * * @author Martin Desruisseaux (Geomatys) * @version 3.01 * * @since 3.00 * @module */ public class SilhouetteMask extends UntiledOpImage { /** * The name of this operation in the JAI registry. * This is {@value}. */ public static final String OPERATION_NAME = "org.geotoolkit.SilhouetteMask"; /** * The background values in the source image. The array length must matches * the number of bands, otherwise this operation can not work. */ private final double[][] background; /** * Constructs a new silhouette mask 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 background The background values in the source image. */ public SilhouetteMask(final RenderedImage source, final ImageLayout layout, final Map<?,?> configuration, double[]... background) { super(source, configuration, layout(source, layout)); final int numBands = source.getSampleModel().getNumBands(); this.background = background = background.clone(); for (int i=0; i<background.length; i++) { background[i] = Arrays.copyOf(background[i], numBands); } } /** * If the user didn't specified explicitly a sample or a color model, creates default ones. * This method is actually a workaround for RFE #4093999 in Sun's bug database * ("Relax constraint on placement of this()/super() call in constructors"). * * @param layout The user-supplied layout. * @return A layout with at least a color model. */ private static ImageLayout layout(final RenderedImage source, ImageLayout layout) { if (layout == null) { layout = new ImageLayout(); } else if ((layout.getValidMask() & (SAMPLE_MODEL_MASK | COLOR_MODEL_MASK)) == 0) { layout = (ImageLayout) layout.clone(); } else { return layout; } final ColorModel cm = ColorUtilities.BINARY_COLOR_MODEL; return layout.setColorModel(cm).setSampleModel(cm.createCompatibleSampleModel( layout.getWidth(source), layout.getHeight(source))); } /** * Returns the source images. */ @Override @SuppressWarnings("unchecked") public Vector<RenderedImage> getSources() { return super.getSources(); } /** * Computes a rectangle of outputs. * * @param sources The source images. Should be an array of length 1. * @param dest The raster to be filled in. * @param destRect The region within the raster to be filled. */ @Override protected void computeImage(final Raster[] sources, final WritableRaster dest, final Rectangle destRect) { assert sources.length == 1; if (destRect.isEmpty()) return; final Raster source = sources[0]; if (false) { /* * According javax.media.jai.TileFactory javadoc (see method createTile), new tiles * should be initialized to 0 even if they were recycled from an older image. So we * should not need to invoke this fill method. */ ImageUtilities.fill(dest, destRect, 0); } final int xmin = destRect.x; final int ymin = destRect.y; final int xmax = destRect.width + xmin; final int ymax = destRect.height + ymin; final int[] ones = new int[dest.getNumBands()]; Arrays.fill(ones, -1); final int transferType = source.getTransferType(); final Set<SampleValues> background = new HashSet<>(); for (final double[] samples : this.background) { background.add(SampleValues.getInstance(transferType, samples)); } final SampleValues buffer = SampleValues.getInstance(transferType, source.getSampleModel().getNumBands()); assert source.getBounds().contains(destRect) : destRect; /* * For each background value found in the source image, sets the destination pixel to -1. * The same algorithm is repeated for the 4 corners, using only different iteration direction. */ for (int corner=0; corner<4; corner++) { int x = source.getMinX(); int y = source.getMinY(); int w = source.getWidth(); int h = source.getHeight(); int dx = 1, dy = 1; if ((corner & 1) != 0) {x += w - 1; dx = -1;} if ((corner & 2) != 0) {y += h - 1; dy = -1;} final int x0 = x; /* * Next line will be scanned only if the first pixel of the previous line had * the background value. Next pixel on a row will be scanned only as long as * background values are found. */ while (--h >= 0 && background.contains(buffer.getPixel(source, x, y))) { if (y >= ymin && y < ymax) { w = destRect.width; do if (x >= xmin && x < xmax) { dest.setPixel(x, y, ones); if (--w == 0) break; } while (background.contains(buffer.getPixel(source, x += dx, y))); } y += dy; x = x0; } } } }