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 }