PhotometricInterpreterFloat.java
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.imaging.formats.tiff.photometricinterpreters.floatingpoint;
import java.awt.Color;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import org.apache.commons.imaging.ImagingException;
import org.apache.commons.imaging.common.ImageBuilder;
import org.apache.commons.imaging.formats.tiff.photometricinterpreters.PhotometricInterpreter;
/**
* Implements a custom photometric interpreter that can be supplied by applications in order to render Java images from real-valued TIFF data products. Most
* TIFF files include a specification for a "photometric interpreter" that implements logic for transforming the raw data in a TIFF file to a rendered image.
* But the TIFF standard does not include a specification for a photometric interpreter that can be used for rendering floating-point data. TIFF files are
* sometimes used to specify non-image data as a floating-point raster. This approach is particularly common in GeoTIFF files (TIFF files that contain tags for
* supporting geospatial reference metadata for Geographic Information Systems). Because of the limits of the stock photometric interpreters, most
* floating-point TIFF files to not produce useful images.
* <p>
* This class allows an Apache Commons implementation to construct and specify a custom photometric interpreter when reading from a TIFF file. Applications may
* supply their own palette that maps real-valued data to specified colors.
* <p>
* This class provides two constructors:
* <ol>
* <li>A simple constructor to support gray scales</li>
* <li>A constructor to support a color palette (with potential interpolation)</li>
* </ol>
* <p>
* To use this class, an application must access the TIFF file using the low-level, TIFF-specific API provided by the Apache Commons Imaging library.
*/
public class PhotometricInterpreterFloat extends PhotometricInterpreter {
ArrayList<PaletteEntry> rangePaletteEntries = new ArrayList<>();
ArrayList<PaletteEntry> singleValuePaletteEntries = new ArrayList<>();
float minFound = Float.POSITIVE_INFINITY;
float maxFound = Float.NEGATIVE_INFINITY;
int xMin;
int yMin;
int xMax;
int yMax;
double sumFound;
int nFound;
/**
* Constructs a photometric interpreter that will produce a gray scale linearly distributed across the RGB color space for values in the range valueBlack to
* valueWhite. Note that the two values may be given in either ascending order or descending order, but they must not be equal. Infinite values will not
* result in proper numerical computations.
*
* @param valueBlack the value associated with the dark side of the gray scale
* @param valueWhite the value associated with the light side of the gray scale
*/
public PhotometricInterpreterFloat(final float valueBlack, final float valueWhite) {
// The abstract base class requires that the following fields
// be set in the constructor:
// samplesPerPixel (int)
// bits per sample (array of type int[samplesPerPixel])
// predictor (int, not used by this class)
// width (int)
// height (int)
super(1, new int[] { 32 }, // bits per sample
0, // not used by this class
32, // pro forma width value
32 // pro format height value
);
if (valueWhite > valueBlack) {
final PaletteEntryForRange entry = new PaletteEntryForRange(valueBlack, valueWhite, Color.black, Color.white);
rangePaletteEntries.add(entry);
} else {
final PaletteEntryForRange entry = new PaletteEntryForRange(valueWhite, valueBlack, Color.white, Color.black);
rangePaletteEntries.add(entry);
}
}
/**
* Constructs a photometric interpreter that will use the specified palette to assign colors to floating-point values.
* <p>
* Although there is no prohibition against using palette entries with overlapping ranges, the behavior of such specifications is undefined and subject to
* change in the future. Therefore, it is not recommended. The exception in the use of single-value palette entries which may be used to override the
* specifications for ranges.
*
* @param paletteEntries a valid, non-empty list of palette entries
*/
public PhotometricInterpreterFloat(final List<PaletteEntry> paletteEntries) {
// The abstract base class requires that the following fields
// be set in the constructor:
// samplesPerPixel (int)
// bits per sample (array of type int[samplesPerPixel])
// predictor (int, not used by this class)
// width (int)
// height (int)
super(1, new int[] { 32 }, // bits per sample
0, // not used by this class
32, // pro forma width value
32 // pro format height value
);
if (paletteEntries == null || paletteEntries.isEmpty()) {
throw new IllegalArgumentException("Palette entries list must be non-null and non-empty");
}
for (final PaletteEntry entry : paletteEntries) {
if (entry.coversSingleEntry()) {
singleValuePaletteEntries.add(entry);
} else {
rangePaletteEntries.add(entry);
}
}
final Comparator<PaletteEntry> comparator = (o1, o2) -> {
if (o1.getLowerBound() == o2.getLowerBound()) {
return Double.compare(o1.getUpperBound(), o2.getUpperBound());
}
return Double.compare(o1.getLowerBound(), o2.getLowerBound());
};
rangePaletteEntries.sort(comparator);
singleValuePaletteEntries.sort(comparator);
}
/**
* Gets the maximum value found while rendering the image
*
* @return if data was processed, a valid value; otherwise, Negative Infinity
*/
public float getMaxFound() {
return maxFound;
}
/**
* Gets the coordinates (x,y) at which the maximum value was identified during processing
*
* @return a valid array of length 2.
*/
public int[] getMaxXY() {
return new int[] { xMax, yMax };
}
/**
* Gets the mean of the values found while processing
*
* @return if data was processed, a valid mean value; otherwise, a zero.
*/
public float getMeanFound() {
if (nFound == 0) {
return 0;
}
return (float) (sumFound / nFound);
}
/**
* Gets the minimum value found while rendering the image
*
* @return if data was processed, a valid value; otherwise, Positive Infinity
*/
public float getMinFound() {
return minFound;
}
/**
* Gets the coordinates (x,y) at which the minimum value was identified during processing
*
* @return a valid array of length 2.
*/
public int[] getMinXY() {
return new int[] { xMin, yMin };
}
@Override
public void interpretPixel(final ImageBuilder imageBuilder, final int[] samples, final int x, final int y) throws ImagingException, IOException {
final float f = Float.intBitsToFloat(samples[0]);
// in the event of NaN, do not store entry in the image builder.
// only the single bound palette entries support NaN
for (final PaletteEntry entry : singleValuePaletteEntries) {
if (entry.isCovered(f)) {
final int p = entry.getArgb(f);
imageBuilder.setRgb(x, y, p);
return;
}
}
if (Float.isNaN(f)) {
// if logic reaches here, there is no definition
// for a NaN.
return;
}
if (f < minFound) {
minFound = f;
xMin = x;
yMin = y;
}
if (f > maxFound) {
maxFound = f;
xMax = x;
yMax = y;
}
nFound++;
sumFound += f;
for (final PaletteEntry entry : singleValuePaletteEntries) {
if (entry.isCovered(f)) {
final int p = entry.getArgb(f);
imageBuilder.setRgb(x, y, p);
return;
}
}
for (final PaletteEntry entry : rangePaletteEntries) {
if (entry.isCovered(f)) {
final int p = entry.getArgb(f);
imageBuilder.setRgb(x, y, p);
break;
}
}
}
/**
* Provides a method for mapping a pixel value to an integer (ARGB) value. This method is not defined for the standard photometric interpreters and is
* provided as a convenience to applications that are processing data outside the standard TIFF image-reading modules.
*
* @param f the floating point value to be mapped to an ARGB value
* @return a valid ARGB value, or zero if no palette specification covers the input value.
*/
public int mapValueToArgb(final float f) {
// The single-value palette entries can accept a Float.NaN as
// a target while the range-of-values entries cannot. So
// check the single-values before testing for Float.isNaN()
// because NaN may have special treatment.
for (final PaletteEntry entry : singleValuePaletteEntries) {
if (entry.isCovered(f)) {
return entry.getArgb(f);
}
}
if (Float.isNaN(f)) {
// if logic reaches here, there is no definition
// for a NaN.
return 0;
}
for (final PaletteEntry entry : rangePaletteEntries) {
if (entry.isCovered(f)) {
return entry.getArgb(f);
}
}
return 0;
}
}