1 package nom.tam.fits.compression.algorithm.quant;
2
3 import nom.tam.fits.compression.algorithm.api.ICompressOption;
4 import nom.tam.fits.compression.provider.param.api.ICompressParameters;
5 import nom.tam.fits.compression.provider.param.base.BundledParameters;
6 import nom.tam.fits.compression.provider.param.quant.QuantizeParameters;
7
8 /*
9 * #%L
10 * nom.tam FITS library
11 * %%
12 * Copyright (C) 1996 - 2024 nom-tam-fits
13 * %%
14 * This is free and unencumbered software released into the public domain.
15 *
16 * Anyone is free to copy, modify, publish, use, compile, sell, or
17 * distribute this software, either in source code form or as a compiled
18 * binary, for any purpose, commercial or non-commercial, and by any
19 * means.
20 *
21 * In jurisdictions that recognize copyright laws, the author or authors
22 * of this software dedicate any and all copyright interest in the
23 * software to the public domain. We make this dedication for the benefit
24 * of the public at large and to the detriment of our heirs and
25 * successors. We intend this dedication to be an overt act of
26 * relinquishment in perpetuity of all present and future rights to this
27 * software under copyright law.
28 *
29 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
30 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
31 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
32 * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
33 * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
34 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
35 * OTHER DEALINGS IN THE SOFTWARE.
36 * #L%
37 */
38
39 /**
40 * Quantization options when they are part of the compression scheme. When compressing tables and images includes
41 * quantization (integer representation of floating point data), users can control how exactly the quantization should
42 * be performed. When reading compressed FITS files, these options will be set automatically based on the header values
43 * recorded in the compressed HDU.
44 *
45 * @see nom.tam.image.compression.hdu.CompressedImageHDU#setQuantAlgorithm(String)
46 * @see nom.tam.image.compression.hdu.CompressedImageHDU#getCompressOption(Class)
47 */
48 public class QuantizeOption implements ICompressOption {
49
50 /**
51 * and including NULL_VALUE. These values may not be used to represent the quantized and scaled floating point pixel
52 * values If lossy Hcompression is used, and the tiledImageOperation contains null values, then it is also possible
53 * for the compressed values to slightly exceed the range of the actual (lossless) values so we must reserve a
54 * little more space value used to represent undefined pixels
55 */
56 private static final int NULL_VALUE = Integer.MIN_VALUE + 1;
57
58 /** Shared configuration across copies */
59 private Config config;
60
61 /** The parameters that represent settings for this option in the FITS headers and/or compressed data columns */
62 protected QuantizeParameters parameters;
63
64 private ICompressOption compressOption;
65
66 private double bScale = Double.NaN;
67
68 private double bZero = Double.NaN;
69
70 private double nullValue = Double.NaN;
71
72 private Integer nullValueIndicator;
73
74 private boolean checkNull;
75
76 private int intMaxValue;
77
78 private int intMinValue;
79
80 private double maxValue;
81
82 private double minValue;
83
84 private int tileIndex = 0;
85
86 private int tileHeight;
87
88 private int tileWidth;
89
90 QuantizeOption() {
91 this(null);
92 }
93
94 /**
95 * Creates a new set of quantization options, to be used together with the specified compression options.
96 *
97 * @param compressOption Compression-specific options to pair with these quantization options, or <code>null</code>.
98 *
99 * @since 1.18
100 */
101 public QuantizeOption(ICompressOption compressOption) {
102 parameters = new QuantizeParameters(this);
103 config = new Config();
104 this.compressOption = compressOption;
105 }
106
107 @Override
108 public QuantizeOption copy() {
109 try {
110 QuantizeOption copy = (QuantizeOption) clone();
111 if (compressOption != null) {
112 copy.compressOption = compressOption.copy();
113 }
114 copy.parameters = parameters.copy(copy);
115 return copy;
116 } catch (CloneNotSupportedException e) {
117 throw new IllegalStateException("option could not be cloned", e);
118 }
119 }
120
121 /**
122 * Returns the integer value that represents missing (<code>null</code>) data in the quantized representation.
123 *
124 * @return the integer blanking value (<code>null</code> value).
125 *
126 * @see #setBNull(Integer)
127 */
128 public Integer getBNull() {
129 return nullValueIndicator;
130 }
131
132 /**
133 * Returns the quantization level.
134 *
135 * @return the floating-point difference between integer levels in the quantized data.
136 *
137 * @see #setBScale(double)
138 * @see #getBZero()
139 */
140 public double getBScale() {
141 return bScale;
142 }
143
144 /**
145 * Returns the quantization offset.
146 *
147 * @return the floating-point value corresponding to the integer level 0.
148 *
149 * @see #setBZero(double)
150 * @see #getBScale()
151 */
152 public double getBZero() {
153 return bZero;
154 }
155
156 @Override
157 public ICompressParameters getCompressionParameters() {
158 if (compressOption == null) {
159 return parameters;
160 }
161 return new BundledParameters(parameters, compressOption.getCompressionParameters());
162 }
163
164 /**
165 * Returns the compression or quantization options, recast for the selected option class.
166 *
167 * @param <T> the generic type of the compression option
168 * @param clazz the option class for the compression algorithm used with the quantization, or
169 * <code>QunatizeOption.class</code> for our own options.
170 *
171 * @return the recast options for the requested class or <code>null</code> id we do not have access to options
172 * of the requested class.
173 *
174 * @see #getCompressOption()
175 */
176 public <T> T getCompressOption(Class<T> clazz) {
177 return unwrap(clazz);
178 }
179
180 /**
181 * Returns the options for the compression algorithm that accompanies quantization.
182 *
183 * @return the options for the compression algorithm, or <code>null</code>
184 *
185 * @see #getCompressOption(Class)
186 */
187 public final ICompressOption getCompressOption() {
188 return compressOption;
189 }
190
191 /**
192 * Returns the maximum integer level in the quantized representation.
193 *
194 * @return the maximum integer level in the quantized data.
195 *
196 * @see #getMaxValue()
197 * @see #getIntMinValue()
198 */
199 public int getIntMaxValue() {
200 return intMaxValue;
201 }
202
203 /**
204 * Returns the maximum integer level in the quantized representation.
205 *
206 * @return the maximum integer level in the quantized data.
207 *
208 * @see #getMinValue()
209 * @see #getIntMinValue()
210 */
211 public int getIntMinValue() {
212 return intMinValue;
213 }
214
215 /**
216 * Returns the maximum floating-point value in the data
217 *
218 * @return the maximum floating-point value in the data before quantization.
219 *
220 * @see #getIntMaxValue()
221 * @see #getMinValue()
222 */
223 public double getMaxValue() {
224 return maxValue;
225 }
226
227 /**
228 * Returns the minimum floating-point value in the data
229 *
230 * @return the minimum floating-point value in the data before quantization.
231 *
232 * @see #getIntMinValue()
233 * @see #getMaxValue()
234 */
235 public double getMinValue() {
236 return minValue;
237 }
238
239 /**
240 * Returns the floating-point value that indicates a <code>null</code> datum in the image before quantization is
241 * applied. Normally, the FITS standard is that NaN values indicate <code>null</code> values in floating-point
242 * images. While this class allows using other values also, they are not recommended since they are not supported by
243 * FITS in a standard way.
244 *
245 * @return the floating-point value that represents a <code>null</code> value (missing data) in the image before
246 * quantization.
247 *
248 * @see #setNullValue(double)
249 * @see #getNullValueIndicator()
250 * @see #isCheckNull()
251 */
252 public double getNullValue() {
253 return nullValue;
254 }
255
256 /**
257 * @deprecated use {@link #getBNull()} instead (duplicate method). Returns the integer value that represents missing
258 * data (<code>null</code>) in the quantized representation.
259 *
260 * @return the integer blanking value (<code>null</code> value).
261 *
262 * @see #setBNull(Integer)
263 */
264 @Deprecated
265 public final Integer getNullValueIndicator() {
266 return getBNull();
267 }
268
269 /**
270 * Returns the quantization resolution level used for automatic qunatization. For Gaussian noise the quantization
271 * level is the standard deviation of the noise divided by this Q value. Thus Q values of a few will ensure that
272 * quantization retains just about all of the information in the noisy data.
273 *
274 * @return The current Q value, defined as the number of quantized levels per standard deviation (for Gaussian
275 * noise).
276 *
277 * @see #setQlevel(double)
278 * @see #getBScale()
279 */
280 public double getQLevel() {
281 return config.qlevel;
282 }
283
284 /**
285 * Gets the random seed value used for dithering
286 *
287 * @return the random seed value used for dithering
288 *
289 * @see #setSeed(long)
290 * @see RandomSequence
291 */
292 public long getSeed() {
293 return config.seed;
294 }
295
296 /**
297 * Returns the sequential tile index that this option is currently configured for.
298 *
299 * @return the sequential tile index that the quantization is configured for
300 *
301 * @see #setTileIndex(int)
302 */
303 public long getTileIndex() {
304 return tileIndex;
305 }
306
307 /**
308 * Returns the tile height
309 *
310 * @return the tile height in pixels
311 *
312 * @see #setTileHeight(int)
313 * @see #getTileWidth()
314 */
315 @Override
316 public int getTileHeight() {
317 return tileHeight;
318 }
319
320 /**
321 * Returns the tile width
322 *
323 * @return the tile width in pixels
324 *
325 * @see #setTileWidth(int)
326 * @see #getTileHeight()
327 */
328 @Override
329 public int getTileWidth() {
330 return tileWidth;
331 }
332
333 /**
334 * Checks whether we force the integer quantized level 0 to correspond to a floating-point level 0.0, when using
335 * automatic quantization.
336 *
337 * @return <code>true</code> if we want to keep `BZERO` at 0 when quantizing automatically.
338 *
339 * @see #setCenterOnZero(boolean)
340 */
341 public boolean isCenterOnZero() {
342 return config.centerOnZero;
343 }
344
345 /**
346 * Whether the floating-point data may contain <code>null</code> values (normally NaNs).
347 *
348 * @return <code>true</code> if we should expect <code>null</code> in the floating-point data. This is automatically
349 * <code>true</code> if {@link #setBNull(Integer)} was called with a non-null value.
350 *
351 * @see #setBNull(Integer)
352 */
353 public boolean isCheckNull() {
354 return checkNull;
355 }
356
357 /**
358 * Whether automatic quantization treats 0.0 as a special value. Normally values within the `BSCALE` quantization
359 * level around 0.0 will be assigned the same integer quanta, and will become indistinguishable in the quantized
360 * data. Some software may, in their misguided ways, assign exact zero values a special meaning (such as no data) in
361 * which case we may want to distinguish these as we apply quantization. However, it is generally not a good idea to
362 * use 0 as a special value.
363 *
364 * @return <code>true</code> to treat 0.0 (exact) as a special value, or <code>false</code> to treat is as any other
365 * measured value (recommended).
366 *
367 * @see #setCheckZero(boolean)
368 * @see #getBScale()
369 */
370 public boolean isCheckZero() {
371 return config.checkZero;
372 }
373
374 /**
375 * Whether dithering is enabled
376 *
377 * @return <code>true</code> if dithering is enabled, or else <code>false</code>
378 *
379 * @see #setDither(boolean)
380 * @see #isDither2()
381 */
382 public boolean isDither() {
383 return config.dither;
384 }
385
386 /**
387 * Whether dither method 2 is used.
388 *
389 * @return <code>true</code> if dither method 2 is used, or else <code>false</code>
390 *
391 * @see #setDither2(boolean)
392 * @see #isDither()
393 */
394 public boolean isDither2() {
395 return config.dither2;
396 }
397
398 @Override
399 public boolean isLossyCompression() {
400 return true;
401 }
402
403 /**
404 * Sets the integer value that represents missing data (<code>null</code>) in the quantized representation.
405 *
406 * @param blank the new integer blanking value (that is one that denotes a missing or <code>null</code> datum).
407 * Setting this option to <code>null</code> disables the treatment of issing or <code>null</code>
408 * data.
409 *
410 * @return itself
411 *
412 * @see #getBNull()
413 * @see #isCheckNull()
414 */
415 public ICompressOption setBNull(Integer blank) {
416 if (blank != null) {
417 nullValueIndicator = blank;
418 checkNull = true;
419 } else {
420 checkNull = false;
421 }
422 return this;
423 }
424
425 /**
426 * Sets the quantization level.
427 *
428 * @param value the new floating-point difference between integer levels in the quantized data.
429 *
430 * @return itself
431 *
432 * @see #setQlevel(double)
433 * @see #setBZero(double)
434 * @see #getBScale()
435 */
436 public QuantizeOption setBScale(double value) {
437 bScale = value;
438 return this;
439 }
440
441 /**
442 * Sets the quantization offset.
443 *
444 * @param value the new floating-point value corresponding to the integer level 0.
445 *
446 * @return itself
447 *
448 * @see #setBScale(double)
449 * @see #getBZero()
450 */
451 public QuantizeOption setBZero(double value) {
452 bZero = value;
453 return this;
454 }
455
456 /**
457 * Enabled or disables keeping `BZERO` at 0 when using automatic quantization.
458 *
459 * @param value <code>true</code> to keep `BZERO` at 0 when quantizing automatically, that is keep the integer
460 * quantized level 0 correspond to floating-point level 0.0. Or, <code>false</code> to let the
461 * automatic quantization algorithm determine the optimal quantization offset.
462 *
463 * @return iftself
464 *
465 * @see #isCenterOnZero()
466 */
467 public QuantizeOption setCenterOnZero(boolean value) {
468 config.centerOnZero = value;
469 return this;
470 }
471
472 /**
473 * @deprecated {@link #setBNull(Integer)} controls this feature automatically as needed. Sets whether we
474 * should expect the floating-point data to contain <code>null</code> values (normally NaNs).
475 *
476 * @param value <code>true</code> if the floating-point data may contain <code>null</code> values.
477 *
478 * @return itself
479 *
480 * @see #setCheckNull(boolean)
481 * @see #setBNull(Integer)
482 * @see #getNullValue()
483 */
484 @Deprecated
485 public QuantizeOption setCheckNull(boolean value) {
486 checkNull = value;
487 if (nullValueIndicator == null) {
488 nullValueIndicator = NULL_VALUE;
489 }
490 return this;
491 }
492
493 /**
494 * Sets whether automatic quantization is to treat 0.0 as a special value. Normally values within the `BSCALE`
495 * quantization level around 0.0 will be assigned the same integer quanta, and will become indistinguishable in the
496 * quantized data. However some software may assign exact zero values a special meaning (such as no data) in which
497 * case we may want to distinguish these as we apply qunatization. However, it is generally not a good idea to use 0
498 * as a special value. To mark missing data, the FITS standard recognises only NaN as a special value -- while all
499 * other values should constitute valid measurements.
500 *
501 * @deprecated It is strongly discouraged to treat 0.0 values as special. FITS only recognises NaN as a
502 * special floating-point value marking missing data. All other floating point values are
503 * considered valid measurements.
504 *
505 * @param value whether to treat values around 0.0 as special.
506 *
507 * @return itself
508 *
509 * @see #isCheckZero()
510 */
511 @Deprecated
512 public QuantizeOption setCheckZero(boolean value) {
513 config.checkZero = value;
514 return this;
515 }
516
517 /**
518 * Enables or disables dithering.
519 *
520 * @param value <code>true</code> to enable dithering, or else <code>false</code> to disable
521 *
522 * @return itself
523 *
524 * @see #isDither()
525 * @see #setDither2(boolean)
526 */
527 public QuantizeOption setDither(boolean value) {
528 config.dither = value;
529 return this;
530 }
531
532 /**
533 * Sets whether dithering is to use method 2.
534 *
535 * @param value <code>true</code> to use dither method 2, or else <code>false</code> for method 1.
536 *
537 * @return itself
538 *
539 * @see #isDither2()
540 * @see #setDither(boolean)
541 */
542 public QuantizeOption setDither2(boolean value) {
543 config.dither2 = value;
544 return this;
545 }
546
547 /**
548 * Sets the maximum integer level in the quantized representation.
549 *
550 * @param value the new maximum integer level in the quantized data.
551 *
552 * @return itself
553 *
554 * @see #getIntMaxValue()
555 * @see #setIntMinValue(int)
556 */
557 public QuantizeOption setIntMaxValue(int value) {
558 intMaxValue = value;
559 return this;
560 }
561
562 /**
563 * Sets the minimum integer level in the quantized representation.
564 *
565 * @param value the new minimum integer level in the quantized data.
566 *
567 * @return itself
568 *
569 * @see #getIntMinValue()
570 * @see #setIntMaxValue(int)
571 */
572 public QuantizeOption setIntMinValue(int value) {
573 intMinValue = value;
574 return this;
575 }
576
577 /**
578 * Sets the maximum floating-point value in the data
579 *
580 * @param value the maximum floating-point value in the data before quantization.
581 *
582 * @return itself
583 *
584 * @see #getMaxValue()
585 * @see #setMinValue(double)
586 */
587 public QuantizeOption setMaxValue(double value) {
588 maxValue = value;
589 return this;
590 }
591
592 /**
593 * Sets the minimum floating-point value in the data
594 *
595 * @param value the mininum floating-point value in the data before quantization.
596 *
597 * @return itself
598 *
599 * @see #getMinValue()
600 * @see #setMaxValue(double)
601 */
602 public QuantizeOption setMinValue(double value) {
603 minValue = value;
604 return this;
605 }
606
607 /**
608 * @deprecated The use of null values other than <code>NaN</code> for floating-point data types is not
609 * standard in FITS. You should therefore avoid using this method to change it. Returns the
610 * floating-point value that indicates a <code>null</code> datum in the image before
611 * quantization is applied. Normally, the FITS standard is that NaN values indicate
612 * <code>null</code> values in floating-point images. While this class allows using other
613 * values also, they are not recommended since they are not supported by FITS in a standard
614 * way.
615 *
616 * @param value the new floating-point value that represents a <code>null</code> value (missing data) in the
617 * image before quantization.
618 *
619 * @return itself
620 *
621 * @see #setNullValue(double)
622 */
623 @Deprecated
624 public QuantizeOption setNullValue(double value) {
625 nullValue = value;
626 return this;
627 }
628
629 @Override
630 public void setParameters(ICompressParameters parameters) {
631 if (parameters instanceof QuantizeParameters) {
632 this.parameters = (QuantizeParameters) parameters.copy(this);
633 } else if (parameters instanceof BundledParameters) {
634 BundledParameters bundle = (BundledParameters) parameters;
635 for (int i = 0; i < bundle.size(); i++) {
636 setParameters(bundle.get(i));
637 }
638 } else if (compressOption != null) {
639 compressOption.setParameters(parameters);
640 }
641 }
642
643 /**
644 * Sets the quantization resolution level to use for automatic quantization. For Gaussian noise the quantization
645 * level is the standard deviation of the noise divided by this Q value. Thus Q values of a few will ensusre that
646 * quantization retains just about all of the information contained in the noisy data.
647 *
648 * @param value The new Q value, defined as the number of quantized levels per standard deviation (for Gaussian
649 * noise).
650 *
651 * @return itself
652 *
653 * @see #getQLevel()
654 * @see #setBScale(double)
655 */
656 public QuantizeOption setQlevel(double value) {
657 config.qlevel = value;
658 return this;
659 }
660
661 /**
662 * Sets the seed value for the dither random generator
663 *
664 * @param value The seed value, as in <code>ZDITHER0</code>, normally a number between 1 and 10000 (inclusive).
665 *
666 * @return itself
667 *
668 * @see #setTileIndex(int)
669 */
670 public QuantizeOption setSeed(long value) {
671 config.seed = value;
672 return this;
673 }
674
675 /**
676 * Sets the tile index for which to initialize the random number generator with the given seed (i.e.
677 * <code>ZDITHER0</code> value).
678 *
679 * @param index The 0-based tile index
680 *
681 * @return itself
682 *
683 * @see #setSeed(long)
684 */
685 public QuantizeOption setTileIndex(int index) {
686 tileIndex = index;
687 return this;
688 }
689
690 @Override
691 public QuantizeOption setTileHeight(int value) {
692 tileHeight = value;
693 if (compressOption != null) {
694 compressOption.setTileHeight(value);
695 }
696 return this;
697 }
698
699 @Override
700 public QuantizeOption setTileWidth(int value) {
701 tileWidth = value;
702 if (compressOption != null) {
703 compressOption.setTileWidth(value);
704 }
705 return this;
706 }
707
708 @Override
709 public <T> T unwrap(Class<T> clazz) {
710 if (clazz.isAssignableFrom(this.getClass())) {
711 return clazz.cast(this);
712 }
713 if (compressOption != null) {
714 if (clazz.isAssignableFrom(compressOption.getClass())) {
715 return clazz.cast(compressOption);
716 }
717 }
718 return null;
719 }
720
721 /**
722 * Stores configuration in a way that can be shared and modified across enclosing option copies.
723 *
724 * @author Attila Kovacs
725 *
726 * @since 1.18
727 */
728 private static final class Config {
729
730 private boolean centerOnZero;
731
732 private boolean checkZero;
733
734 private boolean dither;
735
736 private boolean dither2;
737
738 private double qlevel = Double.NaN;
739
740 private long seed = 1L;
741 }
742 }