Images

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:

  1. open large images (CFHTLS-Deep field) and extract only an interesting area
  2. combine images acquired with different filters and create a RGB image
  3. overplot the contours of the X-ray emission obtained from an X-ray image (XMM-Newton Science Archive)
  4. overplot markers/patches on selected targets
  5. save the final image (png, pdf, svg, ...)

Keys: WCS, SkyCoord, Cutout2D, data visualization (images normalization, countour plots, marker, ...), GRB combination, reprojection, Multidimensional image

Open image

In [1]:
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
Out[1]:
(19354, 19354)
In [ ]:
 

WCS

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.

In [2]:
#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]
Out[2]:
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
In [3]:
image
Out[3]:
<astropy.io.fits.hdu.image.PrimaryHDU at 0x7f6da8212c18>
  • Uses WCS to convert pixels into sky coordinates

    NOTE: 0 is the origin parameter (Numpy-like)

In [4]:
coords = wcs.wcs_pix2world(((5090,5115),(5091,5115),(5090,5116)),0, ra_dec_order=True)
coords
Out[4]:
array([[36.73360896, -4.73008007],
       [36.73355712, -4.73008009],
       [36.73360894, -4.73002841]])
In [ ]:
 
  • and viceversa
In [5]:
wcs.wcs_world2pix(coords,0)
Out[5]:
array([[5090., 5115.],
       [5091., 5115.],
       [5090., 5116.]])
In [ ]:
 

Cutout2D: extract a subimage

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 size
  • partial: 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

In [6]:
#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)
In [7]:
Cutout2D?

SkyCoord and angles

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.

In [8]:
c = SkyCoord('2:25:31 -4:14:27.78', unit=(u.hourangle, u.deg), frame='icrs')
center, c
Out[8]:
(<SkyCoord (ICRS): (ra, dec) in deg
     (36.37916667, -4.24105)>, <SkyCoord (ICRS): (ra, dec) in deg
     (36.37916667, -4.24105)>)
In [ ]:
 
  • coordinates transformations
In [ ]:
c.galactic, c.fk4, c.fk5
In [ ]:
 
  • retrieve angles from coordinates
In [ ]:
c.ra, c.dec
In [ ]:
 
  • angle conversions
In [ ]:
c.ra.hour, c.ra.arcmin, c.ra.rad, c.ra.deg

The distance

SkyCoord has the additional distance attribute

In [9]:
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
Out[9]:
<SkyCoord (ICRS): (ra, dec, distance) in (deg, deg, kpc)
    (15.62916667, -2.17438333, 123.)>
In [ ]:
 
  • compute distances
In [10]:
c2.separation(c3), c2.separation_3d(c3)
Out[10]:
(<Angle 20.81882624 deg>, <Distance 461.10775111 kpc>)
In [ ]:
 
  • quick way to get coordinates for named objects (Sesame)
In [11]:
SkyCoord.from_name('NGC 1142')
Out[11]:
<SkyCoord (ICRS): (ra, dec) in deg
    (43.800955, -0.183547)>
In [ ]:
 

Save the thumbnail

In [12]:
#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)
In [13]:
thumb.wcs.to_header()
Out[13]:
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         
In [14]:
# 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)
In [ ]:
 

Now we extract one subimage for each filter and save it

In [15]:
#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]

f-strings

As of Python 3.6, f-strings are a great new way to format strings. Not only they are more readable, more concise, and less prone to error than other ways of formatting, but they are also faster.

In [ ]:
label="alpha"
value=2.12345E-7
"%s=%.3e" % (label, 5*value) # Old style
In [ ]:
f"{label}={5*value}"
In [ ]:
f"{label}={5*value:.3e}"

Data visualization

astropy.visualization provides functionality that can be helpful when visualizing data. This includes

  • a framework for plotting Astronomical images with coordinates with Matplotlib
  • functionality related to image normalization (including both scaling and stretching)
  • smart histogram plotting
  • RGB color image creation from separate images
In [16]:
#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()    
In [ ]:
 

Display image

WCSAxes is a framework for making plots of Astronomical data in Matplotlib

In [17]:
#%matplotlib qt
%matplotlib notebook
In [18]:
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')
In [19]:
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)

In [20]:
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)
In [ ]:
 

Image stretching and normalization

Now, we will see how to modify an image (arrays), for a better visualization. Two main types of transformations are provided:

  1. Normalization (Interval objects) to the $\left[0,1\right]$ range using lower and upper limits where $x$ represents the values in the original image and $y$ the transformed image:
