F: Integración de imagenes en color

Las imágenes RGB/SHO se pueden producir utilizando la capacidad de matplotlib para crear imágenes de tres colores. En general, una imagen RGB es una matriz $ N:nbsphinx-math:times `M :nbsphinx-math:times 3` $, donde \(N\) es la dimensión \(x\) y \(M\) es la dimensión \(y\).

El 3 representa que hay tres canales, el rojo, el verde y el azul, respectivamente. Se puede especificar una cuarta capa que representa el valor alfa (\(\alpha\)-channel) o transparencia (no confundir con las imágenes en filtro \(ha\)). Se puede consultar algo más sobre espacios de color en la Wikipedia.

Las imágenes a color por una cámara normalmente vienen a partir de una matriz de fotosensores (para cada píxel) con filtros de distinto color formando una matriz o mosaico de Bayer.

Matplotlib tiene varias herramientas para manipular imágenes en color en matplotlib.colors.

Las herramientas de visualización de Astropy se pueden utilizar para estirar la imagen y escalar las capas individuales (R, G y B) de la imagen RGB. Cada capa debe estar en una escala de 0.0 a 1.0 para floats (o de 0 a 255 para números enteros). Los valores que see encuentren fuera de este rango serán recortados.

Lupton et al. (2004) describe un algoritmo «óptimo» para producir imágenes compuestas por los canales RGB a partir de tres matrices independientes. Este método se implementa como una funcion de astropy a traves de make_lupton_rgb.

Además haremos uso de algunas funciones de la biblioteca OpenCV (una biblioteca de visión por computadora), que debemos instalar. Ojo, la instalación tardará un buen rato y tendrá que descargar muchos paquetes:

> conda install -c conda-forge opencv

Imagen virtual

Creamos una imagen virtual de 100x100 pixeles con valores aleatorios para los tres canales RGB.

Aplicamos la funcion `make_lupton_rgb <https://docs.astropy.org/en/stable/api/astropy.visualization.make_lupton_rgb.html>`__ para obtener la imagen RGB.

[1]:
import numpy as np
import matplotlib.pyplot as plt
from astropy.visualization import make_lupton_rgb
[2]:
plt.figure(figsize=(10, 10))
image_r = np.random.random((100,100))
image_g = np.random.random((100,100))
image_b = np.random.random((100,100))
image = make_lupton_rgb(image_r, image_g, image_b, stretch=0.5)
plt.imshow(image, origin = 'lower')
[2]:
<matplotlib.image.AxesImage at 0x7f1be787bf70>
_images/F-2-imagenesColor_6_1.png

Estructura interna de la imagen en color

Una imagen en color, a diferencia de una imagen monocroma, tiene 3 canales y, por lo tanto, tiene 3 valores asociados a esos canales.

[3]:
image
[3]:
array([[[142,  65,  15],
        [217,   2,  27],
        [ 67, 134, 131],
        ...,
        [107,  24, 154],
        [ 57, 143, 132],
        [159,  94,  45]],

       [[153,  30, 110],
        [125,  83, 123],
        [103,  59, 151],
        ...,
        [ 82, 157,  75],
        [131, 141,  26],
        [ 13,  53, 153]],

       [[  0, 133, 123],
        [141, 137,  31],
        [108,  96, 128],
        ...,
        [ 63,  92, 106],
        [195,  35,  46],
        [ 58,  94, 104]],

       ...,

       [[ 19, 145,  59],
        [ 67, 127, 115],
        [110,  23,  82],
        ...,
        [ 74,  51, 106],
        [ 15, 159, 107],
        [ 82,  42, 155]],

       [[121, 118, 100],
        [192,  91,   5],
        [ 71,  89, 150],
        ...,
        [106,  24,  84],
        [100,  90, 133],
        [ 44,   6, 195]],

       [[ 97, 102, 117],
        [111, 132,  17],
        [ 65,   2, 152],
        ...,
        [ 31, 173,  98],
        [136,  77,  71],
        [ 39, 131,  63]]], dtype=uint8)

