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