$$y=\left\{ \begin{array}{} \frac{x−v_{min}}{v_{max}−v_{min}}\\ 0 \quad \text{if $x < v_{min}$}\\ 1 \quad \text{if $x > v_{max}$} \end{array} \right.$$

In addition, classes are provided in order to identify lower and upper limits for a dataset based on specific algorithms.

  1. Stretching (Stretch objects) of values in the $\left[0,1\right]$ range to the $\left[0,1\right]$ range using a linear or non-linear function:
$$ z=f(y) $$

Intervals

Several classes are provided for determining intervals and for normalizing values in this interval to the $\left[0,1\right]$ range:

The zscale example

We use zscale interval class to find image limits

In [21]:
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)
Out[21]:
<matplotlib.colorbar.Colorbar at 0x7f6cdf5c6978>

We can use z-scale (DS9 like) to rescale the image

In [22]:
ax2=fig.add_subplot(122, projection=wcs,  sharex=ax1, sharey=ax1)
im2=ax2.imshow(zscale(g))
fig.colorbar(im2)
Out[22]:
<matplotlib.colorbar.Colorbar at 0x7f6cdf554710>
In [23]:
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 [ ]:
 

Stretch

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.

Matplotlib normalization

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.

In [24]:
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')

Parameters in stretch functions

Some stretching functions, require input parameter used to tune the stretch. For example the alternative power stretch

$$ y = \frac{a^x-1}{a-1}$$

Automatic creation of RGB image

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

In [25]:
# Input: red, green and blue
rgb_lupton = make_lupton_rgb(i, r, g)
In [26]:
rgb_lupton
Out[26]:
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)

RGB manual combination

We want to combine together 3 filters to obtain a RGB image

In [27]:
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)
In [ ]:
 

Display the RGB images

In [28]:
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")
In [ ]:
 

Overplot contours

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

In [29]:
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
Out[29]:
(60, 45)
In [30]:
g.shape
Out[30]:
(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

Re-projection

The reproject package implements image reprojection (resampling) methods for astronomical images and more generally n-dimensional data.

It is not direclty provided with Astropy, thus it must be manually added.

conda install -c astropy reproject
In [31]:
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):
Out[31]:
(1290, 968)
In [ ]:
 

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

In [32]:
axm.contour(array, levels=(2.5, 5, 7.5, 10, 12.5), colors='pink')
Out[32]:
<matplotlib.contour.QuadContourSet at 0x7f6cdc958128>
In [33]:
axm
Out[33]:
<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

In [34]:
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')
Out[34]:
<matplotlib.contour.QuadContourSet at 0x7f6cdc978be0>
In [ ]:
 

scipy.ndimage

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:

  • Filter functions
    • Correlation and convolution
    • Smoothing filters
    • Filters based on order statistics
    • Derivatives
    • Generic filter functions
    • Fourier domain filters
  • Interpolation functions
    • Spline pre-filters
    • Interpolation functions
  • Morphology
    • Binary morphology
    • Grey-scale morphology
  • Distance transforms

Markers or patches

As last step, we overplot a symbol at a position listed in a catalog. The symbol can be:

Read a catalog for objects positions

In [35]:
from astropy.table import Table

catalog=Table.read('data/D1_MLS2_zspec.tbl', format='ascii')
catalog
Out[35]:
Table length=157
numradeczspeczflg
int64float64float64float64float64
97636.3636-4.201610.1022.0
97836.3734-4.227550.14342.0
97936.376-4.235510.14023.0
98036.3781-4.238350.14344.0
98136.3788-4.242480.13652.0
98236.3792-4.244760.041.0
98336.3418-4.260920.17273.0
98436.3809-4.277260.2181.0
193636.43624-4.204710.052822.0
194336.41881-4.270830.05433.0
...............
2445736.37908-4.244850.146-99.0
2445836.38768-4.263410.144-99.0
2446036.39442-4.266770.137-99.0
2446236.40115-4.267230.329-99.0
3049936.33647-4.212951.563.0
3050936.36812-4.261050.563.0
3051336.37814-4.238470.143.0
4183136.40302-4.254990.55592.0
17369436.40302-4.254990.5558-99.0
17369536.37817-4.238540.1429-99.0

Marker

Add markes on manual RGB figure

In [36]:
# 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')
Out[36]:
<matplotlib.collections.PathCollection at 0x7f6cdc83c1d0>
In [37]:
markers=np.array([catalog['ra'], catalog['dec']])
markers.T
Out[37]:
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]])

Patch

Add patches to the Lupton RGB figure

In [38]:
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)

Save the figure

In [39]:
#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')
In [ ]:
 
In [40]:
!ristretto tmp/cluster.png
In [ ]: