View Javadoc
1   package nom.tam.fits.compression.algorithm.quant;
2   
3   /*
4    * #%L
5    * nom.tam FITS library
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 java.nio.ByteBuffer;
35  import java.nio.DoubleBuffer;
36  import java.nio.FloatBuffer;
37  import java.nio.IntBuffer;
38  
39  import nom.tam.fits.compression.algorithm.api.ICompressor;
40  
41  /**
42   * (<i>for internal use</i>) Qunatization step processor as part of compression.
43   */
44  @SuppressWarnings({"javadoc", "deprecation"})
45  public class QuantizeProcessor {
46  
47      public static class DoubleQuantCompressor extends QuantizeProcessor implements ICompressor<DoubleBuffer> {
48  
49          private final ICompressor<IntBuffer> postCompressor;
50  
51          public DoubleQuantCompressor(QuantizeOption quantizeOption, ICompressor<IntBuffer> compressor) {
52              super(quantizeOption);
53              postCompressor = compressor;
54          }
55  
56          @Override
57          public boolean compress(DoubleBuffer buffer, ByteBuffer compressed) {
58              IntBuffer intData = IntBuffer.wrap(new int[quantizeOption.getTileHeight() * quantizeOption.getTileWidth()]);
59              double[] doubles = new double[quantizeOption.getTileHeight() * quantizeOption.getTileWidth()];
60              buffer.get(doubles);
61              if (!this.quantize(doubles, intData)) {
62                  return false;
63              }
64              intData.rewind();
65              postCompressor.compress(intData, compressed);
66              return true;
67          }
68  
69          @Override
70          public void decompress(ByteBuffer compressed, DoubleBuffer buffer) {
71              IntBuffer intData = IntBuffer.wrap(new int[quantizeOption.getTileHeight() * quantizeOption.getTileWidth()]);
72              postCompressor.decompress(compressed, intData);
73              intData.rewind();
74              unquantize(intData, buffer);
75          }
76      }
77  
78      /**
79       * TODO this is done very inefficient and should be refactored!
80       */
81      public static class FloatQuantCompressor extends QuantizeProcessor implements ICompressor<FloatBuffer> {
82  
83          private final ICompressor<IntBuffer> postCompressor;
84  
85          public FloatQuantCompressor(QuantizeOption quantizeOption, ICompressor<IntBuffer> postCompressor) {
86              super(quantizeOption);
87              this.postCompressor = postCompressor;
88          }
89  
90          @Override
91          public boolean compress(FloatBuffer buffer, ByteBuffer compressed) {
92              float[] floats = new float[quantizeOption.getTileHeight() * quantizeOption.getTileWidth()];
93              double[] doubles = new double[quantizeOption.getTileHeight() * quantizeOption.getTileWidth()];
94              buffer.get(floats);
95              for (int index = 0; index < doubles.length; index++) {
96                  doubles[index] = floats[index];
97              }
98              IntBuffer intData = IntBuffer.wrap(new int[quantizeOption.getTileHeight() * quantizeOption.getTileWidth()]);
99              if (!this.quantize(doubles, intData)) {
100                 return false;
101             }
102             intData.rewind();
103             postCompressor.compress(intData, compressed);
104             return true;
105         }
106 
107         @Override
108         public void decompress(ByteBuffer compressed, FloatBuffer buffer) {
109             IntBuffer intData = IntBuffer.wrap(new int[quantizeOption.getTileHeight() * quantizeOption.getTileWidth()]);
110             postCompressor.decompress(compressed, intData);
111             intData.rewind();
112             double[] doubles = new double[quantizeOption.getTileHeight() * quantizeOption.getTileWidth()];
113             DoubleBuffer doubleBuffer = DoubleBuffer.wrap(doubles);
114             unquantize(intData, doubleBuffer);
115             for (double d : doubles) {
116                 buffer.put((float) d);
117             }
118         }
119     }
120 
121     private class BaseFilter extends PixelFilter {
122 
123         BaseFilter() {
124             super(null);
125         }
126 
127         @Override
128         protected void nextPixel() {
129         }
130 
131         @Override
132         protected double toDouble(int pixel) {
133             return (pixel + ROUNDING_HALF) * bScale + bZero;
134         }
135 
136         @Override
137         protected int toInt(double pixel) {
138             return nint((pixel - bZero) / bScale + ROUNDING_HALF);
139         }
140     }
141 
142     private class DitherFilter extends PixelFilter {
143 
144         private static final int RANDOM_MULTIPLICATOR = 500;
145 
146         private int iseed = 0;
147 
148         private int nextRandom = 0;
149 
150         DitherFilter(long seed) {
151             super(null);
152             initialize(seed);
153         }
154 
155         public void initialize(long ditherSeed) {
156             iseed = (int) ((ditherSeed - 1) % RandomSequence.length());
157             initI1();
158         }
159 
160         private void initI1() {
161             nextRandom = (int) (RandomSequence.get(iseed) * RANDOM_MULTIPLICATOR);
162         }
163 
164         public double nextRandom() {
165             return RandomSequence.get(nextRandom);
166         }
167 
168         @Override
169         protected void nextPixel() {
170             nextRandom++;
171             if (nextRandom >= RandomSequence.length()) {
172                 iseed++;
173                 if (iseed >= RandomSequence.length()) {
174                     iseed = 0;
175                 }
176                 initI1();
177             }
178         }
179 
180         @Override
181         protected double toDouble(int pixel) {
182             return (pixel - nextRandom() + ROUNDING_HALF) * bScale + bZero;
183         }
184 
185         @Override
186         protected int toInt(double pixel) {
187             return nint((pixel - bZero) / bScale + nextRandom() - ROUNDING_HALF);
188         }
189     }
190 
191     private class NullFilter extends PixelFilter {
192 
193         private final double nullValue;
194 
195         private final boolean isNaN;
196 
197         private final int nullValueIndicator;
198 
199         NullFilter(double nullValue, int nullValueIndicator, PixelFilter next) {
200             super(next);
201             this.nullValue = nullValue;
202             isNaN = Double.isNaN(this.nullValue);
203             this.nullValueIndicator = nullValueIndicator;
204         }
205 
206         public final boolean isNull(double pixel) {
207             return isNaN ? Double.isNaN(pixel) : nullValue == pixel;
208         }
209 
210         @Override
211         protected double toDouble(int pixel) {
212             if (pixel == nullValueIndicator) {
213                 return nullValue;
214             }
215             return super.toDouble(pixel);
216         }
217 
218         @Override
219         protected int toInt(double pixel) {
220             if (isNull(pixel)) {
221                 return nullValueIndicator;
222             }
223             return super.toInt(pixel);
224         }
225     }
226 
227     private class PixelFilter {
228 
229         private final PixelFilter next;
230 
231         protected PixelFilter(PixelFilter next) {
232             this.next = next;
233         }
234 
235         protected void nextPixel() {
236             next.nextPixel();
237         }
238 
239         protected double toDouble(int pixel) {
240             return next.toDouble(pixel);
241         }
242 
243         protected int toInt(double pixel) {
244             return next.toInt(pixel);
245         }
246     }
247 
248     private class ZeroFilter extends PixelFilter {
249 
250         ZeroFilter(PixelFilter next) {
251             super(next);
252         }
253 
254         @Override
255         protected double toDouble(int pixel) {
256             if (pixel == ZERO_VALUE) {
257                 return 0.0;
258             }
259             return super.toDouble(pixel);
260         }
261 
262         @Override
263         protected int toInt(double pixel) {
264             if (pixel == 0.0) {
265                 return ZERO_VALUE;
266             }
267             return super.toInt(pixel);
268         }
269     }
270 
271     private static final double MAX_INT_AS_DOUBLE = Integer.MAX_VALUE;
272 
273     /**
274      * number of reserved values, starting with
275      */
276     private static final long N_RESERVED_VALUES = 10;
277 
278     private static final double ROUNDING_HALF = 0.5;
279 
280     /**
281      * value used to represent zero-valued pixels
282      */
283     private static final int ZERO_VALUE = Integer.MIN_VALUE + 2;
284 
285     private final boolean centerOnZero;
286 
287     private final PixelFilter pixelFilter;
288 
289     private double bScale;
290 
291     private double bZero;
292 
293     private Quantize quantize;
294 
295     protected final QuantizeOption quantizeOption;
296 
297     public QuantizeProcessor(QuantizeOption quantizeOption) {
298         this.quantizeOption = quantizeOption;
299         bScale = quantizeOption.getBScale();
300         bZero = quantizeOption.getBZero();
301         PixelFilter filter = null;
302         boolean localCenterOnZero = quantizeOption.isCenterOnZero();
303         if (quantizeOption.isDither2()) {
304             filter = new DitherFilter(quantizeOption.getSeed() + quantizeOption.getTileIndex());
305             localCenterOnZero = true;
306             quantizeOption.setCheckZero(true);
307         } else if (quantizeOption.isDither()) {
308             filter = new DitherFilter(quantizeOption.getSeed() + quantizeOption.getTileIndex());
309         } else {
310             filter = new BaseFilter();
311         }
312         if (quantizeOption.isCheckZero()) {
313             filter = new ZeroFilter(filter);
314         }
315         if (quantizeOption.isCheckNull()) {
316             final NullFilter nullFilter = new NullFilter(quantizeOption.getNullValue(), quantizeOption.getBNull(), filter);
317             filter = nullFilter;
318             quantize = new Quantize(quantizeOption) {
319 
320                 @Override
321                 protected int findNextValidPixelWithNullCheck(int nx, DoubleArrayPointer rowpix, int ii) {
322                     while (ii < nx && nullFilter.isNull(rowpix.get(ii))) {
323                         ii++;
324                     }
325                     return ii;
326                 }
327 
328                 @Override
329                 protected boolean isNull(double d) {
330                     return nullFilter.isNull(d);
331                 }
332             };
333         } else {
334             quantize = new Quantize(quantizeOption);
335         }
336         pixelFilter = filter;
337         centerOnZero = localCenterOnZero;
338     }
339 
340     public Quantize getQuantize() {
341         return quantize;
342     }
343 
344     public boolean quantize(double[] doubles, IntBuffer quants) {
345         boolean success = quantize.quantize(doubles, quantizeOption.getTileWidth(), quantizeOption.getTileHeight());
346         if (success) {
347             calculateBZeroAndBscale();
348             quantize(DoubleBuffer.wrap(doubles, 0, quantizeOption.getTileWidth() * quantizeOption.getTileHeight()), quants);
349         }
350         return success;
351     }
352 
353     public void quantize(final DoubleBuffer fdata, final IntBuffer intData) {
354         while (fdata.hasRemaining()) {
355             intData.put(pixelFilter.toInt(fdata.get()));
356             pixelFilter.nextPixel();
357         }
358     }
359 
360     public void unquantize(final IntBuffer intData, final DoubleBuffer fdata) {
361         while (fdata.hasRemaining()) {
362             fdata.put(pixelFilter.toDouble(intData.get()));
363             pixelFilter.nextPixel();
364         }
365     }
366 
367     private void calculateBZeroAndBscale() {
368         bScale = quantizeOption.getBScale();
369         bZero = zeroCenter();
370         quantizeOption.setIntMinValue(nint((quantizeOption.getMinValue() - bZero) / bScale));
371         quantizeOption.setIntMaxValue(nint((quantizeOption.getMaxValue() - bZero) / bScale));
372         quantizeOption.setBZero(bZero);
373     }
374 
375     private int nint(double x) {
376         return x >= 0. ? (int) (x + ROUNDING_HALF) : (int) (x - ROUNDING_HALF);
377     }
378 
379     private double zeroCenter() {
380         final double minValue = quantizeOption.getMinValue();
381         final double maxValue = quantizeOption.getMaxValue();
382         double evaluatedBZero;
383         if (!quantizeOption.isCheckNull() && !centerOnZero) {
384             // don't have to check for nulls
385             // return all positive values, if possible since some compression
386             // algorithms either only work for positive integers, or are more
387             // efficient.
388             if ((maxValue - minValue) / bScale < MAX_INT_AS_DOUBLE - N_RESERVED_VALUES) {
389                 evaluatedBZero = minValue;
390                 // fudge the zero point so it is an integer multiple of bScale
391                 // This helps to ensure the same scaling will be performed if
392                 // the file undergoes multiple fpack/funpack cycles
393                 long iqfactor = (long) (evaluatedBZero / bScale + ROUNDING_HALF);
394                 evaluatedBZero = iqfactor * bScale;
395             } else {
396                 /* center the quantized levels around zero */
397                 evaluatedBZero = (minValue + maxValue) / 2.;
398             }
399         } else {
400             // data contains null values or has be forced to center on zero
401             // shift the range to be close to the value used to represent null
402             // values
403             evaluatedBZero = minValue - bScale * (Integer.MIN_VALUE + N_RESERVED_VALUES + 1);
404         }
405         return evaluatedBZero;
406     }
407 }