use of org.geotoolkit.image.palette.Palette in project geotoolkit by Geomatys.
the class SpatialImageReader method getImageType.
/**
* Returns an image type specifier indicating the {@link SampleModel} and {@link ColorModel}
* to use for reading the image. In addition, this method also detects if some conversions
* (represented by {@link SampleConverter} instances) are required in order to store the
* sample values using the selected models. The conversions (if any) are keept as small as
* possible, but are sometime impossible to avoid for example because {@link IndexColorModel}
* does not allow negative sample values.
* <p>
* The default implementation applies the following steps:
*
* <ol>
* <li><p>The {@linkplain SampleDimension#getValidSampleValues() range of expected values}
* and the {@linkplain SampleDimension#getFillSampleValues() fill values} are extracted
* from the {@linkplain #getImageMetadata(int) image metadata}, if any.</p></li>
*
* <li><p>If the given {@code parameters} argument is an instance of {@link SpatialImageReadParam},
* then the user-supplied {@linkplain SpatialImageReadParam#getPaletteName palette name}
* is fetched. Otherwise or if no palette name was explicitly set, then this method default
* to {@value org.geotoolkit.image.io.SpatialImageReadParam#DEFAULT_PALETTE_NAME}. The
* palette name will be used in order to {@linkplain PaletteFactory#getColors(String)
* read a predefined set of colors} (as [A]RGB values) to be given to the
* {@linkplain IndexColorModel index color model}.</p></li>
*
* <li><p>If the {@linkplain #getRawDataType raw data type} is {@link DataBuffer#TYPE_FLOAT
* TYPE_FLOAT} or {@link DataBuffer#TYPE_DOUBLE TYPE_DOUBLE}, then this method builds
* a {@linkplain PaletteFactory#getContinuousPalette continuous palette} suitable for
* the range fetched at step 1. The data are assumed <cite>geophysics</cite> values
* rather than some packed values. Consequently, the {@linkplain SampleConverter sample
* converters} will replace no-data values by {@linkplain Float#NaN NaN}, but no other
* changes will be applied.</p></li>
*
* <li><p>Otherwise, if the {@linkplain #getRawDataType raw data type} is a unsigned integer type
* like {@link DataBuffer#TYPE_BYTE TYPE_BYTE} or {@link DataBuffer#TYPE_USHORT TYPE_USHORT},
* then this method builds an {@linkplain PaletteFactory#getPalette indexed palette} (i.e. a
* palette backed by an {@linkplain IndexColorModel index color model}) with just the minimal
* {@linkplain IndexColorModel#getMapSize size} needed for containing fully the range and the
* no-data values fetched at step 1. The data are assumed <cite>packed</cite> values rather
* than geophysics values. Consequently, the {@linkplain SampleConverter sample converters}
* will be the {@linkplain SampleConverter#IDENTITY identity converter} except in the
* following cases:
* <ul>
* <li>The {@linkplain SampleDimension#getValidSampleValues() range of valid values} is
* outside the range allowed by the {@linkplain #getRawDataType raw data type} (e.g.
* the range of valid values contains negative integers). In this case, the sample
* converter will shift the values to a strictly positive range and replace fill
* values by 0.</li>
* <li>At least one {@linkplain SampleDimension#getFillSampleValues() fill value} is
* outside the range of values allowed by the {@linkplain #getRawDataType raw data
* type}. In this case, this method will try to only replace the fill values by 0,
* without shifting the valid values if this shift can be avoided.</li>
* <li>At least one {@linkplain SampleDimension#getFillSampleValues() fill value} is
* far away from the {@linkplain SampleDimension#getValidSampleValues() range of
* valid values} (for example 9999 while the range of valid values is [0…255]).
* The meaning of "far away" is determined by the {@link #collapseNoDataValues
* collapseNoDataValues} method.</li>
* </ul>
* </p></li>
*
* <li><p>Otherwise, if the {@linkplain #getRawDataType raw data type} is a signed integer
* type like {@link DataBuffer#TYPE_SHORT TYPE_SHORT}, then this method builds an
* {@linkplain PaletteFactory#getPalette indexed palette} with the maximal {@linkplain
* IndexColorModel#getMapSize size} supported by the raw data type (note that this is
* memory expensive - typically 256 kilobytes). Negative values will be stored in their
* two's complement binary form in order to fit in the range of positive integers
* supported by the {@linkplain IndexColorModel index color model}.</p></li>
* </ol>
*
* {@section Using the Sample Converters}
* If the {@code converters} argument is non-null, then this method will store the
* {@link SampleConverter} instances in the supplied array. The array length shall be equals
* to the number of {@linkplain ImageReadParam#getSourceBands() source} and
* {@linkplain ImageReadParam#getDestinationBands() destination bands}.
* <p>
* The converters shall be used by {@link #read(int,ImageReadParam) read} method
* implementations for converting the values read in the datafile to values acceptable
* by the {@linkplain ColorModel color model}. See the
* {@link #getDestination(int, ImageReadParam, int, int, SampleConverter[]) getDestination}
* method for code example.
*
* {@section Overriding this method}
* Subclasses can override this method for example if the color {@linkplain Palette palette}
* and range of values should be computed in a different way. The example below creates an
* image type using hard-coded objects:
*
* {@preformat java
* int minimum = -2000; // Minimal expected value
* int maximum = +2300; // Maximal expected value
* int fillValue = -9999; // Value for missing data
* String colors = "rainbow"; // Named set of RGB colors
* converters[0] = SampleConverter.createOffset(1 - minimum, fillValue);
* Palette palette = PaletteFactory.getDefault().getPalettePadValueFirst(colors, maximum - minimum);
* return palette.getImageTypeSpecifier();
* }
*
* @param imageIndex
* The index of the image to be queried.
* @param parameters
* The user-supplied parameters, or {@code null}. Note: we recommend to supply
* {@link #getDefaultReadParam} instead of {@code null} since subclasses may
* override the later with default values suitable to a particular format.
* @param converters
* If non-null, an array where to store the converters created by this method.
* The length of this array shall be equals to the number of target bands.
* @return
* The image type (never {@code null}).
* @throws IOException
* If an error occurs while reading the format information from the input source.
*
* @see #getRawDataType
* @see #collapseNoDataValues
* @see #getDestination(int, ImageReadParam, int, int, SampleConverter[])
*/
@SuppressWarnings("fallthrough")
protected ImageTypeSpecifier getImageType(final int imageIndex, final ImageReadParam parameters, final SampleConverter[] converters) throws IOException {
/*
* Extracts all informations we will need from the user-supplied parameters, if any.
* Note: the number of bands in the target image (as requested by the caller)
* may be different than the number of bands in the source image (on disk).
*/
final ImageTypeSpecifier userType;
final String paletteName;
final int[] sourceBands;
final int[] targetBands;
final int visibleBand;
final int numBands;
if (parameters != null) {
sourceBands = parameters.getSourceBands();
targetBands = parameters.getDestinationBands();
userType = parameters.getDestinationType();
} else {
sourceBands = null;
targetBands = null;
userType = null;
}
if (sourceBands != null) {
// == targetBands.length (assuming valid ImageReadParam).
numBands = sourceBands.length;
} else if (targetBands != null) {
numBands = targetBands.length;
} else {
numBands = getNumBands(imageIndex);
}
List<? extends SampleDomain> bands = null;
if (parameters instanceof SpatialImageReadParam) {
final SpatialImageReadParam geoparam = (SpatialImageReadParam) parameters;
paletteName = geoparam.getNonNullPaletteName();
visibleBand = geoparam.getVisibleBand();
bands = geoparam.getSampleDomains();
} else {
paletteName = SpatialImageReadParam.DEFAULT_PALETTE_NAME;
visibleBand = 0;
}
/*
* Gets the band metadata. If the user specified explicitly a SampleDomain in the
* parameters, this is all the information we need - so we can avoid the cost of
* querying IIOMetadata. Otherwise we will need to extract the image IIOMetadata.
*/
boolean convertBandIndices = false;
if (bands == null) {
final SpatialMetadata metadata;
final boolean oldIgnore = ignoreMetadata;
try {
ignoreMetadata = false;
metadata = getImageMetadata(imageIndex);
} finally {
ignoreMetadata = oldIgnore;
}
if (metadata != null) {
final List<SampleDimension> sd = metadata.getListForType(SampleDimension.class);
if (!isNullOrEmpty(sd)) {
convertBandIndices = (sourceBands != null);
bands = sd;
}
}
}
/*
* Gets the data type, and check if we should replace it by an other type. Type
* replacements are allowed only if the appropriate SampleConversionType enum is set.
*/
boolean replaceFillValues = false;
int dataType = (userType != null) ? userType.getSampleModel().getDataType() : getRawDataType(imageIndex);
if (userType == null && parameters instanceof SpatialImageReadParam) {
final SpatialImageReadParam geoparam = (SpatialImageReadParam) parameters;
switch(dataType) {
case DataBuffer.TYPE_SHORT:
{
if (geoparam.isSampleConversionAllowed(SHIFT_SIGNED_INTEGERS)) {
dataType = DataBuffer.TYPE_USHORT;
}
// Fall through
}
case DataBuffer.TYPE_USHORT:
case DataBuffer.TYPE_INT:
case DataBuffer.TYPE_BYTE:
{
if (bands == null || !geoparam.isSampleConversionAllowed(STORE_AS_FLOATS)) {
break;
}
boolean hasFillValues = false;
for (final SampleDomain domain : bands) {
final double[] fillValues = domain.getFillSampleValues();
if (fillValues != null && fillValues.length != 0) {
hasFillValues = true;
break;
}
}
if (!hasFillValues) {
break;
}
dataType = DataBuffer.TYPE_FLOAT;
// Fall through
}
case DataBuffer.TYPE_FLOAT:
case DataBuffer.TYPE_DOUBLE:
{
replaceFillValues = geoparam.isSampleConversionAllowed(REPLACE_FILL_VALUES);
}
}
}
/*
* Gets the minimal and maximal values allowed for the target image type.
* Note that this is meanless for floating point types, so the values in
* that case are arbitrary.
*
* The only integer types that are signed are SHORT (not to be confused with
* USHORT) and INT. Other types like BYTE and USHORT are treated as unsigned.
*/
final boolean isFloat;
final long floor, ceil;
switch(dataType) {
// Actually we don't really know what to do for this case...
case DataBuffer.TYPE_UNDEFINED:
// Fall through since we can treat this case as float.
case DataBuffer.TYPE_DOUBLE:
case DataBuffer.TYPE_FLOAT:
{
isFloat = true;
floor = Long.MIN_VALUE;
ceil = Long.MAX_VALUE;
break;
}
case DataBuffer.TYPE_INT:
{
isFloat = false;
floor = Integer.MIN_VALUE;
ceil = Integer.MAX_VALUE;
break;
}
case DataBuffer.TYPE_SHORT:
{
isFloat = false;
floor = Short.MIN_VALUE;
ceil = Short.MAX_VALUE;
break;
}
default:
{
isFloat = false;
floor = 0;
ceil = (1L << DataBuffer.getDataTypeSize(dataType)) - 1;
break;
}
}
/*
* Computes a range of values for all bands, as the union in order to make sure that
* we can stores every sample values. Also creates SampleConverters in the process.
* The later is an opportunist action since we gather most of the needed information
* during the loop.
*/
NumberRange<?> allRanges = null;
NumberRange<?> visibleRange = null;
SampleConverter visibleConverter = SampleConverter.IDENTITY;
// Only in the visible band, and must be positive.
double maximumFillValue = 0;
if (bands != null) {
// To be created only if needed.
MetadataHelper helper = null;
// Never 0 - check was performed above.
final int numMetadataBands = bands.size();
for (int i = 0; i < numBands; i++) {
int bandIndex = convertBandIndices ? sourceBands[i] : i;
if (bandIndex < 0 || bandIndex >= numMetadataBands) {
if (numMetadataBands != 1) {
// If there is exactly one metadata band, don't log any warning since
// we will assume that the metadata band apply to all data bands.
Warnings.log(this, null, SpatialImageReader.class, "getImageType", indexOutOfBounds(bandIndex, 0, numMetadataBands));
}
bandIndex = numMetadataBands - 1;
}
/*
* Before to get the range, get the fill values with maximal precision.
* Some values may need to be casted from 'double' to 'float' in order
* to match the sample values in the raster. This cast to various types
* will be performed internally by the SampleConverter implementations.
*/
final SampleDomain band = bands.get(bandIndex);
final double[] fillValues = band.getFillSampleValues();
final NumberRange<?> range;
if (band instanceof SampleDimension) {
if (helper == null) {
helper = new MetadataHelper(this);
}
range = helper.getValidSampleValues(bandIndex, (SampleDimension) band, fillValues);
} else {
range = band.getValidSampleValues();
}
double minimum, maximum;
if (range != null) {
minimum = range.getMinDouble();
maximum = range.getMaxDouble();
if (!isFloat) {
// treat as if we use the maximal range allowed by the data type.
if (minimum == Double.NEGATIVE_INFINITY)
minimum = floor;
if (maximum == Double.POSITIVE_INFINITY)
maximum = ceil;
}
final double extent = maximum - minimum;
if (extent >= 0 && (isFloat || extent <= (ceil - floor))) {
allRanges = (allRanges != null) ? allRanges.unionAny(range) : range;
} else {
// Use range.getMin/MaxValue() because they may be integers rather than doubles.
Warnings.log(this, null, SpatialImageReader.class, "getImageType", Errors.Keys.IllegalRange_2, range.getMinValue(), range.getMaxValue());
continue;
}
} else {
minimum = Double.NaN;
maximum = Double.NaN;
}
final int targetBand = (targetBands != null) ? targetBands[i] : i;
/*
* For floating point types, replaces no-data values by NaN because the floating
* point numbers are typically used for geophysics data, so the raster is likely
* to be a "geophysics" view for GridCoverage2D. All other values are stored "as
* is" without any offset.
*
* For integer types, if the range of values from the source data file fits into
* the range of values allowed by the destination raster, we will use an identity
* converter. If the only required conversion is a shift from negative to positive
* values, creates an offset converter with no-data values collapsed to 0.
*/
final SampleConverter converter;
if (isFloat) {
// If the sample values are float values, we need to replace 99.99 fill value
// (for example) by 99.99f, which is 99.98999786376953 in double precision,
// otherwise the SampleConverter may not find them (denpending which method
// is invoked). This cast is done by the PadValueMask constructor.
converter = replaceFillValues ? SampleConverter.createPadValuesMask(fillValues) : SampleConverter.IDENTITY;
} else {
final boolean isZeroValid = (minimum <= 0 && maximum >= 0);
boolean collapsePadValues = false;
if (fillValues != null && fillValues.length != 0) {
final double[] sorted = fillValues.clone();
Arrays.sort(sorted);
double minFill = sorted[0];
double maxFill = minFill;
int indexMax = sorted.length;
while (--indexMax != 0 && Double.isNaN(maxFill = sorted[indexMax])) ;
assert minFill <= maxFill || Double.isNaN(minFill) : maxFill;
if (targetBand == visibleBand && maxFill > maximumFillValue) {
maximumFillValue = maxFill;
}
if (minFill < floor || maxFill > ceil) {
// At least one fill value is outside the range of acceptable values.
collapsePadValues = true;
} else if (minimum >= 0) {
/*
* Arbitrary optimization of memory usage: if there is a "large" empty
* space between the range of valid values and a no-data value, then we
* may (at subclass implementors choice) collapse the no-data values to
* zero in order to avoid wasting the empty space. Note that we do not
* perform this collapse if the valid range contains negative values
* because it would not save any memory. We do not check the no-data
* values between 0 and 'minimum' for the same reason.
*/
int k = Arrays.binarySearch(sorted, maximum);
if (// We want the first element greater than maximum.
k >= 0)
// We want the first element greater than maximum.
k++;
else
// Really ~ operator, not -
k = ~k;
if (k <= indexMax) {
double unusedSpace = Math.max(sorted[k] - maximum - 1, 0);
while (++k <= indexMax) {
final double delta = sorted[k] - sorted[k - 1] - 1;
if (delta > 0) {
unusedSpace += delta;
}
}
final int unused = (int) Math.min(Math.round(unusedSpace), Integer.MAX_VALUE);
collapsePadValues = collapseNoDataValues(isZeroValid, sorted, unused);
// We invoked 'collapseNoDataValues' unconditionally even if
// 'unused' is zero because the user may decide on the basis
// of other criterions, like 'isZeroValid'.
}
}
}
if (minimum < floor || maximum > ceil) {
// The range of valid values is outside the range allowed by raw data type.
converter = SampleConverter.createOffset(Math.ceil(1 - minimum), fillValues);
} else if (collapsePadValues) {
if (isZeroValid) {
// We need to collapse the no-data values to 0, but it causes a clash
// with the range of valid values. So we also shift the later.
converter = SampleConverter.createOffset(Math.ceil(1 - minimum), fillValues);
} else {
// We need to collapse the no-data values and there is no clash.
converter = SampleConverter.createPadValuesMask(fillValues);
}
} else {
/*
* Do NOT take 'fillValues' in account if there is no need to collapse
* them. This is not the converter's job to transform "packed" values to
* "geophysics" values. We just want them to fit in the IndexColorModel,
* and they already fit. So the identity converter is appropriate even
* in presence of pad values.
*/
converter = SampleConverter.IDENTITY;
}
}
if (converters != null && i < converters.length) {
converters[i] = converter;
}
if (targetBand == visibleBand) {
visibleConverter = converter;
visibleRange = range;
}
}
}
/*
* Ensure that all converters are defined. We typically have no converter if there
* is no "ImageDescription/Dimensions" metadata. If the user specified explicitly
* the image type, then we are done.
*/
if (converters != null) {
for (int i = Math.min(converters.length, numBands); --i >= 0; ) {
if (converters[i] == null) {
converters[i] = visibleConverter;
}
}
}
if (userType != null) {
return userType;
}
/*
* Creates a color palette suitable for the range of values in the visible band.
* The case for floating points is the simplest: we should not have any offset,
* at most a replacement of no-data values. In the case of integer values, we
* must make sure that the indexed color map is large enough for containing both
* the highest data value and the highest no-data value.
*/
if (visibleRange == null) {
visibleRange = (allRanges != null) ? allRanges : NumberRange.create(floor, true, ceil, true);
}
PaletteFactory factory = null;
if (parameters instanceof SpatialImageReadParam) {
factory = ((SpatialImageReadParam) parameters).getPaletteFactory();
}
if (factory == null) {
factory = PaletteFactory.getDefault();
}
factory.setWarningLocale(locale);
final double minimum = visibleRange.getMinDouble();
final double maximum = visibleRange.getMaxDouble();
final Palette palette;
if (isFloat) {
assert visibleConverter.getOffset() == 0 : visibleConverter;
palette = factory.getContinuousPalette(paletteName, (float) minimum, (float) maximum, dataType, numBands, visibleBand);
} else {
final double offset = visibleConverter.getOffset();
long lower, upper;
if (minimum == Double.NEGATIVE_INFINITY) {
lower = floor;
} else {
lower = Math.round(minimum + offset);
if (!visibleRange.isMinIncluded()) {
// Must be inclusive
lower++;
}
}
if (maximum == Double.POSITIVE_INFINITY) {
upper = ceil;
} else {
upper = Math.round(maximum + offset);
if (visibleRange.isMaxIncluded()) {
// Must be exclusive
upper++;
}
}
long size = Math.max(upper, Math.round(maximumFillValue) + 1);
if (lower < 0) {
size -= lower;
}
/*
* The target lower, upper and size parameters are usually in the range of SHORT
* or USHORT data type. The Palette class will perform the necessary checks and
* throw an exception if those variables are out of range. However we may have
* values out of this range for TYPE_INT, in which case we will use the same slow
* color model than the one for floating point values.
*/
if (lower >= Short.MIN_VALUE && (lower + size) <= (lower >= 0 ? IndexedPalette.MAX_UNSIGNED + 1 : Short.MAX_VALUE + 1)) {
palette = factory.getPalette(paletteName, (int) lower, (int) upper, (int) size, numBands, visibleBand);
} else {
palette = factory.getContinuousPalette(paletteName, lower, upper, dataType, numBands, visibleBand);
}
}
return palette.getImageTypeSpecifier();
}
use of org.geotoolkit.image.palette.Palette in project geotoolkit by Geomatys.
the class PaletteCreatorDemo method main.
/**
* @param args the command line arguments
*/
public static void main(String[] args) throws FileNotFoundException, IOException {
final Palette palette = PALETTE_FACTORY.getPalette(PALETTE_NAME, NB_COLORS);
final IndexColorModel icm = (IndexColorModel) palette.getColorModel();
for (int i = 0; i < NB_COLORS; i++) {
final Color color = new Color(icm.getRGB(i));
final String hexColor = Integer.toHexString(color.getRGB()).substring(2);
System.out.println("RGB for " + i + " : " + color + " | hexadecimal : #" + hexColor);
}
}
use of org.geotoolkit.image.palette.Palette in project geotoolkit by Geomatys.
the class DefaultJenks method evaluate.
@Override
public Object evaluate(Object object, Class context) {
if (object instanceof RenderedImage) {
final RenderedImage image = (RenderedImage) object;
final int dataType = image.getSampleModel().getDataType();
final Raster data = image.getData();
int classes = (Integer) this.classNumber.getValue();
final int numBands = data.getNumBands();
final int width = data.getWidth();
final int height = data.getHeight();
final Set<Double> values = new TreeSet<Double>();
double[] pixel = new double[numBands];
Double key = Double.NaN;
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
data.getPixel(x, y, pixel);
// arbitrary only get the value ofthe first band
// TODO add bandIndex input parameter in Jenks function
key = Double.valueOf(pixel[0]);
// bypass noData values
if (Arrays.binarySearch(noData, key) < 0 && !values.contains(key)) {
values.add(key);
}
}
}
// prevent classification errors if requested classes is superior to computable classe number.
final int computableClasses = values.size();
if (classes > computableClasses) {
classes = computableClasses;
LOGGER.log(Level.WARNING, "Not enough distinct data to compute the requested number of class. Jenks will be computed for {0} classes.", classes);
}
final double[] pixelValues = new double[values.size()];
int index = 0;
for (Double val : values) {
pixelValues[index] = val.doubleValue();
index++;
}
// compute classes
final Classification classification = new Classification();
classification.setData(pixelValues);
classification.setClassNumber(classes);
classification.computeJenks(false);
final int[] indexes = classification.getIndex();
// create palette
final List<Color> colors = new ArrayList<Color>();
try {
final Palette palette = PALETTE_FACTORY.getPalette((String) paletteName.getValue(), classes);
final IndexColorModel icm = (IndexColorModel) palette.getColorModel();
for (int i = 0; i < classes; i++) {
colors.add(new Color(icm.getRGB(i)));
}
} catch (IOException ex) {
LOGGER.log(Level.WARNING, "Palette not found.", ex);
}
colorMap.clear();
colorMap.put(Double.NEGATIVE_INFINITY, new Color(0, 0, 0, 0));
for (int i = 0; i < indexes.length; i++) {
colorMap.put(pixelValues[indexes[i] - 1], colors.get(i));
}
for (int i = 0; i < noData.length; i++) {
colorMap.put(noData[i], new Color(0, 0, 0, 0));
}
/*
* HACK byte -> no-data = 255 else no-data = Double.NaN
* TODO find more elegant way to support no-data values.
*/
if (dataType == DataBuffer.TYPE_BYTE) {
colorMap.put(255.0, new Color(0, 0, 0, 0));
}
final ColorModel originColorModel = image.getColorModel();
final ColorModel newColorModel = new CompatibleColorModel(originColorModel.getPixelSize(), new JenksCategorize(colorMap));
/*
* Gives the color model to the image layout and creates a new image using the Null
* operation, which merely propagates its first source along the operation chain
* unmodified (except for the ColorModel given in the layout in this case).
*/
final ImageLayout layout = new ImageLayout().setColorModel(newColorModel);
return new NullOpImage(image, layout, null, OpImage.OP_COMPUTE_BOUND);
}
return null;
}
Aggregations