Para comprobar que valores hay en un canal concreto debo de buscar en la matriz los pixeles que quiero y luego el canal normalmente ordenado en RGB (0 - rojo, 1 - verde, 2- azul):

[4]:
image[0:10, 0:10, 0] # [pixel_inicial_y:pixel_final_y, pixel_inicial_x: pixel_final_x, canal]
                     #canal rojo
[4]:
array([[142, 217,  67, 129, 114, 164,  44,  20, 112,  69],
       [153, 125, 103, 121,  90, 128, 167,  65, 115, 145],
       [  0, 141, 108, 100,  99,  41,  24,  68, 124, 124],
       [ 41,  82,  29, 128,  35, 109,  25, 136,  90, 124],
       [ 52, 158,  11, 102, 161, 138, 122, 175,   9,   7],
       [149, 165, 206, 129, 111,  67,  87,  27, 136,  86],
       [ 46, 105, 117,  40,  91, 133,  77,  85, 137,  56],
       [ 94,  67,  98,  78,  54,  36, 152, 116, 162,  56],
       [139, 148,  44,  52, 113, 135,   3, 115,  40, 169],
       [136,  99,   7, 189,  96, 100,  81, 150, 169, 143]], dtype=uint8)
[5]:
image[0:10, 0:10, 1] #canal verde
[5]:
array([[ 65,   2, 134, 149, 116,  24,  53, 174, 165, 145],
       [ 30,  83,  59, 127,  24,  90,  25,  75, 112,  99],
       [133, 137,  96, 106, 135, 141, 144, 151,  72, 136],
       [138,  69, 138,  57, 150, 114,  34,  13, 133,  88],
       [128,  10, 218,  91, 111, 149, 137,   3,  86, 129],
       [ 41, 116,  49, 127,  20, 131, 181, 124, 147, 115],
       [151,   9,  78, 159, 156,  96,  13,  90,  90,  81],
       [144, 131,  61,  86, 128, 104, 145, 128,  32,   5],
       [129,  40, 106, 117, 169,  69, 132, 147, 127,  77],
       [139, 140, 156,  29, 155,  60,  51, 111,  59, 143]], dtype=uint8)
[6]:
image[0:10, 0:10, 2] #canal azul
[6]:
array([[ 15,  27, 131,  42,  88,  74, 132,  78,  34,  44],
       [110, 123, 151,  25, 131,  61, 100, 159, 110,  55],
       [123,  31, 128, 132, 100, 126, 126,  26,  70,  26],
       [142, 112, 139, 133,  93,  73, 126, 102, 118,  60],
       [124, 144,  37,  37,   7,  30,  70, 124, 149, 109],
       [ 71,  28,   0,  90, 172,  96,  11,  99,  26, 126],
       [  9, 162,  61,  83,  34, 100, 157, 148,  71, 140],
       [ 91,  77, 156, 156, 144, 138,  17,  84,  85, 190],
       [ 66,  97,  17,  77,  22, 118, 145,  51, 132,  46],
       [ 58,  85, 148,  54,  48, 157, 145,  26,  52,  48]], dtype=uint8)

Podemos cambiar los valores de un canal concreto a partir de cualquier operacion matemática:

[7]:
canal = 1 # Canal que quiero cambiar. En nuestro caso hemos seleccionando el canal verde
len_y, len_x, color = np.shape(image) # Averiguando las dimensiones de mi imagen

image[:,:,canal] = image[:,:,canal]/1.5 # voy subiendo en y hasta completar toda la imagen
[8]:
plt.figure(figsize=(10, 10))
plt.imshow(image, origin = 'lower')
[8]:
<matplotlib.image.AxesImage at 0x7f1bdf56eb20>
_images/F-2-imagenesColor_16_1.png

Ejemplos con imágenes reales

Grupo Hickson 88, SDSS

