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     @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 }