Image object
Image objects contain a 2D data array of flux values, and a WCS
object that describes the spatial coordinates of the
image. Optionally, an array of variances can also be provided to give the
statistical uncertainties of the fluxes. These can be used for weighting the
flux values and for computing the uncertainties of least-squares fits and other
calculations. Finally a mask array is provided for indicating bad pixels.
The fluxes and their variances are stored in numpy masked arrays, so virtually all numpy and scipy functions can be applied to them.
Preliminary imports:
In [1]: import numpy as np
In [2]: import matplotlib.pyplot as plt
In [3]: import astropy.units as u
In [4]: from mpdaf.obj import Image, WCS
Image creation
There are two common ways to obtain an Image
object:
An image can be created from a user-provided 2D array of the pixel values, or from both a 2D array of pixel values, and a corresponding 2D array of variances. These can be simple numpy arrays, or they can be numpy masked arrays in which bad pixels have been masked. For example:
In [5]: wcs1 = WCS(crval=0, cdelt=0.2)
# numpy data array
In [6]: MyData = np.ones((300,300))
# image filled with MyData
In [7]: ima = Image(data=MyData, wcs=wcs1) # image 300x300 filled with data
In [8]: ima.info()
[INFO] 300 x 300 Image (no name)
[INFO] .data(300 x 300) (no unit), no noise
[INFO] spatial coord (pix): min:(0.0,0.0) max:(59.8,59.8) step:(0.2,0.2) rot:-0.0 deg
Alternatively, an image can be read from a FITS file. In this case the flux and variance values are read from specific extensions:
# data and variance arrays read from the file (extension DATA and STAT)
In [9]: ima = Image('obj/IMAGE-HDFS-1.34.fits')
In [10]: ima.info()
[INFO] 331 x 326 Image (obj/IMAGE-HDFS-1.34.fits)
[INFO] .data(331 x 326) (1e-20), .var(331 x 326)
[INFO] center:(-60:33:48.93213365,22:32:55.52670061) size:(66.200",65.200") step:(0.200",0.200") rot:-0.0 deg frame:ICRS
By default, if a FITS file has more than one extension, then it is expected to have a ‘DATA’ extension that contains the pixel data, and possibly a ‘STAT’ extension that contains the corresponding variances. If the file doesn’t contain extensions of these names, the “ext=” keyword can be used to indicate the appropriate extension or extensions.
The world-coordinate grid of an Image is described by a WCS
object. When an image is read from a FITS file, this is automatically generated
based on FITS header keywords. Alternatively, when an image is extracted from a
cube or another image, the WCS object is derived from the WCS object of the
original object.
As shown in the above example, information about an image can be printed using
the info
method.
Image objects provide a plot
method that is based on
matplotlib.pyplot.plot and
accepts all matplotlib arguments. The colors used to plot an image are
distributed between a minimum and a maximum pixel value. By default these are
the minimum and maximum pixel values in the image, but different thresholds can
be specified via the vmin and vmax arguments, as shown below.
In [11]: plt.figure()
Out[11]: <Figure size 640x480 with 0 Axes>
In [12]: ima.plot(vmin=0, vmax=10, colorbar='v')
Out[12]: <matplotlib.image.AxesImage at 0x7f8ee21d7a60>
The plot
method has many options to customize the plot, for
instance:
In [13]: plt.figure()
Out[13]: <Figure size 640x480 with 0 Axes>
In [14]: ima.plot(zscale=True, colorbar='v', use_wcs=True, scale='sqrt')
Out[14]: <matplotlib.image.AxesImage at 0x7f8ee22ea9d0>
The indexing of the image arrays follows the Python conventions for indexing a 2D array. For an MPDAF image im, the pixel in the lower-left corner is referenced as im[0,0] and the pixel im[p,q] refers to the horizontal pixel q and the vertical pixel p, as follows:
In total, this image im contains nq pixels in the horizontal direction and np pixels in the vertical direction (see Spectrum, Image and Cube format for more information).
Image Geometrical manipulation
In the following example, the sky is rotated within the image by 40 degrees anticlockwise, then re-sampled to change its pixel size from 0.2 arcseconds to 0.4 arcseconds.
In [15]: import astropy.units as u
In [16]: fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(12, 4), tight_layout=True)
In [17]: ima.plot(ax=ax1, colorbar='v')
Out[17]: <matplotlib.image.AxesImage at 0x7f8ee524efd0>
In [18]: ima2 = ima.rotate(40) #this rotation uses an interpolation of the pixels
In [19]: ima2.plot(ax=ax2, colorbar='v')
Out[19]: <matplotlib.image.AxesImage at 0x7f8ee236bf40>
In [20]: ima3 = ima2.resample(newdim=(150,150), newstart=None, newstep=(0.4,0.4), unit_step=u.arcsec, flux=True)
In [21]: ima3.plot(ax=ax3, colorbar='v')
Out[21]: <matplotlib.image.AxesImage at 0x7f8ee256a1c0>
The rotate
method interpolates the image onto a
rotated coordinate grid.
The resample
method also interpolates the image
onto a new grid, but before doing this it applies a decimation filter to remove
high spatial frequencies that would otherwise be undersampled by the pixel
spacing.
The newstart=None
argument indicates that the sky position that appears at
the center of pixel [0,0] should also be at the center of pixel [0,0] of the
resampled image. This argument can alternatively be used to move the sky within
the image.
The resample
method is a simplified interface to
the regrid
function, which provides more options.
The following example shows how images from different telescopes can be resampled onto the same coordinate grid, then how the coordinate offsets of the pixels can be adjusted to account for relative pointing errors:
# Read a small part of an HST image
In [22]: imahst = Image('obj/HST-HDFS.fits')
# Resample the HST image onto the coordinate grid of the MUSE image
In [23]: ima2hst = imahst.align_with_image(ima)
In [24]: plt.figure()
Out[24]: <Figure size 640x480 with 0 Axes>
In [25]: ima.plot(colorbar='v', vmin=0.0, vmax=20.0, title='MUSE image')
Out[25]: <matplotlib.image.AxesImage at 0x7f8ee2570d90>
In [26]: plt.figure()
Out[26]: <Figure size 640x480 with 0 Axes>
In [27]: ima2hst.plot(colorbar='v', title='Part of the HST image')
Out[27]: <matplotlib.image.AxesImage at 0x7f8ee1d0ebe0>
In the example shown above, the align_with_image
method resamples an HST image onto the same
coordinate grid as a MUSE image. The resampled HST image then has the same
number of pixels, and the same pixel coordinates as the MUSE image.
The adjust_coordinates
method then uses
an enhanced form of cross-correlation to estimate and correct for any relative
pointing errors between the two images. Note that, to see the estimated
correction without applying it, the estimate_coordinate_offset
method could have been used.
In the following example, the aligned HST and MUSE images are combined to produce a higher S/N image. Note the use of the addition operator to add the two images:
In [28]: ima2hst[ima2hst.mask] = 0
In [29]: ima2hst.unmask()
In [30]: imacomb = ima + ima2hst
In [31]: plt.figure()
Out[31]: <Figure size 640x480 with 0 Axes>
In [32]: ima[200:, 30:150].plot(colorbar='v', title='original image')
Out[32]: <matplotlib.image.AxesImage at 0x7f8ee2191df0>
In [33]: plt.figure()
Out[33]: <Figure size 640x480 with 0 Axes>
In [34]: imacomb[200:, 30:150].plot(colorbar='v', title='combined image')
Out[34]: <matplotlib.image.AxesImage at 0x7f8ee258ac40>
The subimage
method can be used to extract a square
or rectangular sub-image of given world-coordinate dimensions from an image. In
the following example it is used used to extract a 20 arcsecond square sub-image
from the center of the HST image.
In [35]: dec, ra = imahst.wcs.pix2sky(np.array(imahst.shape)/2)[0]
In [36]: subima = ima.subimage(center=(dec,ra), size=20.0)
In [37]: plt.figure()
Out[37]: <Figure size 640x480 with 0 Axes>
In [38]: subima.plot()
Out[38]: <matplotlib.image.AxesImage at 0x7f8ee1dc1ca0>
The inside
method lets the user test whether a given
coordinate is inside an image. In the following example, dec and ra are the
coordinates of the center of the image that were calculated in the preceding
example.
In [39]: subima.inside([dec, ra])
Out[39]: True
In [40]: subima.inside(ima.get_start())
Out[40]: False
Object analysis: image segmentation, peak measurement, profile fitting
The following demonstration will show some examples of extracting and analyzing
images of individual objects within an image. The first example segments the
image into several cutout images using the (segment
)
method:
In [41]: im = Image('obj/a370II.fits')
In [42]: seg = im.segment(minsize=10, background=2100)
The segment
method returns a list of images of the
detected sources. In the following example, we extract one of these for further
analysis:
In [43]: source = seg[8]
In [44]: plt.figure()
Out[44]: <Figure size 640x480 with 0 Axes>
In [45]: source.plot(colorbar='v')
Out[45]: <matplotlib.image.AxesImage at 0x7f8ee02f6850>
For a first approximation, some simple analysis methods are applied:
background
to estimate the background level,
peak
to locate the peak of the source,
fwhm_gauss
to estimate the FWHM of the source.
# background value and its standard deviation
In [46]: source.background()
Out[46]: (2052.505415162455, 68.01699304230651)
# peak position and intensity
In [47]: source.peak()
Out[47]:
{'x': 40.02632730206195,
'y': -1.6018629119499264,
'p': 15.880560309635259,
'q': 10.834926649327759,
'data': 3201}
# fwhm in arcsec
In [48]: source.fwhm_gauss()
Out[48]: array([0.67815839, 0.63540917])
Then, for greater accuracy we fit a 2D Gaussian to the source, and plot the
isocontours (gauss_fit
):
In [49]: gfit = source.gauss_fit(plot=False)
[INFO] Gaussian center = (-1.60183,40.0263) (error:(2.40391e-06,1.46758e-06))
[INFO] Gaussian integrated flux = 51445.8 (error:687.259)
[INFO] Gaussian peak value = 940.004 (error:-8.98435)
[INFO] Gaussian fwhm = (1.96076,1.04189) (error:(0.0224652,0.0119394))
[INFO] Rotation in degree: 162.395 (error:1.41177)
[INFO] Gaussian continuum = 2022.39 (error:1.86548)
In [50]: gfit = source.gauss_fit(maxiter=150, plot=True)
[INFO] Gaussian center = (-1.60183,40.0263) (error:(2.40391e-06,1.46758e-06))
[INFO] Gaussian integrated flux = 51445.8 (error:687.259)
[INFO] Gaussian peak value = 940.004 (error:-8.98435)
[INFO] Gaussian fwhm = (1.96076,1.04189) (error:(0.0224652,0.0119394))
[INFO] Rotation in degree: 162.395 (error:1.41177)
[INFO] Gaussian continuum = 2022.39 (error:1.86548)
In general, Moffat profiles provide a better representation of the point-spread
functions of ground-based telescope observations, so next we perform a 2D MOFFAT
fit to the same source (moffat_fit
):
In [51]: mfit = source.moffat_fit(plot=True)
[INFO] center = (-1.60183,40.0263) (error:(1.46452e-06,8.9727e-07))
[INFO] integrated flux = 253370 (error:0.000110584)
[INFO] peak value = 1217.37 (error:15.1703)
[INFO] fwhm = (1.56078,0.832519) (error:(0.0196986,0.986155))
[INFO] n = 1.13844 (error:0.0514963)
[INFO] rotation in degree: 162.373 (error:0.453644)
[INFO] continuum = 1964.35 (error:4.31709)
We then subtract the fitted Gaussian and Moffat models of from the original
source to see the residuals. Note the use of gauss_image
and
moffat_image
to create MPDAF images of the 2D Gaussian and Moffat
functions:
In [52]: from mpdaf.obj import gauss_image, moffat_image
In [53]: gfitim = gauss_image(wcs=source.wcs, gauss=gfit)
In [54]: mfitim = moffat_image(wcs=source.wcs, moffat=mfit)
In [55]: gresiduals = source-gfitim
In [56]: mresiduals = source-mfitim
In [57]: plt.figure()
Out[57]: <Figure size 640x480 with 0 Axes>
In [58]: mresiduals.plot(colorbar='v', title='Residuals from 2D Moffat profile fitting')
Out[58]: <matplotlib.image.AxesImage at 0x7f8ee011d2e0>
In [59]: plt.figure()
Out[59]: <Figure size 640x480 with 0 Axes>
In [60]: gresiduals.plot(colorbar='v', title='Residuals from 2D Gaussian profile fitting')
Out[60]: <matplotlib.image.AxesImage at 0x7f8ee0061130>
Finally we estimate the energy received from the source:
The
ee
method computes ensquared or encircled energy, which is the sum of the flux within a given radius of the center of the source.The
ee_size
method computes the size of a square centered on the source that contains a given fraction of the total flux of the source,The
eer_curve
method returns the normalized enclosed energy as a function radius.
# Obtain the encircled flux within a radius of one FWHM of the source
In [61]: source.ee(radius=source.fwhm_gauss(), cont=source.background()[0])
Out[61]: 32765.64259927794
# Get the enclosed energy normalized by the total energy as a function of radius (ERR)
In [62]: radius, ee = source.eer_curve(cont=source.background()[0])
# The size of the square centered on the source that contains 90% of the energy (in arcsec)
In [63]: source.ee_size()
Out[63]: array([4.26351015, 4.26816953])
In [64]: plt.figure()
Out[64]: <Figure size 640x480 with 0 Axes>
In [65]: plt.plot(radius, ee)
Out[65]: [<matplotlib.lines.Line2D at 0x7f8edff4ab20>]
In [66]: plt.xlabel('radius')
Out[66]: Text(0.5, 0, 'radius')
In [67]: plt.ylabel('ERR')
Out[67]: Text(0, 0.5, 'ERR')