View Javadoc
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 }