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