use of org.geotoolkit.image.io.metadata.SampleDimension 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.io.metadata.SampleDimension in project geotoolkit by Geomatys.
the class AsciiGridWriter method prepareHeader.
/**
* Fills the given {@code header} map with values extracted from the given image metadata.
* The {@code "NCOLS"} and {@code "NROWS"} attributes are already defined when this method
* is invoked. This method is responsible for filling the remaining attributes.
*
* @param metadata The metadata.
* @param header The map in which to store the (<var>key</var>, <var>value</var>) pairs
* to be written.
* @return The fill value, or {@code Double#NaN} if none.
* @throws IOException If the metadata can not be prepared.
*/
private String prepareHeader(final SpatialMetadata metadata, final Map<String, String> header, final ImageWriteParam param) throws IOException {
final MetadataHelper helper = new MetadataHelper(this);
final Georectified spatialRp = metadata.getInstanceForType(Georectified.class);
final RectifiedGrid domain = metadata.getInstanceForType(RectifiedGrid.class);
final PixelOrientation ptInPixel = (spatialRp != null) ? spatialRp.getPointInPixel() : null;
final AffineTransform gridToCRS = helper.getAffineTransform(domain, param);
String xll = "XLLCORNER";
String yll = "YLLCORNER";
// reverted (i.e. the corresponding value in OffsetVectors is negative).
if (ptInPixel != null && !ptInPixel.equals(PixelOrientation.UPPER_LEFT)) {
if (ptInPixel.equals(PixelOrientation.CENTER)) {
xll = "XLLCENTER";
yll = "YLLCENTER";
} else if (ptInPixel.equals(PixelOrientation.valueOf("UPPER"))) {
yll = "YLLCENTER";
} else if (ptInPixel.equals(PixelOrientation.valueOf("LEFT"))) {
xll = "XLLCENTER";
} else {
throw new ImageMetadataException(Warnings.message(this, Errors.Keys.IllegalParameterValue_2, "pointInPixel", ptInPixel));
}
}
header.put(xll, String.valueOf(gridToCRS.getTranslateX()));
header.put(yll, String.valueOf(gridToCRS.getTranslateY()));
/*
* Use the CELLSIZE attribute if the pixels are square, or the DX, DY attibutes
* if they are rectangular and we are allowed to use those non-standard attributes.
*/
try {
header.put("CELLSIZE", String.valueOf(helper.getCellSize(gridToCRS)));
} catch (IIOException e) {
final Dimension2D size;
if (strictCellSize || (size = helper.getCellDimension(gridToCRS)) == null) {
throw e;
}
Warnings.log(this, null, AsciiGridWriter.class, "writeHeader", e);
header.put("DX", String.valueOf(size.getWidth()));
header.put("DY", String.valueOf(size.getHeight()));
}
/*
* Get the fill sample value, which is optional. The default defined by
* the ASCII grid format is -9999.
*/
String fillValue = DEFAULT_FILL;
final List<SampleDimension> dimensions = metadata.getListForType(SampleDimension.class);
if (!isNullOrEmpty(dimensions)) {
final SampleDimension dim = dimensions.get(0);
if (dim != null) {
final double[] fillValues = dim.getFillSampleValues();
if (fillValues != null && fillValues.length != 0) {
final double value = fillValues[0];
if (!Double.isNaN(value)) {
fillValue = CharSequences.trimFractionalPart(String.valueOf(value)).toString();
header.put("NODATA_VALUE", fillValue);
}
}
}
}
return fillValue;
}
Aggregations