[9]:
import matplotlib.pyplot as plt
from astropy.visualization import make_lupton_rgb
from astropy.io import fits
from astropy.utils.data import get_pkg_data_filename

# Lee las tres imágenes descargadas desde aquí:
g_name = get_pkg_data_filename('visualization/reprojected_sdss_g.fits.bz2')
r_name = get_pkg_data_filename('visualization/reprojected_sdss_r.fits.bz2')
i_name = get_pkg_data_filename('visualization/reprojected_sdss_i.fits.bz2')

g = fits.open(g_name)[0].data
r = fits.open(r_name)[0].data
i = fits.open(i_name)[0].data
plt.figure(figsize=(10, 10))
rgb_default = make_lupton_rgb(i, r, g, filename="salidas/ngc6976-default.jpg")
plt.imshow(rgb_default, origin='lower')
plt.show()
_images/F-2-imagenesColor_19_0.png
[10]:
plt.figure(figsize=(10, 10))
rgb = make_lupton_rgb(i, r, g, Q=12, stretch=0.2, filename="salidas/ngc6976.jpeg")
plt.imshow(rgb, origin='lower')
plt.show()
_images/F-2-imagenesColor_20_0.png

Pilares de la creación, Hubble

[11]:
import numpy as np
import matplotlib.pyplot as plt
from astropy.io import fits
from astropy.visualization import make_lupton_rgb

forc=np.float_()
r=fits.open("imagenes/eagle/673nmos.fits")[0].data
g=fits.open("imagenes/eagle/656nmos.fits")[0].data
b=fits.open("imagenes/eagle/502nmos.fits")[0].data
[12]:
plt.figure(figsize=(10, 10))
rgb_default = make_lupton_rgb(r,g,b,Q=1,stretch=100,filename="salidas/pilar.jpg")
plt.imshow(r, origin='lower')
plt.show()
_images/F-2-imagenesColor_23_0.png
[13]:
plt.figure(figsize=(10, 10))
rgb_default = make_lupton_rgb(r,g,b,Q=0.001,stretch=300,filename="salidas/pilar.jpg")
plt.imshow(rgb_default, origin='lower')
plt.show()
_images/F-2-imagenesColor_24_0.png
[14]:
plt.figure(figsize=(10, 10))
rgb_default = make_lupton_rgb(r,g*0.1,b,Q=0.001,stretch=50,filename="salidas/pilar.jpg")
plt.imshow(rgb_default, origin='lower')
plt.show()
_images/F-2-imagenesColor_25_0.png
[15]:
plt.figure(figsize=(10, 10))
rgb_default = make_lupton_rgb(r*0.8,g*0.1,b,Q=0.001,stretch=50,filename="salidas/pilar.jpg")
plt.imshow(rgb_default, origin='lower')
plt.show()
_images/F-2-imagenesColor_26_0.png

Nebulosa de la Tarantula, SAG

[16]:
import numpy as np
import matplotlib.pyplot as plt
from astropy.io import fits
from astropy.visualization import make_lupton_rgb
from registrar import registra_lista, imprimir_info
from rotar import rotar_imagen
from trasladar import shift_image, desplazar_imagen
import cv2

r = fits.open("imagenes/salidaCalibracion/sho/imagenFinal_Sii.fit")[0].data
g = fits.open("imagenes/salidaCalibracion/sho/imagenFinal_H-alpha.fit")[0].data
g_header = fits.open("imagenes/salidaCalibracion/sho/imagenFinal_H-alpha.fit")[0].header
b = fits.open("imagenes/salidaCalibracion/sho/imagenFinal_Oiii.fit")[0].data

Tenemos que intentar alinear las imágenes para que las estrellas de cada canal coincidan. Como veremos las funciones rotar_imagen y despazar_imagen no funcionan como deberian.

