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 @SuppressWarnings("deprecation")
208 public void setBuffer(Buffer data) {
209 ElementType<Buffer> elementType = ElementType.forClass(getType());
210 dataArray = ArrayFuncs.newInstance(getType(), getDimensions());
211 MultiArrayIterator<?> iterator = new MultiArrayIterator<>(dataArray);
212 Object array = iterator.next();
213 while (array != null) {
214 elementType.getArray(data, array);
215 array = iterator.next();
216 }
217 tiler = new ImageDataTiler(null, 0, dataDescription);
218 }
219
220 @SuppressWarnings({"resource", "deprecation"})
221 @Override
222 public void write(ArrayDataOutput o) throws FitsException {
223 // Don't need to write null data (noted by Jens Knudstrup)
224 if (byteSize == 0) {
225 return;
226 }
227
228 if (o != getRandomAccessInput()) {
229 ensureData();
230 }
231
232 try {
233 o.writeArray(dataArray);
234 } catch (IOException e) {
235 throw new FitsException("IO Error on image write" + e);
236 }
237
238 FitsUtil.pad(o, getTrueSize());
239 }
240
241 @SuppressWarnings("deprecation")
242 @Override
243 protected void fillHeader(Header head) throws FitsException {
244
245 if (dataArray == null) {
246 head.nullImage();
247 return;
248 }
249
250 Standard.context(ImageData.class);
251
252 // We'll assume it's a primary image, until we know better...
253 // Just in case, we don't want an XTENSION key lingering around...
254 head.deleteKey(Standard.XTENSION);
255
256 Cursor<String, HeaderCard> c = head.iterator();
257 c.add(HeaderCard.create(Standard.SIMPLE, true));
258
259 Class<?> base = getType();
260 int[] dims = getDimensions();
261
262 if (ComplexValue.class.isAssignableFrom(base)) {
263 dims = Arrays.copyOf(dims, dims.length + 1);
264 dims[dims.length - 1] = 2;
265 base = ComplexValue.Float.class.isAssignableFrom(base) ? float.class : double.class;
266 }
267
268 c.add(HeaderCard.create(Standard.BITPIX, Bitpix.forPrimitiveType(base).getHeaderValue()));
269
270 c.add(HeaderCard.create(Standard.NAXIS, dims.length));
271 for (int i = 1; i <= dims.length; i++) {
272 c.add(HeaderCard.create(Standard.NAXISn.n(i), dims[dims.length - i]));
273 }
274
275 // Just in case!
276 c.add(HeaderCard.create(Standard.PCOUNT, 0));
277 c.add(HeaderCard.create(Standard.GCOUNT, 1));
278 c.add(HeaderCard.create(Standard.EXTEND, true));
279
280 if (isComplexValued()) {
281 c.add(HeaderCard.create(Standard.CTYPEn.n(dims.length - dataDescription.complexAxis), COMPLEX_TYPE));
282 }
283
284 if (dataDescription.quant != null) {
285 dataDescription.quant.editImageHeader(head);
286 }
287
288 Standard.context(null);
289 }
290
291 @Override
292 protected long getTrueSize() {
293 return byteSize;
294 }
295
296 /**
297 * Returns the image specification based on its description in a FITS header.
298 *
299 * @param h the FITS header that describes this image with the standard keywords for an image HDU.
300 *
301 * @return an object that captures the description contained in the header for internal use.
302 *
303 * @throws FitsException If there was a problem accessing or interpreting the required header values.
304 */
305 protected ArrayDesc parseHeader(Header h) throws FitsException {
306 String ext = h.getStringValue(Standard.XTENSION, Standard.XTENSION_IMAGE);
307
308 if (!ext.equalsIgnoreCase(Standard.XTENSION_IMAGE) && !ext.equalsIgnoreCase(NonStandard.XTENSION_IUEIMAGE)) {
309 throw new FitsException("Not an image header (XTENSION = " + h.getStringValue(Standard.XTENSION) + ")");
310 }
311
312 int gCount = h.getIntValue(Standard.GCOUNT, 1);
313 int pCount = h.getIntValue(Standard.PCOUNT, 0);
314 if (gCount > 1 || pCount != 0) {
315 throw new FitsException("Group data treated as images");
316 }
317
318 Bitpix bitpix = Bitpix.fromHeader(h);
319 Class<?> baseClass = bitpix.getPrimitiveType();
320 int ndim = h.getIntValue(Standard.NAXIS, 0);
321 int[] dims = new int[ndim];
322 // Note that we have to invert the order of the axes
323 // for the FITS file to get the order in the array we
324 // are generating.
325
326 byteSize = ndim > 0 ? 1 : 0;
327 for (int i = 1; i <= ndim; i++) {
328 int cdim = h.getIntValue(Standard.NAXISn.n(i), 0);
329 if (cdim < 0) {
330 throw new FitsException("Invalid array dimension:" + cdim);
331 }
332 byteSize *= cdim;
333 dims[ndim - i] = cdim;
334 }
335 byteSize *= bitpix.byteSize();
336
337 ArrayDesc desc = new ArrayDesc(dims, baseClass);
338
339 if (COMPLEX_TYPE.equals(h.getStringValue(Standard.CTYPEn.n(1))) && dims[ndim - 1] == 2) {
340 desc.complexAxis = ndim - 1;
341 } else if (COMPLEX_TYPE.equals(h.getStringValue(Standard.CTYPEn.n(ndim))) && dims[0] == 2) {
342 desc.complexAxis = 0;
343 }
344
345 desc.quant = Quantizer.fromImageHeader(h);
346 if (desc.quant.isDefault()) {
347 desc.quant = null;
348 }
349
350 return desc;
351 }
352
353 void setTiler(StandardImageTiler tiler) {
354 this.tiler = tiler;
355 }
356
357 /**
358 * (<i>for expert users</i>) Overrides the image size description in the header to the specified Java array
359 * dimensions. Typically users should not call this method, unless they want to define the image dimensions in the
360 * absence of the actual complete image data. For example, to describe the dimensions when using low-level writes of
361 * an image row-by-row, without ever storing the entire image in memory.
362 *
363 * @param header A FITS image header
364 * @param sizes The array dimensions in Java order (fastest varying index last)
365 *
366 * @throws FitsException if the size has negative values, or the header is not that for an image
367 * @throws IllegalArgumentException should not actually happen
368 *
369 * @since 1.18
370 *
371 * @see #fillHeader(Header)
372 */
373 public static void overrideHeaderAxes(Header header, int... sizes) throws FitsException, IllegalArgumentException {
374 String extType = header.getStringValue(Standard.XTENSION, Standard.XTENSION_IMAGE);
375 if (!extType.equals(Standard.XTENSION_IMAGE) && !extType.equals(NonStandard.XTENSION_IUEIMAGE)) {
376 throw new FitsException("Not an image header (XTENSION = " + extType + ")");
377 }
378
379 // Remove prior NAXISn values
380 int n = header.getIntValue(Standard.NAXIS);
381 for (int i = 1; i <= n; i++) {
382 header.deleteKey(Standard.NAXISn.n(i));
383 }
384
385 Cursor<String, HeaderCard> c = header.iterator();
386 c.setKey(Standard.NAXIS.key());
387
388 c.add(HeaderCard.create(Standard.NAXIS, sizes.length));
389
390 for (int i = 1; i <= sizes.length; i++) {
391 int l = sizes[sizes.length - i];
392 if (l < 0) {
393 throw new FitsException("Invalid size[ " + i + "] = " + l);
394 }
395 c.add(HeaderCard.create(Standard.NAXISn.n(i), l));
396 }
397 }
398
399 /**
400 * Creates a new FITS image using the specified primitive numerical Java array containing data.
401 *
402 * @param data A regulatly shaped primitive numerical Java array, which can be
403 * multi-dimensional.
404 *
405 * @return A new FITS image that encapsulates the specified array data.
406 *
407 * @throws IllegalArgumentException if the argument is not a primitive numerical Java array.
408 *
409 * @since 1.19
410 */
411 public static ImageData from(Object data) throws IllegalArgumentException {
412 return new ImageData(data);
413 }
414
415 /**
416 * Checks if a given data object may constitute the kernel for an image. To conform, the data must be a regularly
417 * shaped primitive numerical array of ant dimensions, or <code>null</code>.
418 *
419 * @param data A regularly shaped primitive numerical array of ny dimension, or
420 * <code>null</code>
421 *
422 * @throws IllegalArgumentException If the array is not regularly shaped.
423 * @throws FitsException If the argument is not a primitive numerical array type
424 *
425 * @since 1.19
426 */
427 static void checkCompatible(Object data) throws IllegalArgumentException, FitsException {
428 if (data != null) {
429 Class<?> base = ArrayFuncs.getBaseClass(data);
430 if (ComplexValue.Float.class.isAssignableFrom(base)) {
431 base = float.class;
432 } else if (ComplexValue.class.isAssignableFrom(base)) {
433 base = double.class;
434 }
435 Bitpix.forPrimitiveType(base);
436 ArrayFuncs.checkRegularArray(data, false);
437 }
438 }
439
440 @Override
441 @SuppressWarnings("deprecation")
442 public ImageHDU toHDU() throws FitsException {
443 Header h = new Header();
444 fillHeader(h);
445 return new ImageHDU(h, this);
446 }
447
448 /**
449 * Sets the conversion between decimal and integer data representations. The quantizer for the image is set
450 * automatically if the image was read from a FITS input, and if any of the associated BSCALE, BZERO, or BLANK
451 * keywords were defined in the HDU's header. User may use this methods to set a different quantization or to use no
452 * quantization at all when converting between floating-point and integer representations.
453 *
454 * @param quant the quantizer that converts between floating-point and integer data representations, or <code>
455 * null</code> to not use quantization and instead rely on simple rounding for decimal-ineger conversions..
456 *
457 * @see #getQuantizer()
458 * @see #convertTo(Class)
459 *
460 * @since 1.20
461 */
462 public void setQuantizer(Quantizer quant) {
463 dataDescription.quant = quant;
464 }
465
466 /**
467 * Returns the conversion between decimal and integer data representations.
468 *
469 * @return the quantizer that converts between floating-point and integer data representations, which may be
470 * <code>null</code>
471 *
472 * @see #setQuantizer(Quantizer)
473 * @see #convertTo(Class)
474 *
475 * @since 1.20
476 */
477 public final Quantizer getQuantizer() {
478 return dataDescription.quant;
479 }
480
481 /**
482 * Returns the element type of this image in its current representation.
483 *
484 * @return The element type of this image, such as <code>int.class</code>, <code>double.class</code> or
485 * {@link ComplexValue}<code>.class</code>.
486 *
487 * @see #getDimensions()
488 * @see #isComplexValued()
489 * @see #convertTo(Class)
490 *
491 * @since 1.20
492 */
493 public final Class<?> getType() {
494 return dataDescription.type;
495 }
496
497 /**
498 * Returns the dimensions of this image.
499 *
500 * @return An array containing the sizes along each data dimension, in Java indexing order. The returned array is
501 * not used internally, and therefore modifying it will not damage the integrity of the image data.
502 *
503 * @see #getType()
504 *
505 * @since 1.20
506 */
507 public final int[] getDimensions() {
508 return Arrays.copyOf(dataDescription.dims, dataDescription.dims.length);
509 }
510
511 /**
512 * Checks if the image data is explicitly designated as a complex-valued image. An image may be designated as
513 * complex-valued either because it was created with {@link ComplexValue} type data, or because it was read from a
514 * FITS file in which one image axis of dimension 2 was designated as an axis containing complex-valued components
515 * with the corresponding CTYPEn header keyword set to 'COMPLEX'. The complex-valued deignation checked by this
516 * method is not the same as {@link #getType()}, as it does not necesarily mean that the data itself is currently in
517 * {@link ComplexValue} type representation. Rather it simply means that this data can be represented as
518 * {@link ComplexValue} type, possibly after an appropriate conversion to a {@link ComplexValue} type.
519 *
520 * @return <code>true</code> if the data is complex valued or has been explicitly designated as complex valued.
521 * Otherwise <code>false</code>.
522 *
523 * @see #convertTo(Class)
524 * @see #getType()
525 *
526 * @since 1.20
527 */
528 public final boolean isComplexValued() {
529 return dataDescription.complexAxis >= 0;
530 }
531
532 /**
533 * Converts this image HDU to another image HDU of a different type, possibly using a qunatizer for the
534 * integer-decimal conversion of the data elements. In all other respects, the returned image is identical to the
535 * the original. If th conversion is th indetity, it will return itself and the data may remain in deferred mode.
536 *
537 * @param type The primitive numerical type (e.g. <code>int.class</code> or <code>double.class</code>), or
538 * else a {@link ComplexValue} type in which data should be represented. Complex
539 * representations are normally available for data whose first or last CTYPEn axis was
540 * described as 'COMPLEX' by the FITS header with a dimensionality is 2 corresponfing to a
541 * pair of real and imaginary data elements. Even without the CTYPEn designation, it is
542 * always possible to convert to complex all arrays that have a trailing Java dimension
543 * (NAXIS1 in FITS) equal to 2.
544 *
545 * @return An image HDU containing the same data in the chosen representation by another type. (It may
546 * be the same as this HDU if the type is unchanged from the original).
547 *
548 * @throws FitsException if the data cannot be read from the input.
549 *
550 * @see #isComplexValued()
551 * @see ArrayFuncs#convertArray(Object, Class, Quantizer)
552 *
553 * @since 1.20
554 */
555 public ImageData convertTo(Class<?> type) throws FitsException {
556 if (type.isAssignableFrom(getType())) {
557 return this;
558 }
559
560 ensureData();
561
562 ImageData typed = null;
563
564 boolean toComplex = ComplexValue.class.isAssignableFrom(type) && !ComplexValue.class.isAssignableFrom(getType());
565
566 if (toComplex && dataDescription.complexAxis == 0) {
567 // Special case of converting separate re/im arrays to complex...
568
569 // 1. Convert to intermediate floating-point class as necessary (with quantization if any)
570 Class<?> numType = ComplexValue.Float.class.isAssignableFrom(type) ? float.class : double.class;
571 Object[] t = (Object[]) ArrayFuncs.convertArray(dataArray, numType, getQuantizer());
572 ImageData f = new ImageData(ArrayFuncs.decimalsToComplex(t[0], t[1]));
573 f.dataDescription.quant = getQuantizer();
574
575 // 2. Assemble complex from separate re/im components.
576 return f.convertTo(type);
577 }
578
579 typed = new ImageData(ArrayFuncs.convertArray(dataArray, type, getQuantizer()));
580 typed.dataDescription.quant = getQuantizer();
581 return typed;
582 }
583 }