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 public final Integer getNullValueIndicator() {
265 return getBNull();
266 }
267
268 /**
269 * Returns the quantization resolution level used for automatic qunatization. For Gaussian noise the quantization
270 * level is the standard deviation of the noise divided by this Q value. Thus Q values of a few will ensure that
271 * quantization retains just about all of the information in the noisy data.
272 *
273 * @return The current Q value, defined as the number of quantized levels per standard deviation (for Gaussian
274 * noise).
275 *
276 * @see #setQlevel(double)
277 * @see #getBScale()
278 */
279 public double getQLevel() {
280 return config.qlevel;
281 }
282
283 /**
284 * Gets the random seed value used for dithering
285 *
286 * @return the random seed value used for dithering
287 *
288 * @see #setSeed(long)
289 * @see RandomSequence
290 */
291 public long getSeed() {
292 return config.seed;
293 }
294
295 /**
296 * Returns the sequential tile index that this option is currently configured for.
297 *
298 * @return the sequential tile index that the quantization is configured for
299 *
300 * @see #setTileIndex(int)
301 */
302 public long getTileIndex() {
303 return tileIndex;
304 }
305
306 /**
307 * Returns the tile height
308 *
309 * @return the tile height in pixels
310 *
311 * @see #setTileHeight(int)
312 * @see #getTileWidth()
313 */
314 @Override
315 public int getTileHeight() {
316 return tileHeight;
317 }
318
319 /**
320 * Returns the tile width
321 *
322 * @return the tile width in pixels
323 *
324 * @see #setTileWidth(int)
325 * @see #getTileHeight()
326 */
327 @Override
328 public int getTileWidth() {
329 return tileWidth;
330 }
331
332 /**
333 * Checks whether we force the integer quantized level 0 to correspond to a floating-point level 0.0, when using
334 * automatic quantization.
335 *
336 * @return <code>true</code> if we want to keep `BZERO` at 0 when quantizing automatically.
337 *
338 * @see #setCenterOnZero(boolean)
339 */
340 public boolean isCenterOnZero() {
341 return config.centerOnZero;
342 }
343
344 /**
345 * Whether the floating-point data may contain <code>null</code> values (normally NaNs).
346 *
347 * @return <code>true</code> if we should expect <code>null</code> in the floating-point data. This is automatically
348 * <code>true</code> if {@link #setBNull(Integer)} was called with a non-null value.
349 *
350 * @see #setBNull(Integer)
351 */
352 public boolean isCheckNull() {
353 return checkNull;
354 }
355
356 /**
357 * Whether automatic quantization treats 0.0 as a special value. Normally values within the `BSCALE` quantization
358 * level around 0.0 will be assigned the same integer quanta, and will become indistinguishable in the quantized
359 * data. Some software may, in their misguided ways, assign exact zero values a special meaning (such as no data) in
360 * which case we may want to distinguish these as we apply quantization. However, it is generally not a good idea to
361 * use 0 as a special value.
362 *
363 * @return <code>true</code> to treat 0.0 (exact) as a special value, or <code>false</code> to treat is as any other
364 * measured value (recommended).
365 *
366 * @see #setCheckZero(boolean)
367 * @see #getBScale()
368 */
369 public boolean isCheckZero() {
370 return config.checkZero;
371 }
372
373 /**
374 * Whether dithering is enabled
375 *
376 * @return <code>true</code> if dithering is enabled, or else <code>false</code>
377 *
378 * @see #setDither(boolean)
379 * @see #isDither2()
380 */
381 public boolean isDither() {
382 return config.dither;
383 }
384
385 /**
386 * Whether dither method 2 is used.
387 *
388 * @return <code>true</code> if dither method 2 is used, or else <code>false</code>
389 *
390 * @see #setDither2(boolean)
391 * @see #isDither()
392 */
393 public boolean isDither2() {
394 return config.dither2;
395 }
396
397 @Override
398 public boolean isLossyCompression() {
399 return true;
400 }
401
402 /**
403 * Sets the integer value that represents missing data (<code>null</code>) in the quantized representation.
404 *
405 * @param blank the new integer blanking value (that is one that denotes a missing or <code>null</code> datum).
406 * Setting this option to <code>null</code> disables the treatment of issing or <code>null</code>
407 * data.
408 *
409 * @return itself
410 *
411 * @see #getBNull()
412 * @see #isCheckNull()
413 */
414 public ICompressOption setBNull(Integer blank) {
415 if (blank != null) {
416 nullValueIndicator = blank;
417 checkNull = true;
418 } else {
419 checkNull = false;
420 }
421 return this;
422 }
423
424 /**
425 * Sets the quantization level.
426 *
427 * @param value the new floating-point difference between integer levels in the quantized data.
428 *
429 * @return itself
430 *
431 * @see #setQlevel(double)
432 * @see #setBZero(double)
433 * @see #getBScale()
434 */
435 public QuantizeOption setBScale(double value) {
436 bScale = value;
437 return this;
438 }
439
440 /**
441 * Sets the quantization offset.
442 *
443 * @param value the new floating-point value corresponding to the integer level 0.
444 *
445 * @return itself
446 *
447 * @see #setBScale(double)
448 * @see #getBZero()
449 */
450 public QuantizeOption setBZero(double value) {
451 bZero = value;
452 return this;
453 }
454
455 /**
456 * Enabled or disables keeping `BZERO` at 0 when using automatic quantization.
457 *
458 * @param value <code>true</code> to keep `BZERO` at 0 when quantizing automatically, that is keep the integer
459 * quantized level 0 correspond to floating-point level 0.0. Or, <code>false</code> to let the
460 * automatic quantization algorithm determine the optimal quantization offset.
461 *
462 * @return iftself
463 *
464 * @see #isCenterOnZero()
465 */
466 public QuantizeOption setCenterOnZero(boolean value) {
467 config.centerOnZero = value;
468 return this;
469 }
470
471 /**
472 * @deprecated {@link #setBNull(Integer)} controls this feature automatically as needed. Sets whether we
473 * should expect the floating-point data to contain <code>null</code> values (normally NaNs).
474 *
475 * @param value <code>true</code> if the floating-point data may contain <code>null</code> values.
476 *
477 * @return itself
478 *
479 * @see #setCheckNull(boolean)
480 * @see #setBNull(Integer)
481 * @see #getNullValue()
482 */
483 public QuantizeOption setCheckNull(boolean value) {
484 checkNull = value;
485 if (nullValueIndicator == null) {
486 nullValueIndicator = NULL_VALUE;
487 }
488 return this;
489 }
490
491 /**
492 * Sets whether automatic quantization is to treat 0.0 as a special value. Normally values within the `BSCALE`
493 * quantization level around 0.0 will be assigned the same integer quanta, and will become indistinguishable in the
494 * quantized data. However some software may assign exact zero values a special meaning (such as no data) in which
495 * case we may want to distinguish these as we apply qunatization. However, it is generally not a good idea to use 0
496 * as a special value. To mark missing data, the FITS standard recognises only NaN as a special value -- while all
497 * other values should constitute valid measurements.
498 *
499 * @deprecated It is strongly discouraged to treat 0.0 values as special. FITS only recognises NaN as a
500 * special floating-point value marking missing data. All other floating point values are
501 * considered valid measurements.
502 *
503 * @param value
504 *
505 * @return itself
506 *
507 * @see #isCheckZero()
508 */
509 public QuantizeOption setCheckZero(boolean value) {
510 config.checkZero = value;
511 return this;
512 }
513
514 /**
515 * Enables or disables dithering.
516 *
517 * @param value <code>true</code> to enable dithering, or else <code>false</code> to disable
518 *
519 * @return itself
520 *
521 * @see #isDither()
522 * @see #setDither2(boolean)
523 */
524 public QuantizeOption setDither(boolean value) {
525 config.dither = value;
526 return this;
527 }
528
529 /**
530 * Sets whether dithering is to use method 2.
531 *
532 * @param value <code>true</code> to use dither method 2, or else <code>false</code> for method 1.
533 *
534 * @return itself
535 *
536 * @see #isDither2()
537 * @see #setDither(boolean)
538 */
539 public QuantizeOption setDither2(boolean value) {
540 config.dither2 = value;
541 return this;
542 }
543
544 /**
545 * Sets the maximum integer level in the quantized representation.
546 *
547 * @param value the new maximum integer level in the quantized data.
548 *
549 * @return itself
550 *
551 * @see #getIntMaxValue()
552 * @see #setIntMinValue(int)
553 */
554 public QuantizeOption setIntMaxValue(int value) {
555 intMaxValue = value;
556 return this;
557 }
558
559 /**
560 * Sets the minimum integer level in the quantized representation.
561 *
562 * @param value the new minimum integer level in the quantized data.
563 *
564 * @return itself
565 *
566 * @see #getIntMinValue()
567 * @see #setIntMaxValue(int)
568 */
569 public QuantizeOption setIntMinValue(int value) {
570 intMinValue = value;
571 return this;
572 }
573
574 /**
575 * Sets the maximum floating-point value in the data
576 *
577 * @param value the maximum floating-point value in the data before quantization.
578 *
579 * @return itself
580 *
581 * @see #getMaxValue()
582 * @see #setMinValue(double)
583 */
584 public QuantizeOption setMaxValue(double value) {
585 maxValue = value;
586 return this;
587 }
588
589 /**
590 * Sets the minimum floating-point value in the data
591 *
592 * @param value the mininum floating-point value in the data before quantization.
593 *
594 * @return itself
595 *
596 * @see #getMinValue()
597 * @see #setMaxValue(double)
598 */
599 public QuantizeOption setMinValue(double value) {
600 minValue = value;
601 return this;
602 }
603
604 /**
605 * @deprecated The use of null values other than <code>NaN</code> for floating-point data types is not
606 * standard in FITS. You should therefore avoid using this method to change it. Returns the
607 * floating-point value that indicates a <code>null</code> datum in the image before
608 * quantization is applied. Normally, the FITS standard is that NaN values indicate
609 * <code>null</code> values in floating-point images. While this class allows using other
610 * values also, they are not recommended since they are not supported by FITS in a standard
611 * way.
612 *
613 * @param value the new floating-point value that represents a <code>null</code> value (missing data) in the
614 * image before quantization.
615 *
616 * @return itself
617 *
618 * @see #setNullValue(double)
619 */
620 public QuantizeOption setNullValue(double value) {
621 nullValue = value;
622 return this;
623 }
624
625 @Override
626 public void setParameters(ICompressParameters parameters) {
627 if (parameters instanceof QuantizeParameters) {
628 this.parameters = (QuantizeParameters) parameters.copy(this);
629 } else if (parameters instanceof BundledParameters) {
630 BundledParameters bundle = (BundledParameters) parameters;
631 for (int i = 0; i < bundle.size(); i++) {
632 setParameters(bundle.get(i));
633 }
634 } else if (compressOption != null) {
635 compressOption.setParameters(parameters);
636 }
637 }
638
639 /**
640 * Sets the quantization resolution level to use for automatic quantization. For Gaussian noise the quantization
641 * level is the standard deviation of the noise divided by this Q value. Thus Q values of a few will ensusre that
642 * quantization retains just about all of the information contained in the noisy data.
643 *
644 * @param value The new Q value, defined as the number of quantized levels per standard deviation (for Gaussian
645 * noise).
646 *
647 * @return itself
648 *
649 * @see #getQLevel()
650 * @see #setBScale(double)
651 */
652 public QuantizeOption setQlevel(double value) {
653 config.qlevel = value;
654 return this;
655 }
656
657 /**
658 * Sets the seed value for the dither random generator
659 *
660 * @param value The seed value, as in <code>ZDITHER0</code>, normally a number between 1 and 10000 (inclusive).
661 *
662 * @return itself
663 *
664 * @see #setTileIndex(int)
665 */
666 public QuantizeOption setSeed(long value) {
667 config.seed = value;
668 return this;
669 }
670
671 /**
672 * Sets the tile index for which to initialize the random number generator with the given seed (i.e.
673 * <code>ZDITHER0</code> value).
674 *
675 * @param index The 0-based tile index
676 *
677 * @return itself
678 *
679 * @see #setSeed(long)
680 */
681 public QuantizeOption setTileIndex(int index) {
682 tileIndex = index;
683 return this;
684 }
685
686 @Override
687 public QuantizeOption setTileHeight(int value) {
688 tileHeight = value;
689 if (compressOption != null) {
690 compressOption.setTileHeight(value);
691 }
692 return this;
693 }
694
695 @Override
696 public QuantizeOption setTileWidth(int value) {
697 tileWidth = value;
698 if (compressOption != null) {
699 compressOption.setTileWidth(value);
700 }
701 return this;
702 }
703
704 @Override
705 public <T> T unwrap(Class<T> clazz) {
706 if (clazz.isAssignableFrom(this.getClass())) {
707 return clazz.cast(this);
708 }
709 if (compressOption != null) {
710 if (clazz.isAssignableFrom(compressOption.getClass())) {
711 return clazz.cast(compressOption);
712 }
713 }
714 return null;
715 }
716
717 /**
718 * Stores configuration in a way that can be shared and modified across enclosing option copies.
719 *
720 * @author Attila Kovacs
721 *
722 * @since 1.18
723 */
724 private static final class Config {
725
726 private boolean centerOnZero;
727
728 private boolean checkZero;
729
730 private boolean dither;
731
732 private boolean dither2;
733
734 private double qlevel = Double.NaN;
735
736 private long seed = 1L;
737 }
738 }