[17]:
rgb = [r, g, b]
rgb_rotado = rotar_imagen(rgb)
reg_desplazado = desplazar_imagen(rgb_rotado)
La imagen 1 está rotada -213 grados
La imagen 2 está rotada -28 grados
La imagen 1 se ha desplazado en (x, y) = (-222, -3738)
La imagen 2 se ha desplazado en (x, y) = (-88, -4387)

Tampoco parece funcionar la librería de astroalign

[18]:
import astroalign as aa
registered_image, footprint = aa.register(r, g)#, fill_value=None, max_control_points=50, detection_sigma=5, min_area=5)
---------------------------------------------------------------------------
MaxIterError                              Traceback (most recent call last)
Cell In[18], line 2
      1 import astroalign as aa
----> 2 registered_image, footprint = aa.register(r, g)#, fill_value=None, max_control_points=50, detection_sigma=5, min_area=5)

File ~/anaconda3/envs/cursoAstronomia2/lib/python3.8/site-packages/astroalign.py:488, in register(source, target, fill_value, propagate_mask, max_control_points, detection_sigma, min_area)
    454 def register(
    455     source,
    456     target,
   (...)
    461     min_area=5,
    462 ):
    463     """Transform ``source`` to coincide pixel to pixel with ``target``.
    464
    465     Args:
   (...)
    486
    487     """
--> 488     t, __ = find_transform(
    489         source=source,
    490         target=target,
    491         max_control_points=max_control_points,
    492         detection_sigma=detection_sigma,
    493         min_area=min_area,
    494     )
    495     aligned_image, footprint = apply_transform(
    496         t, source, target, fill_value, propagate_mask
    497     )
    498     return aligned_image, footprint

File ~/anaconda3/envs/cursoAstronomia2/lib/python3.8/site-packages/astroalign.py:361, in find_transform(source, target, max_control_points, detection_sigma, min_area)
    359     inlier_ind = _np.arange(len(matches))  # All of the indices
    360 else:
--> 361     best_t, inlier_ind = _ransac(
    362         matches, inv_model, PIXEL_TOL, min_matches
    363     )
    364 triangle_inliers = matches[inlier_ind]
    365 d1, d2, d3 = triangle_inliers.shape

File ~/anaconda3/envs/cursoAstronomia2/lib/python3.8/site-packages/astroalign.py:594, in _ransac(data, model, thresh, min_matches)
    591         break
    593 if good_fit is None:
--> 594     raise MaxIterError(
    595         "List of matching triangles exhausted before an acceptable "
    596         "transformation was found"
    597     )
    599 better_fit = good_fit
    600 for i in range(3):

MaxIterError: List of matching triangles exhausted before an acceptable transformation was found

No obstante probamos a generar una imagen en falso color a través de los canales H-alpha SII y OIII creando una paleta SHO.

[19]:
rgb_default = make_lupton_rgb(r, g, b,Q=1,stretch=3000,filename="salidas/shoTarantula.jpg")
#rgb_default = make_lupton_rgb(r, g, b, Q=0.001, stretch=1000,filename="salidas/shoTarantula.jpg")
[20]:
plt.figure(figsize=(8, 8))
plt.imshow(rgb_default, origin='lower')
plt.show()
_images/F-2-imagenesColor_35_0.png
[21]:
from astropy.visualization import simple_norm
from astropy.visualization import (MinMaxInterval, LogStretch, ImageNormalize)
# Create interval object
interval = MinMaxInterval()
vmin, vmax = interval.get_limits(rgb_default)

# Create an ImageNormalize object using a SqrtStretch object
norm = ImageNormalize(vmin=vmin, vmax=vmax, stretch=LogStretch())

plt.figure(figsize=(8, 8))
plt.imshow(rgb_default, origin='lower', norm=norm)
plt.show()
_images/F-2-imagenesColor_36_0.png

Podemos comprobar que las estrellas de los diferentes canales no han coincidido unas con otras. Mas tarde corregiremos este problema.

[22]:
plt.figure(figsize=(8, 8))
plt.imshow(rgb_default[2000:2500,2000:2500], origin='lower', norm=norm)
plt.show()
_images/F-2-imagenesColor_38_0.png

