jaitools.media.jai.regionalize.RegionalizeOpImage Maven / Gradle / Ivy
Show all versions of jt-all Show documentation
/*
* Copyright 2009 Michael Bedward
*
* This file is part of jai-tools.
* jai-tools 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, either version 3 of the
* License, or (at your option) any later version.
* jai-tools 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.
* You should have received a copy of the GNU Lesser General Public
* License along with jai-tools. If not, see .
*
*/
package jaitools.media.jai.regionalize;
import jaitools.CollectionFactory;
import jaitools.imageutils.FillResult;
import jaitools.imageutils.FloodFiller;
import jaitools.tilecache.DiskMemTileCache;
import jaitools.tiledimage.DiskMemImage;
import java.awt.Rectangle;
import java.awt.image.DataBuffer;
import java.awt.image.Raster;
import java.awt.image.RenderedImage;
import java.util.Arrays;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javax.media.jai.AreaOpImage;
import javax.media.jai.ImageLayout;
import javax.media.jai.PointOpImage;
import javax.media.jai.TileCache;
/**
* An operator to identify regions of uniform value, within
* a user-specified tolerance, in the source image. Produces a
* destination image of these regions where pixel values are equal
* to region ID.
*
* To avoid region numbering artefacts on image tile boundaries this
* operator imposes an order on tile computation (by column within row).
* If an arbitrary tile is requested by the caller, the operator first
* checks that all of the preceding tiles have been computed and cached,
* processing any that have not. The operator creates its own
* {@link ExecutorService} for sequential tile computations.
*
* Each computed tile is cached using an instance of {@link DiskMemTileCache}.
* The caller can provide this to the operator via {@code RenderingHints}, or set
* it as the default {@code TileCache} using {@code JAI.getDefaultInstance().setTileCache()}.
* Otherwise the operator will create a {@code DiskMemTileCache} object for itself.
*
*
* @see RegionalizeDescriptor
* @see RegionData
*
* @author Michael Bedward
* @since 1.0
* @version $Id: RegionalizeOpImage.java 1383 2011-02-10 11:22:29Z michael.bedward $
*/
public class RegionalizeOpImage extends PointOpImage {
/**
* Destingation value indicating that the pixel does not
* belong to a region
*/
public static final int NO_REGION = 0;
private boolean singleBand;
private boolean diagonal;
private int band;
private double tolerance;
private final DiskMemImage regionImage;
private FloodFiller filler;
private Map regions;
int currentID;
private final ExecutorService executor;
private final Object getTileLock = new Object();
private final Object computeTileLock = new Object();
private boolean[] tileComputed;
private class ComputeTileTask implements Callable {
int tileX, tileY;
ComputeTileTask(int tileX, int tileY) {
this.tileX = tileX;
this.tileY = tileY;
}
public Raster call() throws Exception {
return computeTile(tileX, tileY);
}
}
/**
* Constructor
* @param source a RenderedImage.
* @param config configurable attributes of the image (see {@link AreaOpImage})
* @param layout an ImageLayout optionally containing the tile grid layout,
* SampleModel, and ColorModel, or null.
* @param band the band to process
* @param tolerance max absolute difference in value between the starting pixel for
* a region and any pixel added to that region
* @param diagonal true to include sub-regions with only diagonal connectedness;
* false to require orthogonal connectedness
*
* @see RegionalizeDescriptor
*/
public RegionalizeOpImage(RenderedImage source,
Map config,
ImageLayout layout,
int band,
double tolerance,
boolean diagonal) {
super(source, layout, config, false);
if (getSampleModel().getDataType() != DataBuffer.TYPE_INT) {
throw new IllegalStateException("destination sample model must be TYPE_INT");
}
this.band = band;
this.tolerance = tolerance;
/*
* @TODO remove later if we expand the operator to
* deal with multiple bands
*/
this.singleBand = true;
this.diagonal = diagonal;
/*
* Any tile cache provided by the caller is ignored
*/
regionImage = new DiskMemImage(getWidth(), getHeight(), getSampleModel());
setTileCache( regionImage.getTileCache() );
filler = new FloodFiller(source, band, regionImage, 0, tolerance, diagonal);
regions = CollectionFactory.sortedMap();
this.executor = Executors.newSingleThreadExecutor();
tileComputed = new boolean[getNumXTiles() * getNumYTiles()];
Arrays.fill(tileComputed, false); // paranoia
this.currentID = 1;
}
/**
* Get a property associated with this operator. Use this
* to retrieve the {@linkplain RegionData} object with the
* property name {@linkplain RegionalizeDescriptor#REGION_DATA_PROPERTY}
*
* @param name property name
* @return the matching object or null if there was no match
*/
@Override
public Object getProperty(String name) {
if (RegionalizeDescriptor.REGION_DATA_PROPERTY.equalsIgnoreCase(name)) {
List regionData = CollectionFactory.list();
regionData.addAll(regions.values());
return regionData;
} else {
return super.getProperty(name);
}
}
/**
* Get the properties for this operator. These will
* include the {@linkplain RegionData} object
*/
@Override
public Hashtable getProperties() {
Hashtable props = super.getProperties();
if (props == null) {
props = new Hashtable();
}
props.put(RegionalizeDescriptor.REGION_DATA_PROPERTY, "dynamic");
return props;
}
/**
* For internal use
*/
@Override
public Class> getPropertyClass(String name) {
if (RegionalizeDescriptor.REGION_DATA_PROPERTY.equalsIgnoreCase(name)) {
return List.class;
} else {
return super.getPropertyClass(name);
}
}
/**
* For internal use
*/
@Override
public String[] getPropertyNames() {
String[] superNames = super.getPropertyNames();
int len = superNames != null ? superNames.length + 1 : 1;
String[] names = new String[len];
int k = 0;
if (len > 1) {
for (String name : superNames) {
names[k++] = name;
}
}
names[k] = RegionalizeDescriptor.REGION_DATA_PROPERTY;
return names;
}
/**
* Returns a tile of this image as a Raster
. If the
* requested tile is completely outside of this image's bounds,
* this method returns null
.
*
* The nature of the regionalizing algorithm means that to compute
* any tile other than the first (top left) we must compute
* all tiles to avoid region numbering artefacts across
* tile boundaries.
*
* @param tileX The X index of the tile.
* @param tileY The Y index of the tile.
*/
@Override
public Raster getTile(int tileX, int tileY) {
Raster tile = null;
if (tileX >= getMinTileX() && tileX <= getMaxTileX() &&
tileY >= getMinTileY() && tileY <= getMaxTileY()) {
if (tileComputed[getTileIndex(tileX, tileY)]) {
tile = regionImage.getTile(tileX, tileY);
} else {
synchronized (getTileLock) {
try {
tile = executor.submit(new ComputeTileTask(tileX, tileY)).get();
} catch (ExecutionException execEx) {
throw new IllegalStateException(execEx);
} catch (InterruptedException intEx) {
// @todo is this safe ?
return null;
}
}
}
}
return tile;
}
@Override
public Raster computeTile(int tileX, int tileY) {
Rectangle destRect = getTileRect(tileX, tileY);
synchronized (computeTileLock) {
for (int destY = destRect.y, row = 0; row < destRect.height; destY++, row++) {
for (int destX = destRect.x, col = 0; col < destRect.width; destX++, col++) {
if (getRegionForPixel(destX, destY) == NO_REGION) {
FillResult fill = filler.fill(destX, destY, currentID);
regions.put(currentID, new Region(fill));
currentID++;
}
}
}
tileComputed[getTileIndex(tileX, tileY)] = true;
}
return regionImage.getTile(tileX, tileY);
}
/**
* Convenience method to calculate a single value tile coordinate
* @param tileX tile X coordinate
* @param tileY tile Y coordinate
* @return single integer coordinate used to index fields in this class
*/
private int getTileIndex(int tileX, int tileY) {
return (tileY - getMinTileY()) * getNumXTiles() + (tileX - getMinTileX());
}
/**
* This method is overridden to prevent it being used because the
* {@code RegionalizeOpImage} object should be soley responsible for
* creating tiles and caching them. If invoked an
* {@linkplain UnsupportedOperationException} will be thrown.
*/
@Override
protected void addTileToCache(int tileX, int tileY, Raster tile) {
throw new UnsupportedOperationException("this method should not be called !");
}
/**
* This method is overridden to ensure that the cache is always addressed
* through the {@code DiskMemImage} being used by this operator, otherwise
* tile IDs calculated by the cache will vary with the perceived owner
* (the image or the operator) of the tile.
*
* @param tileX tile X coordinate
* @param tileY tile Y coordinate
* @return the requested tile
*/
@Override
protected Raster getTileFromCache(int tileX, int tileY) {
return regionImage.getTile(tileX, tileY);
}
/**
* Set the tile cache. The supplied cache must be an instance of
* {@code DiskMemTileCache}.
*
* @param cache an instance of DiskMemTileCache
* @throws IllegalArgumentException if cache is null or not an instance
* of {@code DiskMemTileCache}
*/
@Override
public void setTileCache(TileCache cache) {
if (cache != null && cache instanceof DiskMemTileCache) {
super.setTileCache(cache);
} else {
throw new IllegalArgumentException("cache must be an instance of DiskMemTileCache");
}
}
/**
* Get the ID of the region that contains the given pixel
* position
*
* @return the id of the region that contains this pixel OR
* NO_REGION if the pixel hasn't been processed
*/
private int getRegionForPixel(int x, int y) {
int tileX = XToTileX(x);
int tileY = YToTileY(y);
Raster tile = regionImage.getTile(tileX, tileY);
assert(tile != null);
return tile.getSample(x, y, 0);
}
}