View Javadoc
1   package nom.tam.image.compression.hdu;
2   
3   import java.io.IOException;
4   import java.nio.Buffer;
5   import java.nio.ByteBuffer;
6   import java.util.Arrays;
7   import java.util.HashMap;
8   import java.util.List;
9   import java.util.Map;
10  
11  import nom.tam.fits.BinaryTableHDU;
12  import nom.tam.fits.FitsException;
13  import nom.tam.fits.FitsUtil;
14  import nom.tam.fits.Header;
15  import nom.tam.fits.HeaderCard;
16  import nom.tam.fits.HeaderCardException;
17  import nom.tam.fits.ImageData;
18  import nom.tam.fits.ImageHDU;
19  import nom.tam.fits.compression.algorithm.api.ICompressOption;
20  import nom.tam.fits.header.Compression;
21  import nom.tam.fits.header.GenericKey;
22  import nom.tam.fits.header.IFitsHeader;
23  import nom.tam.fits.header.Standard;
24  import nom.tam.image.compression.CompressedImageTiler;
25  import nom.tam.util.ByteBufferInputStream;
26  import nom.tam.util.ByteBufferOutputStream;
27  import nom.tam.util.Cursor;
28  import nom.tam.util.FitsInputStream;
29  import nom.tam.util.FitsOutputStream;
30  
31  /*
32   * #%L
33   * nom.tam FITS library
34   * %%
35   * Copyright (C) 1996 - 2024 nom-tam-fits
36   * %%
37   * This is free and unencumbered software released into the public domain.
38   *
39   * Anyone is free to copy, modify, publish, use, compile, sell, or
40   * distribute this software, either in source code form or as a compiled
41   * binary, for any purpose, commercial or non-commercial, and by any
42   * means.
43   *
44   * In jurisdictions that recognize copyright laws, the author or authors
45   * of this software dedicate any and all copyright interest in the
46   * software to the public domain. We make this dedication for the benefit
47   * of the public at large and to the detriment of our heirs and
48   * successors. We intend this dedication to be an overt act of
49   * relinquishment in perpetuity of all present and future rights to this
50   * software under copyright law.
51   *
52   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
53   * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
54   * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
55   * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
56   * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
57   * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
58   * OTHER DEALINGS IN THE SOFTWARE.
59   * #L%
60   */
61  
62  import static nom.tam.fits.header.Compression.ZIMAGE;
63  import static nom.tam.fits.header.Standard.BLANK;
64  
65  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
66  
67  /**
68   * <p>
69   * A header-data unit (HDU) containing a compressed image. A compressed image is a normal binary table with some
70   * additional constraints. The original image is divided into tiles and each tile is compressed on its own. The
71   * compressed data is then stored in the 3 data columns of this binary table (compressed, gzipped and uncompressed)
72   * depending on the compression type used in the tile. Additional data columns may contain specific compression or
73   * quantization options for each tile (i.e. compressed table row) individually. Table keywords, which conflict with
74   * those in the original image are 'saved' under standard alternative names, so they may be restored with the image as
75   * appropriate.
76   * </p>
77   * <p>
78   * Compressing an image HDU is typically a multi-step process:
79   * </p>
80   * <ol>
81   * <li>Create a <code>CompressedImageHDU</code>, e.g. with {@link #fromImageHDU(ImageHDU, int...)}.</li>
82   * <li>Set up the compression algorithm, including quantization (if desired) via {@link #setCompressAlgorithm(String)}
83   * and {@link #setQuantAlgorithm(String)}, and optionally the compressiomn method used for preserving the blank values
84   * via {@link #preserveNulls(String)}.</li>
85   * <li>Set compression (and quantization) options, via calling on {@link #getCompressOption(Class)}</li>
86   * <li>Perform the compression via {@link #compress()}</li>
87   * </ol>
88   * <p>
89   * For example to compress an image HDU:
90   * </p>
91   * 
92   * <pre>
93   *   ImageHDU image = ...
94   *   
95   *   // 1. Create compressed HDU
96   *   CompressedImageHDU compressed = CompressedImageHDU.fromImageHDU(image, 60, 40);
97   *   
98   *   // 2. Set compression (and optional qunatizaiton) algorithm(s)
99   *   compressed.setCompressAlgorithm(Compression.ZCMPTYPE_RICE_1)
100  *             .setQuantAlgorithm(Compression.ZQUANTIZ_SUBTRACTIVE_DITHER_1)
101  *             .preserveNulls(Compression.ZCMPTYPE_HCOMPRESS_1);
102  *             
103  *   // 3. Set compression (and quantizaiton) options
104  *   compressed.getCompressOption(RiceCompressOption.class).setBlockSize(32);
105  *   compressed.getCompressOption(QuantizeOption.class).setBZero(3.0).setBScale(0.1).setBNull(-999);
106  *                    
107  *   // 4. Perform the compression.
108  *   compressed.compress();
109  * </pre>
110  * <p>
111  * After the compression, the compressed image HDU can be handled just like any other HDU, and written to a file or
112  * stream, for example.
113  * </p>
114  * <p>
115  * The reverse process is simply via the {@link #asImageHDU()} method. E.g.:
116  * </p>
117  * 
118  * <pre>
119  *    CompressedImageHDU compressed = ...
120  *    ImageHDU image = compressed.asImageHDU();
121  * </pre>
122  * 
123  * @see CompressedImageData
124  * @see nom.tam.image.compression.CompressedImageTiler
125  */
126 @SuppressWarnings("deprecation")
127 public class CompressedImageHDU extends BinaryTableHDU {
128     /** The maximum number of table columns FITS supports */
129     public static final int MAX_NAXIS_ALLOWED = 999;
130 
131     /**
132      * keys that are only valid in tables and should not go into the uncompressed image.
133      */
134     private static final List<IFitsHeader> TABLE_COLUMN_KEYS = Arrays.asList(binaryTableColumnKeyStems());
135 
136     static final Map<IFitsHeader, CompressedCard> COMPRESSED_HEADER_MAPPING = new HashMap<>();
137 
138     static final Map<IFitsHeader, CompressedCard> UNCOMPRESSED_HEADER_MAPPING = new HashMap<>();
139 
140     /**
141      * Prepare a compressed image hdu for the specified image. the tile axis that are specified with -1 default to
142      * tiling by rows. To actually perform the compression, you will next have to select the compression algorithm (and
143      * optinally a quantization algorithm), then configure options for these, and finally call {@link #compress()} to
144      * perform the compression. See the description of this class for more details.
145      *
146      * @param  imageHDU      the image to compress
147      * @param  tileAxis      the requested tile sizes in pixels in x, y, z... order (i.e. opposite of the Java array
148      *                           indexing order!). The actual tile sizes that are set might be different, e.g. to fit
149      *                           within the image bounds and/or to conform to tiling conventions (esp. in more than 2
150      *                           dimensions).
151      *
152      * @return               the prepared compressed image hdu.
153      *
154      * @throws FitsException if the image could not be used to create a compressed image.
155      * 
156      * @see                  #asImageHDU()
157      * @see                  #setCompressAlgorithm(String)
158      * @see                  #setQuantAlgorithm(String)
159      * @see                  #getCompressOption(Class)
160      * @see                  #compress()
161      */
162     public static CompressedImageHDU fromImageHDU(ImageHDU imageHDU, int... tileAxis) throws FitsException {
163         Header header = new Header();
164         CompressedImageData compressedData = new CompressedImageData();
165         int[] size = imageHDU.getAxes();
166         int[] tileSize = new int[size.length];
167 
168         compressedData.setAxis(size);
169 
170         // Start with the default tile size.
171         int nm1 = size.length - 1;
172         Arrays.fill(tileSize, 1);
173         tileSize[nm1] = size[nm1];
174 
175         // Check and apply the requested tile sizes.
176         int n = Math.min(size.length, tileAxis.length);
177         for (int i = 0; i < n; i++) {
178             if (tileAxis[i] > 0) {
179                 tileSize[nm1 - i] = Math.min(tileAxis[i], size[nm1 - i]);
180             }
181         }
182 
183         compressedData.setTileSize(tileSize);
184 
185         compressedData.fillHeader(header);
186         Cursor<String, HeaderCard> iterator = header.iterator();
187         Cursor<String, HeaderCard> imageIterator = imageHDU.getHeader().iterator();
188         while (imageIterator.hasNext()) {
189             HeaderCard card = imageIterator.next();
190             CompressedCard.restore(card, iterator);
191         }
192         CompressedImageHDU compressedImageHDU = new CompressedImageHDU(header, compressedData);
193         compressedData.prepareUncompressedData(imageHDU.getData().getData(), header);
194         return compressedImageHDU;
195     }
196 
197     /**
198      * Check that this HDU has a valid header for this type.
199      * 
200      * @deprecated     (<i>for internal use</i>) Will reduce visibility in the future
201      *
202      * @param      hdr header to check
203      *
204      * @return         <CODE>true</CODE> if this HDU has a valid header.
205      */
206     @Deprecated
207     @SuppressFBWarnings(value = "HSM_HIDING_METHOD", justification = "deprecated existing method, kept for compatibility")
208     public static boolean isHeader(Header hdr) {
209         return hdr.getBooleanValue(ZIMAGE, false);
210     }
211 
212     /**
213      * Returns an empty compressed image data object based on its description in a FITS header.
214      * 
215      * @param      hdr           the FITS header containing a description of the compressed image
216      * 
217      * @return                   an empty compressed image data corresponding to the header description.
218      * 
219      * @throws     FitsException if the header does not sufficiently describe a compressed image
220      * 
221      * @deprecated               (<i>for internal use</i>) Will reduce visibility in the future
222      */
223     @Deprecated
224     public static CompressedImageData manufactureData(Header hdr) throws FitsException {
225         return new CompressedImageData(hdr);
226     }
227 
228     /**
229      * Creates an new compressed image HDU with the specified header and compressed data.
230      * 
231      * @param hdr   the header
232      * @param datum the compressed image data. The data may not be actually compressed at this point, int which case you
233      *                  may need to call {@link #compress()} before writing the new compressed HDU to a stream.
234      * 
235      * @see         #compress()
236      */
237     public CompressedImageHDU(Header hdr, CompressedImageData datum) {
238         super(hdr, datum);
239     }
240 
241     /**
242      * Restores the original image HDU by decompressing the data contained in this compresed image HDU.
243      * 
244      * @return               The uncompressed Image HDU.
245      * 
246      * @throws FitsException If there was an issue with the decompression.
247      * 
248      * @see                  #getTileHDU(int[], int[])
249      * @see                  #fromImageHDU(ImageHDU, int...)
250      */
251     public ImageHDU asImageHDU() throws FitsException {
252         final Header header = getImageHeader();
253         ImageData data = ImageHDU.manufactureData(header);
254         ImageHDU imageHDU = new ImageHDU(header, data);
255         data.setBuffer(getUncompressedData());
256         return imageHDU;
257     }
258 
259     /**
260      * Returns an <code>ImageHDU</code>, with the specified decompressed image area. The HDU's header will be adjusted
261      * as necessary to reflect the correct size and coordinate system of the image cutout.
262      * 
263      * @param  corners                  the location in pixels where the tile begins in the full (uncompressed) image.
264      *                                      The number of elements in the array must match the image dimnesion.
265      * @param  lengths                  the size of the tile in pixels. The number of elements in the array must match
266      *                                      the image dimnesion.
267      * 
268      * @return                          a new image HDU containing the selected area of the uncompresed image, including
269      *                                      the adjusted header for the selected area,
270      * 
271      * @throws IOException              If the tiling operation itself could not be performed
272      * @throws FitsException            If the compressed image itself if invalid or imcomplete
273      * @throws IllegalArgumentException if the tile area is not fully contained inside the uncompressed image or if the
274      *                                      lengths are not positive definite.
275      * 
276      * @see                             #asImageHDU()
277      * @see                             CompressedImageTiler#getTile(int[], int[])
278      * 
279      * @since                           1.18
280      */
281     public ImageHDU getTileHDU(int[] corners, int[] lengths) throws IOException, FitsException, IllegalArgumentException {
282         Header h = getImageHeader();
283 
284         int dim = h.getIntValue(Standard.NAXIS);
285 
286         if (corners.length != lengths.length || corners.length != dim) {
287             throw new IllegalArgumentException("arguments for mismatched dimensions");
288         }
289 
290         // Edit the image bound for the tile
291         for (int i = 0; i < corners.length; i++) {
292             int naxis = h.getIntValue(Standard.NAXISn.n(dim - i));
293 
294             if (lengths[0] <= 0) {
295                 throw new IllegalArgumentException("Illegal tile size in dim " + i + ": " + lengths[i]);
296             }
297 
298             if (corners[i] < 0 || corners[i] + lengths[i] > naxis) {
299                 throw new IllegalArgumentException("tile out of bounds in dim " + i + ": [" + corners[i] + ":"
300                         + (corners[i] + lengths[i]) + "] in " + naxis);
301             }
302 
303             h.addValue(Standard.NAXISn.n(dim - i), lengths[i]);
304 
305             // Adjust the CRPIXn values
306             HeaderCard crpix = h.getCard(Standard.CRPIXn.n(dim - i));
307             if (crpix != null) {
308                 crpix.setValue(crpix.getValue(Double.class, Double.NaN) - corners[i]);
309             }
310 
311             // Adjust CRPIXna values
312             for (char c = 'A'; c <= 'Z'; c++) {
313                 crpix = h.getCard("CRPIX" + (dim - i) + Character.toString(c));
314                 if (crpix != null) {
315                     crpix.setValue(crpix.getValue(Double.class, Double.NaN) - corners[i]);
316                 }
317             }
318         }
319 
320         ImageData im = ImageHDU.manufactureData(h);
321         ByteBuffer buf = ByteBuffer.wrap(new byte[(int) FitsUtil.addPadding(im.getSize())]);
322 
323         try (FitsOutputStream out = new FitsOutputStream(new ByteBufferOutputStream(buf))) {
324             new CompressedImageTiler(this).getTile(out, corners, lengths);
325             out.close();
326         }
327 
328         // Rewind buffer for reading, including padding.
329         buf.limit(buf.capacity());
330         buf.position(0);
331 
332         try (FitsInputStream in = new FitsInputStream(new ByteBufferInputStream(buf))) {
333             im.read(in);
334             in.close();
335         }
336 
337         return new ImageHDU(h, im);
338     }
339 
340     /**
341      * Given this compressed HDU, get the original (decompressed) axes.
342      *
343      * @return               the dimensions of the axis.
344      *
345      * @throws FitsException if the axis are configured wrong.
346      *
347      * @since                1.18
348      */
349     public int[] getImageAxes() throws FitsException {
350         int nAxis = myHeader.getIntValue(Compression.ZNAXIS);
351         if (nAxis < 0) {
352             throw new FitsException("Negative ZNAXIS (or NAXIS) value " + nAxis);
353         }
354         if (nAxis > CompressedImageHDU.MAX_NAXIS_ALLOWED) {
355             throw new FitsException("ZNAXIS/NAXIS value " + nAxis + " too large");
356         }
357 
358         if (nAxis == 0) {
359             return null;
360         }
361 
362         final int[] axes = new int[nAxis];
363         for (int i = 1; i <= nAxis; i++) {
364             axes[nAxis - i] = myHeader.getIntValue(Compression.ZNAXISn.n(i));
365         }
366 
367         return axes;
368     }
369 
370     /**
371      * Obtain a header representative of a decompressed ImageHDU.
372      *
373      * @return                     Header with decompressed cards.
374      *
375      * @throws HeaderCardException if the card could not be copied
376      *
377      * @since                      1.18
378      */
379     public Header getImageHeader() throws HeaderCardException {
380         Header header = new Header();
381 
382         Cursor<String, HeaderCard> imageIterator = header.iterator();
383         Cursor<String, HeaderCard> iterator = getHeader().iterator();
384 
385         while (iterator.hasNext()) {
386             HeaderCard card = iterator.next();
387 
388             if (!TABLE_COLUMN_KEYS.contains(GenericKey.lookup(card.getKey()))) {
389                 CompressedCard.backup(card, imageIterator);
390             }
391         }
392         return header;
393     }
394 
395     /**
396      * Performs the actual compression with the selected algorithm(s) and options. When creating a compressed image HDU,
397      * e.g using the {@link #fromImageHDU(ImageHDU, int...)} method, the HDU is merely prepared but without actually
398      * performing the compression to allow the user to configure the algorithm(S) to be used as well as any specific
399      * compression (or quantization) options. See details in the class description.
400      * 
401      * @throws FitsException if the compression could not be performed
402      * 
403      * @see                  #fromImageHDU(ImageHDU, int...)
404      * @see                  #setCompressAlgorithm(String)
405      * @see                  #setQuantAlgorithm(String)
406      * @see                  #getCompressOption(Class)
407      */
408     public void compress() throws FitsException {
409         getData().compress(this);
410     }
411 
412     /**
413      * Specify an area within the image that will not undergo a lossy compression. This will only have affect it the
414      * selected compression (including the options) is a lossy compression. All tiles touched by this region will be
415      * handled so that there is no loss of any data, the reconstruction will be exact.
416      *
417      * @param  x      the x position in the image
418      * @param  y      the y position in the image
419      * @param  width  the width of the area
420      * @param  heigth the height of the area
421      *
422      * @return        this
423      */
424     public CompressedImageHDU forceNoLoss(int x, int y, int width, int heigth) {
425         getData().forceNoLoss(x, y, width, heigth);
426         return this;
427     }
428 
429     /**
430      * Returns the compression (or quantization) options for a selected compression option class. It is presumed that
431      * the requested options are appropriate for the compression and/or quantization algorithm that was selected. E.g.,
432      * if you called <code>setCompressionAlgorithm({@link Compression#ZCMPTYPE_RICE_1})</code>, then you can retrieve
433      * options for it with this method as
434      * <code>getCompressOption({@link nom.tam.fits.compression.algorithm.rice.RiceCompressOption}.class)</code>.
435      * 
436      * @param  <T>   The generic type of the compression class
437      * @param  clazz the compression class
438      * 
439      * @return       The current set of options for the requested type, or <code>null</code> if there are no options or
440      *                   if the requested type does not match the algorithm(s) selected.
441      * 
442      * @see          nom.tam.fits.compression.algorithm.hcompress.HCompressorOption
443      * @see          nom.tam.fits.compression.algorithm.rice.RiceCompressOption
444      * @see          nom.tam.fits.compression.algorithm.quant.QuantizeOption
445      */
446     public <T extends ICompressOption> T getCompressOption(Class<T> clazz) {
447         return getData().getCompressOption(clazz);
448     }
449 
450     @Override
451     public CompressedImageData getData() {
452         return (CompressedImageData) super.getData();
453     }
454 
455     /**
456      * Returns the uncompressed image in serialized form, as it would appear in a stream.
457      * 
458      * @deprecated               (<i>for internal use</i>) There is no reason why this should be exposed to users. Use
459      *                               {@link #asImageHDU()} instead. Future release may restrict the visibility to
460      *                               private.
461      * 
462      * @return                   the buffer containing the serialized form of the uncompressed image.
463      * 
464      * @throws     FitsException if the decompression could not be performed.
465      * 
466      * @see                      #asImageHDU()
467      */
468     @Deprecated
469     public Buffer getUncompressedData() throws FitsException {
470         return getData().getUncompressedData(getHeader());
471     }
472 
473     @Override
474     @Deprecated
475     public boolean isHeader() {
476         return super.isHeader() && isHeader(myHeader);
477     }
478 
479     /**
480      * Sets the compression algorithm used for preserving the blank values in the original image even if the compression
481      * is lossy. When compression an integer image, a BLANK header should be defined in its header. You should typically
482      * use one of the enum values defined in {@link Compression}.
483      *
484      * @param  compressionAlgorithm compression algorithm to use for the null pixel mask, see {@link Compression} for
485      *                                  recognized names.
486      *
487      * @return                      itself
488      * 
489      * @see                         Compression
490      * @see                         #setCompressAlgorithm(String)
491      * @see                         #setQuantAlgorithm(String)
492      */
493     public CompressedImageHDU preserveNulls(String compressionAlgorithm) {
494         long nullValue = getHeader().getLongValue(BLANK, Long.MIN_VALUE);
495         getData().preserveNulls(nullValue, compressionAlgorithm);
496         return this;
497     }
498 
499     /**
500      * Sets the compression algorithm to use, by its standard FITS name. You should typically use one of the enum values
501      * defined in {@link Compression}.
502      * 
503      * @param  compressAlgorithm compression algorithm to use, see {@link Compression} for recognized names.
504      * 
505      * @return                   itself
506      * 
507      * @throws FitsException     if no algorithm is available by the specified name
508      * 
509      * @see                      Compression
510      * @see                      #setQuantAlgorithm(String)
511      * @see                      #preserveNulls(String)
512      */
513     public CompressedImageHDU setCompressAlgorithm(String compressAlgorithm) throws FitsException {
514         HeaderCard compressAlgorithmCard = HeaderCard.create(Compression.ZCMPTYPE, compressAlgorithm);
515         getData().setCompressAlgorithm(compressAlgorithmCard);
516         return this;
517     }
518 
519     /**
520      * Sets the quantization algorithm to use, by its standard FITS name. You should typically use one of the enum
521      * values defined in {@link Compression}.
522      * 
523      * @param  quantAlgorithm quantization algorithm to use, see {@link Compression} for recognized names.
524      * 
525      * @return                itself
526      * 
527      * @throws FitsException  if no algorithm is available by the specified name
528      * 
529      * @see                   Compression
530      * @see                   #setCompressAlgorithm(String)
531      * @see                   #preserveNulls(String)
532      */
533     public CompressedImageHDU setQuantAlgorithm(String quantAlgorithm) throws FitsException {
534         if (quantAlgorithm != null && !quantAlgorithm.isEmpty()) {
535             HeaderCard quantAlgorithmCard = HeaderCard.create(Compression.ZQUANTIZ, quantAlgorithm);
536             getData().setQuantAlgorithm(quantAlgorithmCard);
537         } else {
538             getData().setQuantAlgorithm(null);
539         }
540         return this;
541     }
542 }