Image denoise

Al igual que la reduccion de ruido se puede aplicar antes de generar la imagen RGB, también podemos reducir ruido después de generar dicha imagen aunque no es conveniente eliminar mucho ruido después del estirado que se genera al realizar la imagen en color.

Para la reducción de ruido usaremos el método `fastNlMeansDenoisingColored <https://docs.opencv.org/3.4/d1/d79/group__photo__denoise.html>`__ de la biblioteca OpenCV

[23]:
import numpy as np
import cv2
from matplotlib import pyplot as plt

dst = cv2.fastNlMeansDenoisingColored(rgb_default,None,10,10,7,21)

Para normalizar la imagen y estirarla mejor podemos usar MinMaxInterval que determina los límites de los valores en función de los valores mínimo y máximo de la matriz de la imagen.

Los límites se pueden determinar llamando al método .get_limits() que toma la matriz de valores.

Por último creamos el objeto ImageNormalize que pasaremos a matplotlib

[24]:
interval = MinMaxInterval()
vmin, vmax = interval.get_limits(dst)

# Create an ImageNormalize object using a SqrtStretch object
norm = ImageNormalize(vmin=vmin, vmax=vmax, stretch=LogStretch())

plt.figure(figsize=(8, 8))
plt.imshow(dst, origin='lower', norm=norm)
plt.show()
_images/F-2-imagenesColor_43_0.png

Guardamos la imagen generada

[25]:
SHO = fits.PrimaryHDU(data=dst.astype(np.float32), header = g_header)
SHO.writeto("salidas/salidaCalibracion/SHO.fit", overwrite=True)

Localización de una imagen a traves de WCS

World Coordinate Systems (WCS) describen las transformaciones geométricas entre un conjunto de coordenadas y otro. Una aplicación común es mapear los píxeles de una imagen en la esfera celeste. Otra aplicación común es mapear los píxeles a la longitud de onda en un espectro.

astropy.wcs (documentación) contiene utilidades para gestionar las transformaciones del World Coordinate Systems (WCS) definidas en varias convenciones estándar FITS. Estas transformaciones funcionan tanto del píxel en la imagen a coordenadas como de coordenadas hacia el píxel en la imagen.

Para ello usaremos la función WCS de la biblioteca de astropy. A esta función debemos pasar la cabezera de la imagen que queramos sacar las coordenadas. ¡Atención! El programa de captura deberá haber anotado dicha información en la captura para poder obtener las coordenadas de la imagen. Si no, haría falta un paso previo de resolución de placas.

[26]:
from astropy.wcs import WCS
from astropy.io import fits
h_header = fits.open("salidas/salidaCalibracion/sho/imagenFinal_H-alpha.fit")[0].header
SHO = fits.open("salidas/salidaCalibracion/SHO.fit")[0].data
wcs = WCS(h_header)
wcs
WARNING: FITSFixedWarning: RADECSYS= 'FK5 ' / Equatorial coordinate system
the RADECSYS keyword is deprecated, use RADESYSa. [astropy.wcs.wcs]
WARNING: FITSFixedWarning: 'datfix' made the change 'Set MJD-OBS to 58887.129340 from DATE-OBS'. [astropy.wcs.wcs]
[26]:
WCS Keywords

Number of WCS axes: 2
CTYPE : 'RA---TAN'  'DEC--TAN'
CRVAL : 84.3359148996  -69.3257259717
CRPIX : 2048.0  2048.0
CD1_1 CD1_2  : -3.38076526016e-06  -0.00026594291957
CD2_1 CD2_2  : 0.000265979746215  -3.38029717098e-06
NAXIS : 4096  4096

Aquí nos aparece cierta informacion, la mas relevante es: + CRVAL: RA y Dec del pixel central. + CRPIX: valor del pixel central. + CD1: separacion entre un pixel y otro en RA. + CD2: separacion entre un pixel y otro en Dec.

