View Javadoc
1   package nom.tam.util;
2   
3   /*-
4    * #%L
5    * nom.tam.fits
6    * %%
7    * Copyright (C) 1996 - 2024 nom-tam-fits
8    * %%
9    * This is free and unencumbered software released into the public domain.
10   * 
11   * Anyone is free to copy, modify, publish, use, compile, sell, or
12   * distribute this software, either in source code form or as a compiled
13   * binary, for any purpose, commercial or non-commercial, and by any
14   * means.
15   * 
16   * In jurisdictions that recognize copyright laws, the author or authors
17   * of this software dedicate any and all copyright interest in the
18   * software to the public domain. We make this dedication for the benefit
19   * of the public at large and to the detriment of our heirs and
20   * successors. We intend this dedication to be an overt act of
21   * relinquishment in perpetuity of all present and future rights to this
22   * software under copyright law.
23   * 
24   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
25   * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
26   * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
27   * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
28   * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
29   * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
30   * OTHER DEALINGS IN THE SOFTWARE.
31   * #L%
32   */
33  
34  import nom.tam.fits.Header;
35  import nom.tam.fits.HeaderCard;
36  import nom.tam.fits.header.Standard;
37  
38  /**
39   * <p>
40   * Quantizes floating point values as integers. FITS allows representing
41   * floating-point values as integers, e.g. to allow more compact storage at some
42   * tolerable level of precision loss. For example, you may store floating-point
43   * values (4 bytes) discretized into 64k levels as 16-bit integers. The
44   * conversion involves a linear transformation:
45   * </p>
46   * 
47   * <pre>
48   *   {float-value}= {scaling} * {int-value} + {offset}
49   * </pre>
50   * <p>
51   * and the inverse transformation:
52   * </p>
53   * 
54   * <pre>
55   *   {int-value} = round(({float-value} - {offset}) / {scaling})
56   * </pre>
57   * <p>
58   * The latter floating-point to integer conversion naturally results in some
59   * loss of precision, comparable to the level of the scaling factor, i.e. the
60   * peration of discrete levels at which information is preserved.
61   * </p>
62   * <p>
63   * In addition to the scaling conversion, FITS also allows designating an
64   * integer blanking value to indicate missing or invalid data, which is mapped
65   * to NaN in the floating point representation.
66   * </p>
67   * <p>
68   * Fits allows for quantized representations of floating-point data both in
69   * image HDUs and for columns in binary table HDUs. The quantization parameters
70   * are stored differently for the two types of HDUs, using the BSCALE, BZERO,
71   * and BLANK keywords for images, and the TSCALn, TZEROn, and TNULLn keywords
72   * for individual columns in a table.
73   * </p>
74   * 
75   * @author Attila Kovacs
76   * @since 1.20
77   */
78  public class Quantizer {
79  
80      private Long blankingValue;
81  
82      private double scale = 1.0;
83  
84      private double offset;
85  
86      /**
87       * Constructs a new decimal/integer conversion rule.
88       * 
89       * @param scale
90       *            The scaling value, that is the spacing of the qunatized levels
91       * @param offset
92       *            The floating-point value that corresponds to an integer value
93       *            of 0 (zero).
94       * @param blankingValue
95       *            The value to use to represent NaN values in the integer
96       *            representation, that is missing or invalid data.
97       */
98      public Quantizer(double scale, double offset, int blankingValue) {
99          this(scale, offset, (long) blankingValue);
100     }
101 
102     /**
103      * Constructs a new decimal/integer conversion rule.
104      * 
105      * @param scale
106      *            The scaling value, that is the spacing of the qunatized levels
107      * @param offset
108      *            The floating-point value that corresponds to an integer value
109      *            of 0 (zero).
110      * @param blankingValue
111      *            The value to use to represent NaN values in the integer
112      *            representation, that is missing or invalid data. It may be
113      *            <code>null</code> if the floating-point data is not expected
114      *            to contain NaN values ever.
115      */
116     public Quantizer(double scale, double offset, Long blankingValue) {
117         this.scale = scale;
118         this.offset = offset;
119         this.blankingValue = blankingValue;
120     }
121 
122     /**
123      * Converts a floating point value to its integer representation using the
124      * quantization.
125      * 
126      * @param value
127      *            the floating point value
128      * @return the corresponding qunatized integer value
129      * @see #toDouble(long)
130      */
131     public long toLong(double value) {
132         if (!Double.isFinite(value)) {
133             if (blankingValue == null) {
134                 throw new IllegalStateException("No blanking value was defined.");
135             }
136             return blankingValue;
137         }
138         return Math.round((value - offset) / scale);
139     }
140 
141     /**
142      * Converts an integer value to the floating-point value it represents under
143      * the qunatization.
144      * 
145      * @param value
146      *            the integer value
147      * @return the corresponding floating-point value, which may be NaN.
148      * @see #toLong(double)
149      */
150     public double toDouble(long value) {
151         if (blankingValue != null && value == blankingValue) {
152             return Double.NaN;
153         }
154         return scale * value + offset;
155     }
156 
157     /**
158      * Checks if the quantization is the same as the default quantization. For
159      * example, maybe we don't need to (want to) write the quantization keywords
160      * into the FITS headers if these are irrelevant and/or not meaningful. So
161      * this method might help us decide when quantization is necessary /
162      * meaningful vs when it is irrelevant.
163      * 
164      * @return <code>true</code> if the scaling is 1.0, the offset 0.0, and the
165      *         blanking value is <code>null</code>. Otherwise <code>false</code>
166      *         .
167      */
168     public boolean isDefault() {
169         return scale == 1.0 && offset == 0.0 && blankingValue == null;
170     }
171 
172     /**
173      * Adds the quantization parameters to an image header,
174      * 
175      * @param h
176      *            the image header.
177      * @see #fromImageHeader(Header)
178      * @see #editTableHeader(Header, int)
179      */
180     public void editImageHeader(Header h) {
181         h.addValue(Standard.BSCALE, scale);
182         h.addValue(Standard.BZERO, offset);
183 
184         if (blankingValue != null) {
185             h.addValue(Standard.BLANK, blankingValue);
186         } else {
187             h.deleteKey(Standard.BLANK);
188         }
189     }
190 
191     /**
192      * Adds the quantization parameters to a binaty table header,
193      * 
194      * @param h
195      *            the binary table header.
196      * @param col
197      *            the zero-based Java column index
198      * @see #fromTableHeader(Header, int)
199      * @see #editImageHeader(Header)
200      */
201     public void editTableHeader(Header h, int col) {
202         Cursor<String, HeaderCard> c = h.iterator();
203         c.setKey(Standard.TFORMn.n(col + 1).key());
204 
205         c.add(HeaderCard.create(Standard.TSCALn.n(col + 1), scale));
206         c.add(HeaderCard.create(Standard.TZEROn.n(col + 1), offset));
207 
208         if (blankingValue != null) {
209             c.add(HeaderCard.create(Standard.TNULLn.n(col + 1), blankingValue));
210         } else {
211             h.deleteKey(Standard.TNULLn.n(col + 1));
212         }
213     }
214 
215     /**
216      * Returns the quantizer that is described by an image header.
217      * 
218      * @param h
219      *            an image header
220      * @return the quantizer that id described by the header. It may be the
221      *         default quantizer if the header does not contain any of the
222      *         quantization keywords.
223      * @see #editImageHeader(Header)
224      * @see #fromTableHeader(Header, int)
225      * @see #isDefault()
226      */
227     public static Quantizer fromImageHeader(Header h) {
228         return new Quantizer(h.getDoubleValue(Standard.BSCALE, 1.0), h.getDoubleValue(Standard.BZERO, 0.0), //
229                 h.containsKey(Standard.BLANK) ? h.getLongValue(Standard.BLANK) : null);
230     }
231 
232     /**
233      * Returns the quantizer that is described by a binary table header.
234      * 
235      * @param h
236      *            a binary table header
237      * @param col
238      *            the zero-based Java column index
239      * @return the quantizer that id described by the header. It may be the
240      *         default quantizer if the header does not contain any of the
241      *         quantization keywords.
242      * @see #editTableHeader(Header, int)
243      * @see #fromImageHeader(Header)
244      * @see #isDefault()
245      */
246     public static Quantizer fromTableHeader(Header h, int col) {
247         return new Quantizer(h.getDoubleValue(Standard.TSCALn.n(col + 1), 1.0), //
248                 h.getDoubleValue(Standard.TZEROn.n(col + 1), 0.0), //
249                 h.containsKey(Standard.TNULLn.n(col + 1)) ? h.getLongValue(Standard.TNULLn.n(col + 1)) : null);
250     }
251 }