001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.commons.imaging.formats.tiff.photometricinterpreters.floatingpoint; 018 019import java.awt.Color; 020import java.io.IOException; 021import java.util.ArrayList; 022import java.util.Comparator; 023import java.util.List; 024 025import org.apache.commons.imaging.ImagingException; 026import org.apache.commons.imaging.common.ImageBuilder; 027import org.apache.commons.imaging.formats.tiff.photometricinterpreters.AbstractPhotometricInterpreter; 028 029/** 030 * Implements a custom photometric interpreter that can be supplied by applications in order to render Java images from real-valued TIFF data products. Most 031 * 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. 032 * 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 033 * 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 034 * supporting geospatial reference metadata for Geographic Information Systems). Because of the limits of the stock photometric interpreters, most 035 * floating-point TIFF files to not produce useful images. 036 * <p> 037 * This class allows an Apache Commons implementation to construct and specify a custom photometric interpreter when reading from a TIFF file. Applications may 038 * supply their own palette that maps real-valued data to specified colors. 039 * <p> 040 * This class provides two constructors: 041 * <ol> 042 * <li>A simple constructor to support gray scales</li> 043 * <li>A constructor to support a color palette (with potential interpolation)</li> 044 * </ol> 045 * <p> 046 * 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. 047 */ 048public final class PhotometricInterpreterFloat extends AbstractPhotometricInterpreter { 049 050 ArrayList<PaletteEntry> rangePaletteEntries = new ArrayList<>(); 051 ArrayList<PaletteEntry> singleValuePaletteEntries = new ArrayList<>(); 052 053 float minFound = Float.POSITIVE_INFINITY; 054 float maxFound = Float.NEGATIVE_INFINITY; 055 int xMin; 056 int yMin; 057 int xMax; 058 int yMax; 059 060 double sumFound; 061 int nFound; 062 063 /** 064 * Constructs a photometric interpreter that will produce a gray scale linearly distributed across the RGB color space for values in the range valueBlack to 065 * 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 066 * result in proper numerical computations. 067 * 068 * @param valueBlack the value associated with the dark side of the gray scale 069 * @param valueWhite the value associated with the light side of the gray scale 070 */ 071 public PhotometricInterpreterFloat(final float valueBlack, final float valueWhite) { 072 // The abstract base class requires that the following fields 073 // be set in the constructor: 074 // samplesPerPixel (int) 075 // bits per sample (array of type int[samplesPerPixel]) 076 // predictor (int, not used by this class) 077 // width (int) 078 // height (int) 079 super(1, new int[] { 32 }, // bits per sample 080 0, // not used by this class 081 32, // pro forma width value 082 32 // pro format height value 083 ); 084 085 if (valueWhite > valueBlack) { 086 final PaletteEntryForRange entry = new PaletteEntryForRange(valueBlack, valueWhite, Color.black, Color.white); 087 rangePaletteEntries.add(entry); 088 } else { 089 final PaletteEntryForRange entry = new PaletteEntryForRange(valueWhite, valueBlack, Color.white, Color.black); 090 rangePaletteEntries.add(entry); 091 } 092 } 093 094 /** 095 * Constructs a photometric interpreter that will use the specified palette to assign colors to floating-point values. 096 * <p> 097 * Although there is no prohibition against using palette entries with overlapping ranges, the behavior of such specifications is undefined and subject to 098 * 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 099 * specifications for ranges. 100 * 101 * @param paletteEntries a valid, non-empty list of palette entries 102 */ 103 public PhotometricInterpreterFloat(final List<PaletteEntry> paletteEntries) { 104 // The abstract base class requires that the following fields 105 // be set in the constructor: 106 // samplesPerPixel (int) 107 // bits per sample (array of type int[samplesPerPixel]) 108 // predictor (int, not used by this class) 109 // width (int) 110 // height (int) 111 super(1, new int[] { 32 }, // bits per sample 112 0, // not used by this class 113 32, // pro forma width value 114 32 // pro format height value 115 ); 116 117 if (paletteEntries == null || paletteEntries.isEmpty()) { 118 throw new IllegalArgumentException("Palette entries list must be non-null and non-empty"); 119 } 120 121 for (final PaletteEntry entry : paletteEntries) { 122 if (entry.coversSingleEntry()) { 123 singleValuePaletteEntries.add(entry); 124 } else { 125 rangePaletteEntries.add(entry); 126 } 127 } 128 129 final Comparator<PaletteEntry> comparator = (o1, o2) -> { 130 if (o1.getLowerBound() == o2.getLowerBound()) { 131 return Double.compare(o1.getUpperBound(), o2.getUpperBound()); 132 } 133 return Double.compare(o1.getLowerBound(), o2.getLowerBound()); 134 }; 135 136 rangePaletteEntries.sort(comparator); 137 singleValuePaletteEntries.sort(comparator); 138 } 139 140 /** 141 * Gets the maximum value found while rendering the image 142 * 143 * @return if data was processed, a valid value; otherwise, Negative Infinity 144 */ 145 public float getMaxFound() { 146 return maxFound; 147 } 148 149 /** 150 * Gets the coordinates (x,y) at which the maximum value was identified during processing 151 * 152 * @return a valid array of length 2. 153 */ 154 public int[] getMaxXY() { 155 return new int[] { xMax, yMax }; 156 } 157 158 /** 159 * Gets the mean of the values found while processing 160 * 161 * @return if data was processed, a valid mean value; otherwise, a zero. 162 */ 163 public float getMeanFound() { 164 if (nFound == 0) { 165 return 0; 166 } 167 return (float) (sumFound / nFound); 168 } 169 170 /** 171 * Gets the minimum value found while rendering the image 172 * 173 * @return if data was processed, a valid value; otherwise, Positive Infinity 174 */ 175 public float getMinFound() { 176 return minFound; 177 } 178 179 /** 180 * Gets the coordinates (x,y) at which the minimum value was identified during processing 181 * 182 * @return a valid array of length 2. 183 */ 184 public int[] getMinXY() { 185 return new int[] { xMin, yMin }; 186 } 187 188 @Override 189 public void interpretPixel(final ImageBuilder imageBuilder, final int[] samples, final int x, final int y) throws ImagingException, IOException { 190 191 final float f = Float.intBitsToFloat(samples[0]); 192 // in the event of NaN, do not store entry in the image builder. 193 194 // only the single bound palette entries support NaN 195 for (final PaletteEntry entry : singleValuePaletteEntries) { 196 if (entry.isCovered(f)) { 197 final int p = entry.getArgb(f); 198 imageBuilder.setRgb(x, y, p); 199 return; 200 } 201 } 202 203 if (Float.isNaN(f)) { 204 // if logic reaches here, there is no definition 205 // for a NaN. 206 return; 207 } 208 if (f < minFound) { 209 minFound = f; 210 xMin = x; 211 yMin = y; 212 } 213 if (f > maxFound) { 214 maxFound = f; 215 xMax = x; 216 yMax = y; 217 } 218 nFound++; 219 sumFound += f; 220 221 for (final PaletteEntry entry : singleValuePaletteEntries) { 222 if (entry.isCovered(f)) { 223 final int p = entry.getArgb(f); 224 imageBuilder.setRgb(x, y, p); 225 return; 226 } 227 } 228 229 for (final PaletteEntry entry : rangePaletteEntries) { 230 if (entry.isCovered(f)) { 231 final int p = entry.getArgb(f); 232 imageBuilder.setRgb(x, y, p); 233 break; 234 } 235 } 236 } 237 238 /** 239 * 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 240 * provided as a convenience to applications that are processing data outside the standard TIFF image-reading modules. 241 * 242 * @param f the floating point value to be mapped to an ARGB value 243 * @return a valid ARGB value, or zero if no palette specification covers the input value. 244 */ 245 public int mapValueToArgb(final float f) { 246 247 // The single-value palette entries can accept a Float.NaN as 248 // a target while the range-of-values entries cannot. So 249 // check the single-values before testing for Float.isNaN() 250 // because NaN may have special treatment. 251 for (final PaletteEntry entry : singleValuePaletteEntries) { 252 if (entry.isCovered(f)) { 253 return entry.getArgb(f); 254 } 255 } 256 257 if (Float.isNaN(f)) { 258 // if logic reaches here, there is no definition 259 // for a NaN. 260 return 0; 261 } 262 263 for (final PaletteEntry entry : rangePaletteEntries) { 264 if (entry.isCovered(f)) { 265 return entry.getArgb(f); 266 } 267 } 268 return 0; 269 } 270 271}