View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.imaging.formats.tiff.photometricinterpreters.floatingpoint;
18  
19  import java.awt.Color;
20  import java.io.IOException;
21  import java.util.ArrayList;
22  import java.util.Comparator;
23  import java.util.List;
24  
25  import org.apache.commons.imaging.ImagingException;
26  import org.apache.commons.imaging.common.ImageBuilder;
27  import org.apache.commons.imaging.formats.tiff.photometricinterpreters.PhotometricInterpreter;
28  
29  /**
30   * Implements a custom photometric interpreter that can be supplied by applications in order to render Java images from real-valued TIFF data products. Most
31   * 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.
32   * 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
33   * 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
34   * supporting geospatial reference metadata for Geographic Information Systems). Because of the limits of the stock photometric interpreters, most
35   * floating-point TIFF files to not produce useful images.
36   * <p>
37   * This class allows an Apache Commons implementation to construct and specify a custom photometric interpreter when reading from a TIFF file. Applications may
38   * supply their own palette that maps real-valued data to specified colors.
39   * <p>
40   * This class provides two constructors:
41   * <ol>
42   * <li>A simple constructor to support gray scales</li>
43   * <li>A constructor to support a color palette (with potential interpolation)</li>
44   * </ol>
45   * <p>
46   * 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.
47   */
48  public class PhotometricInterpreterFloat extends PhotometricInterpreter {
49  
50      ArrayList<PaletteEntry> rangePaletteEntries = new ArrayList<>();
51      ArrayList<PaletteEntry> singleValuePaletteEntries = new ArrayList<>();
52  
53      float minFound = Float.POSITIVE_INFINITY;
54      float maxFound = Float.NEGATIVE_INFINITY;
55      int xMin;
56      int yMin;
57      int xMax;
58      int yMax;
59  
60      double sumFound;
61      int nFound;
62  
63      /**
64       * Constructs a photometric interpreter that will produce a gray scale linearly distributed across the RGB color space for values in the range valueBlack to
65       * 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
66       * result in proper numerical computations.
67       *
68       * @param valueBlack the value associated with the dark side of the gray scale
69       * @param valueWhite the value associated with the light side of the gray scale
70       */
71      public PhotometricInterpreterFloat(final float valueBlack, final float valueWhite) {
72          // The abstract base class requires that the following fields
73          // be set in the constructor:
74          // samplesPerPixel (int)
75          // bits per sample (array of type int[samplesPerPixel])
76          // predictor (int, not used by this class)
77          // width (int)
78          // height (int)
79          super(1, new int[] { 32 }, // bits per sample
80                  0, // not used by this class
81                  32, // pro forma width value
82                  32 // pro format height value
83          );
84  
85          if (valueWhite > valueBlack) {
86              final PaletteEntryForRange entry = new PaletteEntryForRange(valueBlack, valueWhite, Color.black, Color.white);
87              rangePaletteEntries.add(entry);
88          } else {
89              final PaletteEntryForRange entry = new PaletteEntryForRange(valueWhite, valueBlack, Color.white, Color.black);
90              rangePaletteEntries.add(entry);
91          }
92      }
93  
94      /**
95       * Constructs a photometric interpreter that will use the specified palette to assign colors to floating-point values.
96       * <p>
97       * Although there is no prohibition against using palette entries with overlapping ranges, the behavior of such specifications is undefined and subject to
98       * 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
99       * 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 }