Un ejemplo del uso de WCS es pixel_to_world para convertir coordenadas de píxel a coordenadas RA y Dec:

[27]:
coordenadas = wcs.pixel_to_world(30, 40)
coordenadas
[27]:
<SkyCoord (FK5: equinox=2000.0): (ra, dec) in deg
    (85.90508779, -69.84845846)>

Del mismo modo podemos usar world_to_pixel para obtener coordenadas RA y Dec a través coordenadas de píxel en mi imagen:

[28]:
x_pixel, y_pixel = wcs.world_to_pixel(coordenadas)
x_pixel, y_pixel
[28]:
(array(30.), array(40.))

Si intentamos buscar unas coordenadas RA y Dec fuera de mi campo imagen voy a obtener un NaN:

[29]:
from astropy.coordinates import SkyCoord
import astropy.units as u
coords = ["1:12:43.2 +31:12:43"] #ra y dec
c = SkyCoord(coords, unit=(u.hourangle, u.deg))
c
[29]:
<SkyCoord (ICRS): (ra, dec) in deg
    [(18.18, 31.21194444)]>
[30]:
x_pixel, y_pixel = wcs.world_to_pixel(c)
x_pixel, y_pixel
[30]:
(array([nan]), array([nan]))

Generando una rejilla (grill) de RA y Dec

Podemos utilizar el WCS para definir proyecciones en Matplotlib.

[31]:
fig = plt.figure(figsize=(8, 8))
ax = fig.add_subplot(1, 1, 1, projection=wcs)
im = ax.imshow(dst, norm=norm, origin='lower')

ax.coords[0].set_ticklabel_position('l')
ax.coords[0].grid(color='white', ls='dotted')
ax.set_ylabel('RA')
ax.coords[1].set_ticklabel_position('b')
ax.coords[1].grid(color='white', ls='dotted')
ax.set_xlabel('Dec');
plt.savefig('salidas/grild_ra_dec.jpg')
_images/F-2-imagenesColor_59_0.png

Regiones

Ademas del manejo de las líneas de la cuadrícula, la clase WCSAxes se comporta como una instancia normal de Matplotlib Axes, y los métodos como imshow(), plot(), scatter(), etc., funcionarán y trazarán los datos en coordenadas de píxeles por defecto.

En el siguiente ejemplo trazaremos los marcadores y el rectángulo en coordenadas de píxeles:

[32]:
fig = plt.figure(figsize=(8, 8))
ax = fig.add_subplot(1, 1, 1, projection=wcs)
im = ax.imshow(dst, norm=norm, origin='lower')
# La siguiente línea hace que el nivel de zoom ya no cambie,
# de lo contrario Matplotlib tiene tendencia a alejar el zoom cuando se añaden superposiciones.
ax.set_autoscale_on(False)

# Añadir un rectángulo con la esquina inferior izquierda en la posición de píxel (3000, 3000) con una
# anchura y altura de 100 y 100 píxeles respectivamente.
from matplotlib.patches import Rectangle
r = Rectangle((2500., 1500.), 500., 500., edgecolor='yellow', facecolor='none')
ax.add_patch(r)

# Añade tres marcadores en (40, 30), (100, 130) y (130, 60).
#El color de la cara es un blanco transparente (0,5 es el valor alfa).
ax.scatter([2000, 0, 4000], [2000, 2000, 2000], s=100, edgecolor='white', facecolor=(1, 1, 1, 0.5))

ax.coords[0].set_ticklabel_position('l')
#ax.coords[0].grid(color='white', ls='dotted')
ax.set_ylabel('RA')
ax.coords[1].set_ticklabel_position('b')
#ax.coords[1].grid(color='white', ls='dotted')
ax.set_xlabel('Dec');
_images/F-2-imagenesColor_62_0.png

Podemos identificar regiones que queramos destacar de nuestra imagen

