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 }