In this exercise we will create a composite (RGB) optical image of a galaxy cluster (from the XXL survey) with the diffuse X-ray emission overplotted as contours. We will see, how to:
Keys: WCS
, SkyCoord
, Cutout2D
, data visualization
(images normalization, countour plots, marker, ...), GRB combination
, reprojection
, Multidimensional image
from astropy.io import fits
hdulist = fits.open('data/CFHTLS_D-25_g_022559-042940_T0004.fits')
image=hdulist[0]
#image is very large
image.data.shape
(19354, 19354)
astropy.wcs
contains utilities for managing World Coordinate System transformations in FITS files. These transformations map the pixel locations in an image to their real-world units, such as their position on the sky sphere. These transformations can work both forward (from pixel to sky) and backward (from sky to pixel).
It performs three separate classes of WCS transformations:
Core WCS, as defined in the FITS WCS standard, based on Mark Calabretta’s wcslib
Simple Imaging Polynomial (SIP
) convention.
table lookup distortions as defined in the FITS WCS distortion paper
.
Each of these transformations can be used independently or together.
#Create the WCS object
from astropy.wcs import WCS
wcs = WCS(image)
wcs
WARNING: FITSFixedWarning: RADECSYS= 'FK5 ' / Astrometric system the RADECSYS keyword is deprecated, use RADESYSa. [astropy.wcs.wcs]
WCS Keywords Number of WCS axes: 2 CTYPE : 'RA---TAN' 'DEC--TAN' CRVAL : 36.49583333 -4.494444444 CRPIX : 9677.5 9677.5 CD1_1 CD1_2 : -5.166666789e-05 0.0 CD2_1 CD2_2 : 0.0 5.166666789e-05 NAXIS : 19354 19354
image
<astropy.io.fits.hdu.image.PrimaryHDU at 0x7f6da8212c18>
Uses WCS
to convert pixels into sky coordinates
NOTE: 0 is the origin parameter (Numpy-like)
coords = wcs.wcs_pix2world(((5090,5115),(5091,5115),(5090,5116)),0, ra_dec_order=True)
coords
array([[36.73360896, -4.73008007], [36.73355712, -4.73008009], [36.73360894, -4.73002841]])
wcs.wcs_world2pix(coords,0)
array([[5090., 5115.], [5091., 5115.], [5090., 5116.]])
The Cutout2D
class can be used to create a postage stamp of an image (2D array). If an optional WCS object is input to Cutout2D, then the Cutout2D object will contain an updated WCS corresponding to the cutout array.
In this example, we will work with the WCS. The same arrangement can be performed in using pixels.
There are three modes for creating cutout arrays:
trim
(default): partial overlap of the cutout array and the input data array is sufficient. The result can be smaller than the requested sizepartial
: partial overlap of the cutout array and the input data array is sufficient. The cutout will never be trimmed, it will be filled with fill_value (default numpy.nan
)strict
: will raise an exception if the cutout is not fully contained#Define the center
from astropy.coordinates import SkyCoord
center = SkyCoord('2h25m31s -4d14m27.78s')
#Create the thumbnail
from astropy import units as u
from astropy.nddata import Cutout2D
thumb=Cutout2D(image.data, center, (4*u.arcmin, 3*u.arcmin),wcs=wcs)
Cutout2D?
The coordinates package
provides classes for representing a variety of celestial/spatial coordinates and their velocity components, as well as tools for converting between common coordinate systems in a uniform way.
c = SkyCoord('2:25:31 -4:14:27.78', unit=(u.hourangle, u.deg), frame='icrs')
center, c
(<SkyCoord (ICRS): (ra, dec) in deg (36.37916667, -4.24105)>, <SkyCoord (ICRS): (ra, dec) in deg (36.37916667, -4.24105)>)
c.galactic, c.fk4, c.fk5
c.ra, c.dec
c.ra.hour, c.ra.arcmin, c.ra.rad, c.ra.deg
SkyCoord has the additional distance
attribute
c2 = SkyCoord('2:25:31 -4:14:27.78', unit=(u.hourangle, u.deg), distance=574*u.kpc)
c3 = SkyCoord('1:02:31 -2:10:27.78', unit=(u.hourangle, u.deg), distance=123*u.kpc)
c3
<SkyCoord (ICRS): (ra, dec, distance) in (deg, deg, kpc) (15.62916667, -2.17438333, 123.)>
c2.separation(c3), c2.separation_3d(c3)
(<Angle 20.81882624 deg>, <Distance 461.10775111 kpc>)
Sesame
)SkyCoord.from_name('NGC 1142')
<SkyCoord (ICRS): (ra, dec) in deg (43.800955, -0.183547)>
#Create a new fits image
data = thumb.data
header = thumb.wcs.to_header()
primary = fits.PrimaryHDU(data=data, header=header)
primary.writeto("tmp/g.fits", overwrite=True)
thumb.wcs.to_header()
WCSAXES = 2 / Number of coordinate axes CRPIX1 = -1767.5 / Pixel coordinate of reference point CRPIX2 = -4258.5 / Pixel coordinate of reference point PC1_1 = -5.166666789E-05 / Coordinate transformation matrix element PC2_2 = 5.166666789E-05 / Coordinate transformation matrix element CDELT1 = 1.0 / [deg] Coordinate increment at reference point CDELT2 = 1.0 / [deg] Coordinate increment at reference point CUNIT1 = 'deg' / Units of coordinate increment and value CUNIT2 = 'deg' / Units of coordinate increment and value CTYPE1 = 'RA---TAN' / Right ascension, gnomonic projection CTYPE2 = 'DEC--TAN' / Declination, gnomonic projection CRVAL1 = 36.49583333 / [deg] Coordinate value at reference point CRVAL2 = -4.494444444 / [deg] Coordinate value at reference point LONPOLE = 180.0 / [deg] Native longitude of celestial pole LATPOLE = -4.494444444 / [deg] Native latitude of celestial pole RADESYS = 'FK5' / Equatorial coordinate system EQUINOX = 2000.0 / [yr] Equinox of equatorial coordinates
# Preserve the orignal header and update the WCS
original_header = image.header
original_header.update(thumb.wcs.to_header())
primary = fits.PrimaryHDU(data=data, header=original_header)
primary.writeto("tmp/g.fits", overwrite=True)
Now we extract one subimage for each filter and save it
#Loop over filters
for _filter in ('g', 'r', 'i'):
#Open image creating the the proper file name
hdulist = fits.open('data/CFHTLS_D-25_%s_022559-042940_T0004.fits' % _filter)
#Get the image a wcs
image=hdulist[0]
wcs = WCS(image)
#Extract subimage
thumb=Cutout2D(image.data, center, (4*u.arcmin, 3*u.arcmin),wcs=wcs)
#Save on disk
primary = fits.PrimaryHDU(data=thumb.data, header=thumb.wcs.to_header())
primary.writeto(f"tmp/{_filter}.fits", overwrite=True) #Different str syntax
hdulist.close()
WARNING: FITSFixedWarning: RADECSYS= 'FK5 ' / Astrometric system the RADECSYS keyword is deprecated, use RADESYSa. [astropy.wcs.wcs]
label="alpha"
value=2.12345E-7
"%s=%.3e" % (label, 5*value) # Old style
f"{label}={5*value}"
f"{label}={5*value:.3e}"
astropy.visualization
provides functionality that can be helpful when visualizing data. This includes
#Open new data and wcs
with fits.open("tmp/g.fits") as hdulist:
g = hdulist[0].data.copy()
wcs = WCS(hdulist[0]) #All 3 images have the same WCS in this example
with fits.open("tmp/r.fits") as hdulist:
r = hdulist[0].data.copy()
with fits.open("tmp/i.fits") as hdulist:
i = hdulist[0].data.copy()
#%matplotlib qt
%matplotlib notebook
from matplotlib import pyplot as plt
fig=plt.figure("Display images")
ax11=fig.add_subplot(221, projection=wcs)
ax11.imshow(g)
ax11.grid(color='white', ls='--', alpha=0.5)
ax11.set_xlabel('RA')
ax11.set_ylabel('Declination')
ax21=fig.add_subplot(223)
ax21.set_yscale('log')
_ = ax21.hist(g.ravel(), bins=100)
A simple way to "see" an image is to exclude the extreme values (e.g. using a sigma clipping)
from astropy.stats.sigma_clipping import sigma_clip
clipped_g = sigma_clip(g, sigma=5, maxiters=3)
ax12=fig.add_subplot(222, projection=wcs)
ax12.imshow(g, vmin=clipped_g.min(), vmax=clipped_g.max())
ax12.grid(color='white', ls='--', alpha=0.5)
ax12.set_xlabel('RA')
ax12.set_ylabel('Declination')
ax22=fig.add_subplot(224)
_ = ax22.hist(clipped_g.ravel(), bins=300)
Now, we will see how to modify an image (arrays), for a better visualization. Two main types of transformations are provided:
In addition, classes are provided in order to identify lower and upper limits for a dataset based on specific algorithms.
Several classes are provided for determining intervals and for normalizing values in this interval to the $\left[0,1\right]$ range:
We use zscale interval class to find image limits
from astropy.visualization import ZScaleInterval
fig=plt.figure("zscale")
ax1=fig.add_subplot(121, projection=wcs)
zscale=ZScaleInterval()
vmin,vmax=zscale.get_limits(g)
im1=ax1.imshow(g, vmin=vmin, vmax=vmax)
fig.colorbar(im1)
<matplotlib.colorbar.Colorbar at 0x7f6cdf5c6978>
We can use z-scale (DS9 like) to rescale the image
ax2=fig.add_subplot(122, projection=wcs, sharex=ax1, sharey=ax1)
im2=ax2.imshow(zscale(g))
fig.colorbar(im2)
<matplotlib.colorbar.Colorbar at 0x7f6cdf554710>
fig=plt.figure()
#Defined the scale
zscale=ZScaleInterval()
gmin, gmax = zscale.get_limits(g)
rmin, rmax = zscale.get_limits(r)
imin, imax = zscale.get_limits(i)
ax11=fig.add_subplot(231)
ax11.imshow(g, vmin=gmin, vmax=imax, cmap="Blues", origin='lower') #lower parameter!
ax12=fig.add_subplot(232)
ax12.imshow(r, vmin=rmin, vmax=rmax, cmap="Greens", origin='lower')
ax13=fig.add_subplot(233)
ax13.imshow(i, vmin=imin, vmax=imax, cmap="Reds", origin='lower')
hrange=[min(gmin, rmin, imin), max(gmax, rmax, imax)]
#Plot histograms
ax21=fig.add_subplot(234)
_ = ax21.hist(g.ravel(), range=hrange, bins=100, color='blue')
ax22=fig.add_subplot(235)
_ = ax22.hist(r.ravel(), range=hrange, bins=100, color='green')
ax23=fig.add_subplot(236)
_ = ax23.hist(i.ravel(), range=hrange, bins=100, color='red')
In addition to classes that can scale values to the $\left[0:1\right]$ range, a number of classes are provided to stretch
the values using different functions. These map a $\left[0:1\right]$ range onto a transformed $\left[0:1\right]$ range.
AsinhStretch
BaseStretch
ContrastBiasStretch
HistEqStretch
LinearStretch
LogStretch
PowerDistStretch
PowerStretch
SinhStretch
SqrtStretch
NOTE: The stretch functions are similar but not always strictly identical to those used in e.g. DS9.
Matplotlib provides a custom normalization
and stretch when displaying data by passing a matplotlib.colors.Normalize object
NOTE: Stretches can also be combined with other stretches, just like transformations. The resulting CompositeStretch
can be used to normalize Matplotlib images like any other stretch.
from astropy.visualization import *
fig=plt.figure("Image normalize")
# Create an ImageNormalize object
norm = ImageNormalize(image, interval=ManualInterval(0.2, 5.0),
stretch=AsinhStretch()+PowerDistStretch(3.0))
im = plt.imshow(g, norm=norm, origin='lower')
Some stretching functions, require input parameter used to tune the stretch. For example the alternative power stretch
$$ y = \frac{a^x-1}{a-1}$$Lupton et al. (2004) describe an “optimal” algorithm for producing red-green-blue composite images from three separate high-dynamic range arrays. This method is implemented in make_lupton_rgb
as a convenience wrapper function
# Input: red, green and blue
rgb_lupton = make_lupton_rgb(i, r, g)
rgb_lupton
array([[[ 0, 0, 0], [ 0, 0, 0], [ 0, 0, 0], ..., [ 0, 0, 0], [ 0, 0, 0], [12, 1, 17]], [[ 0, 0, 0], [35, 0, 0], [ 0, 0, 0], ..., [ 0, 0, 0], [ 0, 27, 23], [ 0, 30, 35]], [[ 0, 0, 0], [ 0, 0, 0], [ 0, 0, 0], ..., [ 0, 0, 0], [ 0, 20, 21], [ 0, 0, 0]], ..., [[ 0, 0, 0], [ 0, 0, 0], [ 0, 0, 0], ..., [ 0, 0, 0], [ 0, 0, 0], [18, 28, 9]], [[ 0, 0, 0], [ 0, 0, 25], [53, 35, 0], ..., [ 0, 0, 0], [ 8, 12, 0], [ 2, 17, 48]], [[ 0, 0, 0], [ 1, 19, 1], [ 0, 0, 0], ..., [23, 26, 0], [ 0, 0, 0], [ 0, 36, 34]]], dtype=uint8)
We want to combine together 3 filters to obtain a RGB image
new_images=[]
#Loop over images: g, r, i
for image in (i, r, g):
#Simple example: use a custom recipe
median = np.median(image.data)
delta = (image.max()-median)/200.
high = median + delta
low = median - delta/8.
#Define transformation
T = ManualInterval(low, high)
#Apply transformation and create a new image
new_images.append(T(image))
grb_manual = np.dstack(new_images)
fig=plt.figure()
#Plot histograms
axm=fig.add_subplot(121, projection=wcs)
axm.set_title("Manual")
axm.imshow(grb_manual, origin='lower')
axm.grid(color='white', ls='--', alpha=0.5)
axm.set_xlabel("RA")
axm.set_ylabel("Declination")
axl=fig.add_subplot(122, projection=wcs, sharex=axm, sharey=axm)
axl.set_title("Lupton")
axl.imshow(rgb_lupton, origin='lower')
axl.grid(color='white', ls='--', alpha=0.5)
axl.set_xlabel("RA")
axl.set_ylabel("Declination")
Open the image selected to show contour levels, in this case an X-ray image from XMM, and select the region of interest as previously done in creating the RGB image
with fits.open("data/MLS2_xmm_img.fits.gz") as hdulist:
xmm = hdulist[0].copy()
x = Cutout2D(xmm.data, center, (4*u.arcmin, 3*u.arcmin),wcs=WCS(xmm))
x.shape
(60, 45)
g.shape
(1290, 968)
NOTE: In this case the image has not the same WCS as the CFHT images. We need to reproject the image onto the WCS of the GRB image
from reproject import reproject_interp
array, footprint = reproject_interp((x.data, x.wcs), wcs, shape_out=g.shape)
array.shape
/home/ict_user/ict_astropy/anaconda3/lib/python3.7/site-packages/reproject/interpolation/core_celestial.py:26: FutureWarning: Conversion of the second argument of issubdtype from `float` to `np.floating` is deprecated. In future, it will be treated as `np.float64 == np.dtype(float).type`. if not np.issubdtype(array.dtype, np.float):
(1290, 968)
We want to use the X-ray image to compute the contour plot of the X-ray emission and oveplot it onto the RGB image
axm.contour(array, levels=(2.5, 5, 7.5, 10, 12.5), colors='pink')
<matplotlib.contour.QuadContourSet at 0x7f6cdc958128>
axm
<matplotlib.axes._subplots.WCSAxesSubplot at 0x7f6cdf5330f0>
To plot smoother contours, use the scipy.ndimage
package to smooth the original image and recompute contour on it
import scipy.ndimage as ndimage
sarray = ndimage.gaussian_filter(array, sigma=5.0)
axl.contour(sarray, levels=(2.5, 5, 7.5, 10, 12.5), colors='cyan')
<matplotlib.contour.QuadContourSet at 0x7f6cdc978be0>
The scipy.ndimage
package provides a number of general image processing and analysis functions that are designed to operate with arrays of arbitrary dimensionality. The package currently includes:
from astropy.table import Table
catalog=Table.read('data/D1_MLS2_zspec.tbl', format='ascii')
catalog
num | ra | dec | zspec | zflg |
---|---|---|---|---|
int64 | float64 | float64 | float64 | float64 |
976 | 36.3636 | -4.20161 | 0.102 | 2.0 |
978 | 36.3734 | -4.22755 | 0.1434 | 2.0 |
979 | 36.376 | -4.23551 | 0.1402 | 3.0 |
980 | 36.3781 | -4.23835 | 0.1434 | 4.0 |
981 | 36.3788 | -4.24248 | 0.1365 | 2.0 |
982 | 36.3792 | -4.24476 | 0.04 | 1.0 |
983 | 36.3418 | -4.26092 | 0.1727 | 3.0 |
984 | 36.3809 | -4.27726 | 0.218 | 1.0 |
1936 | 36.43624 | -4.20471 | 0.0528 | 22.0 |
1943 | 36.41881 | -4.27083 | 0.0543 | 3.0 |
... | ... | ... | ... | ... |
24457 | 36.37908 | -4.24485 | 0.146 | -99.0 |
24458 | 36.38768 | -4.26341 | 0.144 | -99.0 |
24460 | 36.39442 | -4.26677 | 0.137 | -99.0 |
24462 | 36.40115 | -4.26723 | 0.329 | -99.0 |
30499 | 36.33647 | -4.21295 | 1.56 | 3.0 |
30509 | 36.36812 | -4.26105 | 0.56 | 3.0 |
30513 | 36.37814 | -4.23847 | 0.14 | 3.0 |
41831 | 36.40302 | -4.25499 | 0.5559 | 2.0 |
173694 | 36.40302 | -4.25499 | 0.5558 | -99.0 |
173695 | 36.37817 | -4.23854 | 0.1429 | -99.0 |
Add markes on manual RGB figure
# Retreive marker in pixels
markers=np.array([catalog['ra'], catalog['dec']])
# COnvert pixels in coordinate system positions
pix_markers = wcs.wcs_world2pix(markers.T, 0)
axm.scatter(pix_markers[:,0], pix_markers[:,1], s=30, edgecolor='red', facecolor='none')
<matplotlib.collections.PathCollection at 0x7f6cdc83c1d0>
markers=np.array([catalog['ra'], catalog['dec']])
markers.T
array([[36.3636 , -4.20161], [36.3734 , -4.22755], [36.376 , -4.23551], [36.3781 , -4.23835], [36.3788 , -4.24248], [36.3792 , -4.24476], [36.3418 , -4.26092], [36.3809 , -4.27726], [36.43624, -4.20471], [36.41881, -4.27083], [36.42199, -4.26681], [36.36984, -4.2775 ], [36.40837, -4.26592], [36.39288, -4.21246], [36.41027, -4.25943], [36.41568, -4.2015 ], [36.42631, -4.276 ], [36.39288, -4.25948], [36.38897, -4.19544], [36.37774, -4.27459], [36.38976, -4.27189], [36.39051, -4.2229 ], [36.32451, -4.28146], [36.39158, -4.27182], [36.36637, -4.26903], [36.42687, -4.26465], [36.41674, -4.2711 ], [36.39976, -4.26811], [36.31615, -4.27182], [36.41765, -4.25329], [36.41985, -4.25618], [36.35527, -4.2682 ], [36.40365, -4.26118], [36.39457, -4.26397], [36.43215, -4.27449], [36.37237, -4.27349], [36.33191, -4.27158], [36.43079, -4.22233], [36.39637, -4.28394], [36.38213, -4.2767 ], [36.329 , -4.28839], [36.38657, -4.2543 ], [36.41492, -4.27822], [36.42226, -4.22621], [36.40081, -4.28624], [36.38097, -4.2224 ], [36.43084, -4.25962], [36.31823, -4.2736 ], [36.43533, -4.25464], [36.32049, -4.27735], [36.3784 , -4.26862], [36.41206, -4.24977], [36.36259, -4.27016], [36.38295, -4.22539], [36.3967 , -4.24229], [36.38504, -4.20267], [36.35067, -4.26804], [36.41579, -4.24576], [36.42044, -4.2065 ], [36.32263, -4.27401], [36.40618, -4.26502], [36.42738, -4.20795], [36.41394, -4.20895], [36.425 , -4.28557], [36.4365 , -4.28662], [36.33829, -4.26992], [36.38833, -4.23308], [36.42394, -4.26122], [36.32693, -4.27651], [36.40503, -4.27157], [36.4247 , -4.22358], [36.40865, -4.22786], [36.39048, -4.26151], [36.31617, -4.27181], [36.31821, -4.27361], [36.3205 , -4.27736], [36.32262, -4.274 ], [36.3245 , -4.28147], [36.32692, -4.2765 ], [36.329 , -4.28839], [36.33192, -4.27158], [36.33829, -4.26992], [36.33842, -4.23119], [36.35067, -4.26803], [36.35529, -4.26819], [36.36259, -4.27017], [36.36638, -4.26903], [36.36983, -4.2775 ], [36.37238, -4.2735 ], [36.37775, -4.27458], [36.37842, -4.26861], [36.38096, -4.22239], [36.38213, -4.27669], [36.38296, -4.22539], [36.38504, -4.20267], [36.38658, -4.25431], [36.38834, -4.23308], [36.38896, -4.19544], [36.38975, -4.27189], [36.39046, -4.2615 ], [36.3905 , -4.22289], [36.39159, -4.27183], [36.39288, -4.21247], [36.39288, -4.25947], [36.39458, -4.26397], [36.39637, -4.28394], [36.39671, -4.24228], [36.39975, -4.26811], [36.40079, -4.28625], [36.40367, -4.26119], [36.40504, -4.27158], [36.40617, -4.26503], [36.40866, -4.22786], [36.41029, -4.25942], [36.41204, -4.24978], [36.41396, -4.20894], [36.41492, -4.27822], [36.41567, -4.2015 ], [36.41579, -4.24575], [36.41675, -4.27111], [36.41767, -4.25328], [36.41879, -4.27083], [36.41983, -4.25619], [36.42046, -4.2065 ], [36.422 , -4.26681], [36.42225, -4.22622], [36.42392, -4.26122], [36.42471, -4.22358], [36.425 , -4.28558], [36.42633, -4.276 ], [36.42688, -4.26464], [36.42738, -4.20794], [36.43079, -4.22233], [36.43084, -4.25961], [36.43217, -4.2745 ], [36.43533, -4.25464], [36.43625, -4.20469], [36.4365 , -4.28661], [36.32535, -4.27399], [36.3425 , -4.26231], [36.36804, -4.26113], [36.37141, -4.25798], [36.37334, -4.22769], [36.37489, -4.25695], [36.37577, -4.23555], [36.3781 , -4.23849], [36.37882, -4.24263], [36.37908, -4.24485], [36.38768, -4.26341], [36.39442, -4.26677], [36.40115, -4.26723], [36.33647, -4.21295], [36.36812, -4.26105], [36.37814, -4.23847], [36.40302, -4.25499], [36.40302, -4.25499], [36.37817, -4.23854]])
Add patches to the Lupton RGB figure
from matplotlib.patches import RegularPolygon
for ra,dec in catalog['ra', 'dec']:
c = RegularPolygon((ra, dec), 5, 0.001, edgecolor='yellow', facecolor='none',
transform=axl.get_transform('fk5'))
axl.add_patch(c)
#Prepare the canvas
fig=plt.figure(figsize=[4.0, 4.0], dpi=300)
ax=plt.subplot(projection=wcs)
ax.imshow(grb_manual, origin='lower')
ax.grid(color='white', ls='--')
ax.set_xlabel("RA", fontsize='x-small')
ax.set_ylabel("Declination", fontsize='x-small')
ax.tick_params(labelsize='xx-small')
#Display patches
for ra,dec in catalog['ra', 'dec']:
c = RegularPolygon((ra, dec), 5, 0.001, edgecolor='yellow', facecolor='none',
transform=ax.get_transform('fk5'))
ax.add_patch(c)
#Display contour
ax.contour(sarray, levels=(2.5, 5, 7.5, 10, 12.5), colors='cyan')
#Select a region
ax.set_xlim(200,800)
ax.set_ylim(400,1000)
#Save
plt.savefig('tmp/cluster.png')
plt.savefig('tmp/cluster.pdf')
!ristretto tmp/cluster.png