[33]:
fig = plt.figure(figsize=(8, 8))
ax = fig.add_subplot(1, 1, 1, projection=wcs)
im = ax.imshow(dst, norm=norm, origin='lower')
# La siguiente línea hace que el nivel de zoom ya no cambie,
# de lo contrario Matplotlib tiene tendencia a alejar el zoom cuando se añaden superposiciones.
ax.set_autoscale_on(False)

#Para el caso especial en el que el WCS representa coordenadas celestes, se pueden pasar otras entradas a get_transform(). Estas son:
ax.scatter(84.92, -69.77, transform=ax.get_transform('fk5'), s=300, edgecolor='white', facecolor='none')

ax.coords[0].set_ticklabel_position('l')
#ax.coords[0].grid(color='white', ls='dotted')
ax.set_ylabel('RA')
ax.coords[1].set_ticklabel_position('b')
#ax.coords[1].grid(color='white', ls='dotted')
ax.set_xlabel('Dec');
_images/F-2-imagenesColor_64_0.png

Contornos

El trazado de imágenes como mapas de bits o contornos debe hacerse mediante los métodos habituales de matplotlib como imshow() o contour(). Por ejemplo, continuando con el ejemplo de inicializar ejes con WCS, se puede hacer:

[34]:
import numpy as np

fig = plt.figure(figsize=(8, 8))
ax = fig.add_subplot(1, 1, 1, projection=wcs)
im = ax.imshow(g, vmin=np.min(g), vmax=np.median(g)*2, origin='lower')

# Levels:
# Determina el número y las posiciones de las curvas de nivel / regiones.
# Si es un int n, utiliza MaxNLocator, que intenta elegir automáticamente no más de n+1 niveles de contorno "agradables" entre vmin y vmax.
# Si es un array, dibuja las curvas de nivel en los niveles especificados. Los valores deben estar en orden creciente.

ax.contour(g, transform=ax.get_transform(wcs), levels= [1690,1700], colors='white')

ax.coords[0].set_ticklabel_position('l')
#ax.coords[0].grid(color='white', ls='dotted')
ax.set_ylabel('RA')
ax.coords[1].set_ticklabel_position('b')
#ax.coords[1].grid(color='white', ls='dotted')
ax.set_xlabel('Dec');

_images/F-2-imagenesColor_67_0.png

Aplicación de WCS: Alineado de imágenes

Como ejercicio podemos alinear estrellas utilizando las coordenadas WCS de nuestras imagenes H-alpha, SII y OIII (que no conseguíamos alinear correctamente antes).

Realizaremos una función en el que podamos introducir el directorio o los valores de las propias imágenes para que me devuelva las imágenes alineadas.

[35]:
import numpy as np
import matplotlib.pyplot as plt
from astropy.io import fits
from astropy.wcs import WCS
import glob
from astropy import units as u
from astropy.coordinates import SkyCoord
import math
from scipy.ndimage.interpolation import rotate

def shift_image(X, dx, dy):
    ''''
    X: imagen que queremos desplazar
    dx: cantidad de píxeles de desplazamiento en el eje x
    dy: cantidad de píxeles de desplazamiento en el eje x
    return: imagen desplazada en un ndarray
    '''
    X = np.roll(X, dy, axis=0)
    X = np.roll(X, dx, axis=1)
    if dy>0:
        X[:dy, :] = 0
    elif dy<0:
        X[dy:, :] = 0
    if dx>0:
        X[:, :dx] = 0
    elif dx<0:
        X[:, dx:] = 0
    return X

