1 package nom.tam.fits;
2
3 /*-
4 * #%L
5 * nom.tam FITS library
6 * %%
7 * Copyright (C) 1996 - 2024 nom-tam-fits
8 * %%
9 * This is free and unencumbered software released into the public domain.
10 *
11 * Anyone is free to copy, modify, publish, use, compile, sell, or
12 * distribute this software, either in source code form or as a compiled
13 * binary, for any purpose, commercial or non-commercial, and by any
14 * means.
15 *
16 * In jurisdictions that recognize copyright laws, the author or authors
17 * of this software dedicate any and all copyright interest in the
18 * software to the public domain. We make this dedication for the benefit
19 * of the public at large and to the detriment of our heirs and
20 * successors. We intend this dedication to be an overt act of
21 * relinquishment in perpetuity of all present and future rights to this
22 * software under copyright law.
23 *
24 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
25 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
26 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
27 * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
28 * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
29 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
30 * OTHER DEALINGS IN THE SOFTWARE.
31 * #L%
32 */
33
34 import java.io.IOException;
35 import java.nio.Buffer;
36 import java.util.Arrays;
37
38 import nom.tam.fits.header.Bitpix;
39 import nom.tam.fits.header.NonStandard;
40 import nom.tam.fits.header.Standard;
41 import nom.tam.image.ImageTiler;
42 import nom.tam.image.StandardImageTiler;
43 import nom.tam.util.ArrayDataInput;
44 import nom.tam.util.ArrayDataOutput;
45 import nom.tam.util.ArrayFuncs;
46 import nom.tam.util.ComplexValue;
47 import nom.tam.util.Cursor;
48 import nom.tam.util.FitsEncoder;
49 import nom.tam.util.Quantizer;
50 import nom.tam.util.RandomAccess;
51 import nom.tam.util.array.MultiArrayIterator;
52 import nom.tam.util.type.ElementType;
53
54 /**
55 * <p>
56 * Image data. Essentially these data are a primitive multi-dimensional array, such as a <code>double[]</code>,
57 * <code>float[][]</code>, or <code>short[][][]</code>. Or, as of version 1.20, they may also be {@link ComplexValue}
58 * types also.
59 * </p>
60 * <p>
61 * Starting in version 0.9 of the FITS library, this class allows users to defer the reading of images if the FITS data
62 * is being read from a file. An {@link ImageTiler} object is supplied which can return an arbitrary subset of the image
63 * as a one dimensional array -- suitable for manipulation by standard Java libraries. The image data may not be read
64 * from the input until the user calls a method that requires the actual data (e.g. the {@link #getData()} /
65 * {@link #getKernel()}, {@link #convertTo(Class)} or {@link #write(ArrayDataOutput)} methods).
66 * </p>
67 *
68 * @see ImageHDU
69 */
70 public class ImageData extends Data {
71
72 private static final String COMPLEX_TYPE = "COMPLEX";
73
74 /**
75 * This class describes an array
76 */
77 protected static class ArrayDesc {
78
79 private final Class<?> type;
80 private int[] dims;
81
82 private Quantizer quant;
83
84 private int complexAxis = -1;
85
86 ArrayDesc(int[] dims, Class<?> type) {
87 this.dims = dims;
88 this.type = type;
89
90 if (ComplexValue.class.isAssignableFrom(type)) {
91 complexAxis = dims.length;
92 }
93 }
94 }
95
96 /**
97 * This inner class allows the ImageTiler to see if the user has read in the data.
98 */
99 protected class ImageDataTiler extends StandardImageTiler {
100
101 ImageDataTiler(RandomAccess o, long offset, ArrayDesc d) {
102 super(o, offset, d.dims, d.type);
103 }
104
105 @Override
106 protected Object getMemoryImage() {
107 return dataArray;
108 }
109 }
110
111 // private static final Logger LOG = getLogger(ImageData.class);
112
113 /** The size of the data */
114 private long byteSize;
115
116 /**
117 * The actual array of data. This is normally a multi-dimensional primitive array. It may be null until the
118 * getData() routine is invoked, or it may be filled by during the read call when a non-random access device is
119 * used.
120 */
121 private Object dataArray;
122
123 /** A description of what the data should look like */
124 private ArrayDesc dataDescription;
125
126 /** The image tiler associated with this image. */
127 private StandardImageTiler tiler;
128
129 /**
130 * Create the equivalent of a null data element.
131 */
132 public ImageData() {
133 this(new byte[0]);
134 }
135
136 /**
137 * (<i>for internal use</i>) Create an array from a header description. This is typically how data will be created
138 * when reading FITS data from a file where the header is read first. This creates an empty array.
139 *
140 * @param h header to be used as a template.
141 *
142 * @throws FitsException if there was a problem with the header description.
143 */
144 public ImageData(Header h) throws FitsException {
145 dataDescription = parseHeader(h);
146 }
147
148 /**
149 * Create an ImageData object using the specified object to initialize the data array.
150 *
151 * @param x The initial data array. This should be a primitive array but this is not checked
152 * currently.
153 *
154 * @throws IllegalArgumentException if x is not a suitable primitive array
155 */
156 public ImageData(Object x) throws IllegalArgumentException {
157 try {
158 checkCompatible(x);
159 } catch (FitsException e) {
160 throw new IllegalArgumentException(e.getMessage(), e);
161 }
162
163 dataDescription = new ArrayDesc(ArrayFuncs.getDimensions(x), ArrayFuncs.getBaseClass(x));
164 dataArray = x;
165 byteSize = FitsEncoder.computeSize(x);
166 }
167
168 @Override
169 protected void loadData(ArrayDataInput in) throws IOException, FitsException {
170 if (tiler != null) {
171 dataArray = tiler.getCompleteImage();
172 } else {
173 dataArray = ArrayFuncs.newInstance(getType(), getDimensions());
174 in.readImage(dataArray);
175 }
176 }
177
178 @Override
179 public void read(ArrayDataInput in) throws FitsException {
180 tiler = (in instanceof RandomAccess) ?
181 new ImageDataTiler((RandomAccess) in, ((RandomAccess) in).getFilePointer(), dataDescription) :
182 null;
183 super.read(in);
184 }
185
186 @Override
187 protected Object getCurrentData() {
188 return dataArray;
189 }
190
191 /**
192 * Returns the an image tiler instance that can be used to divide this image into tiles that may be processed
193 * separately (and in parallel). A default tiler, which returns the image in memory, is returned for images not
194 * associated with a random-accessible input.
195 *
196 * @return image tiler for this image instance.
197 */
198 public StandardImageTiler getTiler() {
199 return tiler != null ? tiler : new ImageDataTiler(null, 0, dataDescription);
200 }
201
202 /**
203 * Sets the buffer that may hold a serialized version of the data for this image.
204 *
205 * @param data the buffer that may hold this image's data in serialized form.
206 */
207 public void setBuffer(Buffer data) {
208 ElementType<Buffer> elementType = ElementType.forClass(getType());
209 dataArray = ArrayFuncs.newInstance(getType(), getDimensions());
210 MultiArrayIterator<?> iterator = new MultiArrayIterator<>(dataArray);
211 Object array = iterator.next();
212 while (array != null) {
213 elementType.getArray(data, array);
214 array = iterator.next();
215 }
216 tiler = new ImageDataTiler(null, 0, dataDescription);
217 }
218
219 @SuppressWarnings({"resource", "deprecation"})
220 @Override
221 public void write(ArrayDataOutput o) throws FitsException {
222 // Don't need to write null data (noted by Jens Knudstrup)
223 if (byteSize == 0) {
224 return;
225 }
226
227 if (o != getRandomAccessInput()) {
228 ensureData();
229 }
230
231 try {
232 o.writeArray(dataArray);
233 } catch (IOException e) {
234 throw new FitsException("IO Error on image write" + e);
235 }
236
237 FitsUtil.pad(o, getTrueSize());
238 }
239
240 @SuppressWarnings("deprecation")
241 @Override
242 protected void fillHeader(Header head) throws FitsException {
243
244 if (dataArray == null) {
245 head.nullImage();
246 return;
247 }
248
249 Standard.context(ImageData.class);
250
251 // We'll assume it's a primary image, until we know better...
252 // Just in case, we don't want an XTENSION key lingering around...
253 head.deleteKey(Standard.XTENSION);
254
255 Cursor<String, HeaderCard> c = head.iterator();
256 c.add(HeaderCard.create(Standard.SIMPLE, true));
257
258 Class<?> base = getType();
259 int[] dims = getDimensions();
260
261 if (ComplexValue.class.isAssignableFrom(base)) {
262 dims = Arrays.copyOf(dims, dims.length + 1);
263 dims[dims.length - 1] = 2;
264 base = ComplexValue.Float.class.isAssignableFrom(base) ? float.class : double.class;
265 }
266
267 c.add(HeaderCard.create(Standard.BITPIX, Bitpix.forPrimitiveType(base).getHeaderValue()));
268
269 c.add(HeaderCard.create(Standard.NAXIS, dims.length));
270 for (int i = 1; i <= dims.length; i++) {
271 c.add(HeaderCard.create(Standard.NAXISn.n(i), dims[dims.length - i]));
272 }
273
274 // Just in case!
275 c.add(HeaderCard.create(Standard.PCOUNT, 0));
276 c.add(HeaderCard.create(Standard.GCOUNT, 1));
277 c.add(HeaderCard.create(Standard.EXTEND, true));
278
279 if (isComplexValued()) {
280 c.add(HeaderCard.create(Standard.CTYPEn.n(dims.length - dataDescription.complexAxis), COMPLEX_TYPE));
281 }
282
283 if (dataDescription.quant != null) {
284 dataDescription.quant.editImageHeader(head);
285 }
286
287 Standard.context(null);
288 }
289
290 @Override
291 protected long getTrueSize() {
292 return byteSize;
293 }
294
295 /**
296 * Returns the image specification based on its description in a FITS header.
297 *
298 * @param h the FITS header that describes this image with the standard keywords for an image HDU.
299 *
300 * @return an object that captures the description contained in the header for internal use.
301 *
302 * @throws FitsException If there was a problem accessing or interpreting the required header values.
303 */
304 protected ArrayDesc parseHeader(Header h) throws FitsException {
305 String ext = h.getStringValue(Standard.XTENSION, Standard.XTENSION_IMAGE);
306
307 if (!ext.equalsIgnoreCase(Standard.XTENSION_IMAGE) && !ext.equalsIgnoreCase(NonStandard.XTENSION_IUEIMAGE)) {
308 throw new FitsException("Not an image header (XTENSION = " + h.getStringValue(Standard.XTENSION) + ")");
309 }
310
311 int gCount = h.getIntValue(Standard.GCOUNT, 1);
312 int pCount = h.getIntValue(Standard.PCOUNT, 0);
313 if (gCount > 1 || pCount != 0) {
314 throw new FitsException("Group data treated as images");
315 }
316
317 Bitpix bitpix = Bitpix.fromHeader(h);
318 Class<?> baseClass = bitpix.getPrimitiveType();
319 int ndim = h.getIntValue(Standard.NAXIS, 0);
320 int[] dims = new int[ndim];
321 // Note that we have to invert the order of the axes
322 // for the FITS file to get the order in the array we
323 // are generating.
324
325 byteSize = ndim > 0 ? 1 : 0;
326 for (int i = 1; i <= ndim; i++) {
327 int cdim = h.getIntValue(Standard.NAXISn.n(i), 0);
328 if (cdim < 0) {
329 throw new FitsException("Invalid array dimension:" + cdim);
330 }
331 byteSize *= cdim;
332 dims[ndim - i] = cdim;
333 }
334 byteSize *= bitpix.byteSize();
335
336 ArrayDesc desc = new ArrayDesc(dims, baseClass);
337
338 if (COMPLEX_TYPE.equals(h.getStringValue(Standard.CTYPEn.n(1))) && dims[ndim - 1] == 2) {
339 desc.complexAxis = ndim - 1;
340 } else if (COMPLEX_TYPE.equals(h.getStringValue(Standard.CTYPEn.n(ndim))) && dims[0] == 2) {
341 desc.complexAxis = 0;
342 }
343
344 desc.quant = Quantizer.fromImageHeader(h);
345 if (desc.quant.isDefault()) {
346 desc.quant = null;
347 }
348
349 return desc;
350 }
351
352 void setTiler(StandardImageTiler tiler) {
353 this.tiler = tiler;
354 }
355
356 /**
357 * (<i>for expert users</i>) Overrides the image size description in the header to the specified Java array
358 * dimensions. Typically users should not call this method, unless they want to define the image dimensions in the
359 * absence of the actual complete image data. For example, to describe the dimensions when using low-level writes of
360 * an image row-by-row, without ever storing the entire image in memory.
361 *
362 * @param header A FITS image header
363 * @param sizes The array dimensions in Java order (fastest varying index last)
364 *
365 * @throws FitsException if the size has negative values, or the header is not that for an image
366 * @throws IllegalArgumentException should not actually happen
367 *
368 * @since 1.18
369 *
370 * @see #fillHeader(Header)
371 */
372 public static void overrideHeaderAxes(Header header, int... sizes) throws FitsException, IllegalArgumentException {
373 String extType = header.getStringValue(Standard.XTENSION, Standard.XTENSION_IMAGE);
374 if (!extType.equals(Standard.XTENSION_IMAGE) && !extType.equals(NonStandard.XTENSION_IUEIMAGE)) {
375 throw new FitsException("Not an image header (XTENSION = " + extType + ")");
376 }
377
378 // Remove prior NAXISn values
379 int n = header.getIntValue(Standard.NAXIS);
380 for (int i = 1; i <= n; i++) {
381 header.deleteKey(Standard.NAXISn.n(i));
382 }
383
384 Cursor<String, HeaderCard> c = header.iterator();
385 c.setKey(Standard.NAXIS.key());
386
387 c.add(HeaderCard.create(Standard.NAXIS, sizes.length));
388
389 for (int i = 1; i <= sizes.length; i++) {
390 int l = sizes[sizes.length - i];
391 if (l < 0) {
392 throw new FitsException("Invalid size[ " + i + "] = " + l);
393 }
394 c.add(HeaderCard.create(Standard.NAXISn.n(i), l));
395 }
396 }
397
398 /**
399 * Creates a new FITS image using the specified primitive numerical Java array containing data.
400 *
401 * @param data A regulatly shaped primitive numerical Java array, which can be
402 * multi-dimensional.
403 *
404 * @return A new FITS image that encapsulates the specified array data.
405 *
406 * @throws IllegalArgumentException if the argument is not a primitive numerical Java array.
407 *
408 * @since 1.19
409 */
410 public static ImageData from(Object data) throws IllegalArgumentException {
411 return new ImageData(data);
412 }
413
414 /**
415 * Checks if a given data object may constitute the kernel for an image. To conform, the data must be a regularly
416 * shaped primitive numerical array of ant dimensions, or <code>null</code>.
417 *
418 * @param data A regularly shaped primitive numerical array of ny dimension, or
419 * <code>null</code>
420 *
421 * @throws IllegalArgumentException If the array is not regularly shaped.
422 * @throws FitsException If the argument is not a primitive numerical array type
423 *
424 * @since 1.19
425 */
426 static void checkCompatible(Object data) throws IllegalArgumentException, FitsException {
427 if (data != null) {
428 Class<?> base = ArrayFuncs.getBaseClass(data);
429 if (ComplexValue.Float.class.isAssignableFrom(base)) {
430 base = float.class;
431 } else if (ComplexValue.class.isAssignableFrom(base)) {
432 base = double.class;
433 }
434 Bitpix.forPrimitiveType(base);
435 ArrayFuncs.checkRegularArray(data, false);
436 }
437 }
438
439 @Override
440 @SuppressWarnings("deprecation")
441 public ImageHDU toHDU() throws FitsException {
442 Header h = new Header();
443 fillHeader(h);
444 return new ImageHDU(h, this);
445 }
446
447 /**
448 * Sets the conversion between decimal and integer data representations. The quantizer for the image is set
449 * automatically if the image was read from a FITS input, and if any of the associated BSCALE, BZERO, or BLANK
450 * keywords were defined in the HDU's header. User may use this methods to set a different quantization or to use no
451 * quantization at all when converting between floating-point and integer representations.
452 *
453 * @param quant the quantizer that converts between floating-point and integer data representations, or <code>
454 * null</code> to not use quantization and instead rely on simple rounding for decimal-ineger conversions..
455 *
456 * @see #getQuantizer()
457 * @see #convertTo(Class)
458 *
459 * @since 1.20
460 */
461 public void setQuantizer(Quantizer quant) {
462 dataDescription.quant = quant;
463 }
464
465 /**
466 * Returns the conversion between decimal and integer data representations.
467 *
468 * @return the quantizer that converts between floating-point and integer data representations, which may be
469 * <code>null</code>
470 *
471 * @see #setQuantizer(Quantizer)
472 * @see #convertTo(Class)
473 *
474 * @since 1.20
475 */
476 public final Quantizer getQuantizer() {
477 return dataDescription.quant;
478 }
479
480 /**
481 * Returns the element type of this image in its current representation.
482 *
483 * @return The element type of this image, such as <code>int.class</code>, <code>double.class</code> or
484 * {@link ComplexValue}<code>.class</code>.
485 *
486 * @see #getDimensions()
487 * @see #isComplexValued()
488 * @see #convertTo(Class)
489 *
490 * @since 1.20
491 */
492 public final Class<?> getType() {
493 return dataDescription.type;
494 }
495
496 /**
497 * Returns the dimensions of this image.
498 *
499 * @return An array containing the sizes along each data dimension, in Java indexing order. The returned array is
500 * not used internally, and therefore modifying it will not damage the integrity of the image data.
501 *
502 * @see #getType()
503 *
504 * @since 1.20
505 */
506 public final int[] getDimensions() {
507 return Arrays.copyOf(dataDescription.dims, dataDescription.dims.length);
508 }
509
510 /**
511 * Checks if the image data is explicitly designated as a complex-valued image. An image may be designated as
512 * complex-valued either because it was created with {@link ComplexValue} type data, or because it was read from a
513 * FITS file in which one image axis of dimension 2 was designated as an axis containing complex-valued components
514 * with the corresponding CTYPEn header keyword set to 'COMPLEX'. The complex-valued deignation checked by this
515 * method is not the same as {@link #getType()}, as it does not necesarily mean that the data itself is currently in
516 * {@link ComplexValue} type representation. Rather it simply means that this data can be represented as
517 * {@link ComplexValue} type, possibly after an appropriate conversion to a {@link ComplexValue} type.
518 *
519 * @return <code>true</code> if the data is complex valued or has been explicitly designated as complex valued.
520 * Otherwise <code>false</code>.
521 *
522 * @see #convertTo(Class)
523 * @see #getType()
524 *
525 * @since 1.20
526 */
527 public final boolean isComplexValued() {
528 return dataDescription.complexAxis >= 0;
529 }
530
531 /**
532 * Converts this image HDU to another image HDU of a different type, possibly using a qunatizer for the
533 * integer-decimal conversion of the data elements. In all other respects, the returned image is identical to the
534 * the original. If th conversion is th indetity, it will return itself and the data may remain in deferred mode.
535 *
536 * @param type The primitive numerical type (e.g. <code>int.class</code> or <code>double.class</code>), or
537 * else a {@link ComplexValue} type in which data should be represented. Complex
538 * representations are normally available for data whose first or last CTYPEn axis was
539 * described as 'COMPLEX' by the FITS header with a dimensionality is 2 corresponfing to a
540 * pair of real and imaginary data elements. Even without the CTYPEn designation, it is
541 * always possible to convert to complex all arrays that have a trailing Java dimension
542 * (NAXIS1 in FITS) equal to 2.
543 *
544 * @return An image HDU containing the same data in the chosen representation by another type. (It may
545 * be the same as this HDU if the type is unchanged from the original).
546 *
547 * @throws FitsException if the data cannot be read from the input.
548 *
549 * @see #isComplexValued()
550 * @see ArrayFuncs#convertArray(Object, Class, Quantizer)
551 *
552 * @since 1.20
553 */
554 public ImageData convertTo(Class<?> type) throws FitsException {
555 if (type.isAssignableFrom(getType())) {
556 return this;
557 }
558
559 ensureData();
560
561 ImageData typed = null;
562
563 boolean toComplex = ComplexValue.class.isAssignableFrom(type) && !ComplexValue.class.isAssignableFrom(getType());
564
565 if (toComplex && dataDescription.complexAxis == 0) {
566 // Special case of converting separate re/im arrays to complex...
567
568 // 1. Convert to intermediate floating-point class as necessary (with quantization if any)
569 Class<?> numType = ComplexValue.Float.class.isAssignableFrom(type) ? float.class : double.class;
570 Object[] t = (Object[]) ArrayFuncs.convertArray(dataArray, numType, getQuantizer());
571 ImageData f = new ImageData(ArrayFuncs.decimalsToComplex(t[0], t[1]));
572 f.dataDescription.quant = getQuantizer();
573
574 // 2. Assemble complex from separate re/im components.
575 return f.convertTo(type);
576 }
577
578 typed = new ImageData(ArrayFuncs.convertArray(dataArray, type, getQuantizer()));
579 typed.dataDescription.quant = getQuantizer();
580 return typed;
581 }
582 }