def aling_wcs(directorio):
    ''''
    directorio: donde se encuenrtan las imagenes que queremos alinear, importante solo tener las que queremos alinear.
    return: ndarray con las imagenes alineadas.
    '''
    imagenes_header = []
    imagenes_data = []
    wcs = []
    lista_imagenes = sorted(glob.glob(f"{directorio}*"))
    for imagenes in lista_imagenes:
        hdu = fits.open(imagenes)
        imagenes_header.append(hdu[0].header)
        imagenes_data.append(hdu[0].data)
        wcs.append(WCS(header=hdu[0].header))
        hdu.close()

    ra_centro_ref = imagenes_header[0]['CRVAL1']*u.degree
    dec_centro_ref = imagenes_header[0]['CRVAL2']*u.degree

    centro_imagen_ref = SkyCoord(ra_centro_ref ,dec_centro_ref , frame='icrs')

    x_centro = []
    y_centro = []
    for imagen in range(len(lista_imagenes)):
        x_centro.append(wcs[imagen].world_to_pixel(centro_imagen_ref)[0])
        y_centro.append(wcs[imagen].world_to_pixel(centro_imagen_ref)[1])
    dx = []
    dy = []
    for imagen in range(len(lista_imagenes)):
        dx.append(round(x_centro[0] - x_centro[imagen]))
        dy.append(round(y_centro[0] - y_centro[imagen]))

    imagenes_desplazadas = []
    for imagen in range(len(lista_imagenes)):
        imagenes_desplazadas.append(shift_image(imagenes_data[imagen], dx[imagen], dy[imagen]))

    sky = wcs[0].pixel_to_world(0, 0)
    x_cero = []
    y_cero = []
    for imagen in range(len(lista_imagenes)):
        x_cero.append(wcs[imagen].world_to_pixel(sky)[0])
        y_cero.append(wcs[imagen].world_to_pixel(sky)[1])
    angulo = [] # defino una lista vacia en la que voy a tener en cuenta el angulo de rotacion que hay que aplicar a cada imagen

    for i in range(len(lista_imagenes)):
        #angulo.append(math.degrees(math.atan2(y[i][1] - y[i][0], x[i][1] - x[i][0]))) # calculo el angulo entre dos puntos de la misma imagen
        angulo.append(round(math.degrees(math.atan2(y_cero[i] - y_centro[i], x_cero[i] - x_centro[i])))) # calculo el angulo entre dos puntos de la misma imagen

    imagenes_derotadas = [imagenes_desplazadas[0], ] # aquí uso la funcion rotate(imagen, angulo_de_rotacion) de la biblioteca scipy
    for i in range(1,len(lista_imagenes)):
            imagenes_derotadas.append(rotate(imagenes_desplazadas[i], angle=angulo[0]-angulo[i]))
            print(f"La imagen {i} está rotada {angulo[0]-angulo[i]} grados")
    return imagenes_desplazadas
[36]:
directorio = 'salidas/salidaCalibracion/sho/'
h, o, s = aling_wcs(directorio)
WARNING: FITSFixedWarning: RADECSYS= 'FK5 ' / Equatorial coordinate system
the RADECSYS keyword is deprecated, use RADESYSa. [astropy.wcs.wcs]
WARNING: FITSFixedWarning: 'datfix' made the change 'Set MJD-OBS to 58887.129340 from DATE-OBS'. [astropy.wcs.wcs]
WARNING: FITSFixedWarning: 'datfix' made the change 'Set MJD-OBS to 58887.162245 from DATE-OBS'. [astropy.wcs.wcs]
WARNING: FITSFixedWarning: 'datfix' made the change 'Set MJD-OBS to 58888.090220 from DATE-OBS'. [astropy.wcs.wcs]
La imagen 1 está rotada 0 grados
La imagen 2 está rotada 0 grados
[37]:
rgb_default = make_lupton_rgb(s, h, o,Q=1,stretch=3000,filename="salidas/salidaCalibracion/shoTarantula.jpg")
[38]:
plt.figure(figsize=(8, 8))
plt.imshow(rgb_default, origin='lower', norm=norm)
plt.show()
_images/F-2-imagenesColor_73_0.png
[39]:
plt.figure(figsize=(8, 8))
plt.imshow(rgb_default[2000:2500,2000:2500], origin='lower', norm=norm)
plt.show()
_images/F-2-imagenesColor_74_0.png