Canales y colores#
Outline del capítulo
Las imágenes con múltiples canales de color pueden mostrarse de manera diferente en diferentes tipos de software
Las imágenes RGB son un caso especial y, por lo general, se ven similares en distintos programas.
En ImageJ, las imágenes multicanal que no son RGB pueden denominarse imágenes compuestas
¡Al convertir imágenes a RGB a menudo se pierde información!
Show code cell content
%load_ext autoreload
%autoreload 2
# Default imports
import sys
sys.path.append('../../../')
from helpers import *
from matplotlib import pyplot as plt
from myst_nb import glue
import numpy as np
from scipy import ndimage
Introducción#
Una forma de introducir color en las imágenes es utilizar una LUT adecuada, como se describe en Imágenes y píxeles. Entonces, el hecho de que en la visualización de tales imágenes pudieran intervenir diferentes colores era en realidad sólo incidental: en cada lugar de la imagen todavía había sólo un canal, un píxel y un valor.
Hay imágenes en las que el color juega un papel más importante. Consideraremos dos tipos:
Imágenes RGB: que se utilizan ampliamente para visualización, pero normalmente no son muy buenas para el análisis cuantitativo.
Imágenes multicanal/compuestas: a menudo son mejores para el análisis, pero deben convertirse a RGB para su visualización.
Dado que a menudo lucen iguales, pero se comportan de manera muy diferente, saber qué tipo de imagen a color tienes es importante para cualquier trabajo científico.
Mezclando rojo, verde y azul#
Anteriormente analizamos cómo las LUTs de imágenes proporcionan una forma de asignar valores de píxeles a colores que se pueden mostrar en la pantalla. Ahora que hemos analizado los tipos de imágenes y las profundidades de bits, podemos ampliar un poco más cómo funciona en la práctica.
En general, cada color se representa mediante tres enteros sin signo de 8 bits: uno para red (rojo), uno para green (verde) y uno para blue (azul). Cada valor entero define qué cantidad de cada color primario se debe mezclar para crear el color final utilizado para mostrar el píxel.
En el caso de una LUT en escala de grises, los valores de rojo, verde y azul son todos iguales:
Show code cell source
import pandas as pd
import numpy as np
from matplotlib import colormaps
from matplotlib.colors import LinearSegmentedColormap
def colormap_style(s):
"""
Style each row of the LUT.
"""
return [#f"font-weight: bold",
f"color: rgba({s['Red']}, 0, 0)",
f"color: rgba(0, {s['Green']}, 0)",
f"color: rgba(0, 0, {s['Blue']})",
f"background-color: rgba({s['Red']}, {s['Green']}, {s['Blue']})"]
def show_colormap(name: str, n_colors: int, caption = None):
"""
Display colormap in a pandas dataframe.
"""
# Select a colormap & convert it to 8-bit
cmap = colormaps[name]
color_inds = np.linspace(0, 255, n_colors).astype(np.uint32)
colors = np.vstack([np.asarray(cmap(ind)[:3]) for ind in color_inds])
colors = (colors * 255).astype(np.uint32)
# Create a table to display the colormap
df = pd.DataFrame(colors, columns=('Red', 'Green', 'Blue'))
df['Color'] = ''
d = dict(selector="th",
props=[('text-align', 'center')])
s = df.style.set_properties(**{'width':'8em', 'font-size':'80%', 'text-align':'center'})\
.set_table_styles([d])\
.hide(axis='index')\
.apply(colormap_style, axis=1)
if caption:
s = s.set_caption(caption)
display(s)
# Create standard red, green & blue colormaps
cm_reds = LinearSegmentedColormap.from_list('reds', [(0,0,0), (1,0,0)])
colormaps.register(cm_reds)
cm_greens = LinearSegmentedColormap.from_list('greens', [(0,0,0), (0,1,0)])
colormaps.register(cm_greens)
cm_blues = LinearSegmentedColormap.from_list('blues', [(0,0,0), (0,0,1)])
colormaps.register(cm_blues)
# Create additional colormaps
cm_cyans = LinearSegmentedColormap.from_list('cyans', [(0,0,0), (0,1,1)])
colormaps.register(cm_cyans)
cm_yellows = LinearSegmentedColormap.from_list('yellows', [(0,0,0), (1,1,0)])
colormaps.register(cm_yellows)
cm_magentas = LinearSegmentedColormap.from_list('magentas', [(0,0,0), (1,0,1)])
colormaps.register(cm_magentas)
show_colormap('gray', 16, 'Grayscale LUT')
Red | Green | Blue | Color |
---|---|---|---|
0 | 0 | 0 | |
17 | 17 | 17 | |
34 | 34 | 34 | |
51 | 51 | 51 | |
68 | 68 | 68 | |
85 | 85 | 85 | |
102 | 102 | 102 | |
119 | 119 | 119 | |
136 | 136 | 136 | |
153 | 153 | 153 | |
170 | 170 | 170 | |
187 | 187 | 187 | |
204 | 204 | 204 | |
221 | 221 | 221 | |
238 | 238 | 238 | |
255 | 255 | 255 |
Otros LUTs pueden incluir solo un color, y los demás se establecen en cero:
Show code cell source
show_colormap('reds', 16, 'Red LUT')
Red | Green | Blue | Color |
---|---|---|---|
0 | 0 | 0 | |
17 | 0 | 0 | |
34 | 0 | 0 | |
51 | 0 | 0 | |
68 | 0 | 0 | |
85 | 0 | 0 | |
102 | 0 | 0 | |
119 | 0 | 0 | |
136 | 0 | 0 | |
153 | 0 | 0 | |
170 | 0 | 0 | |
187 | 0 | 0 | |
204 | 0 | 0 | |
221 | 0 | 0 | |
238 | 0 | 0 | |
255 | 0 | 0 |
Sin embargo, para la mayoría de los LUTs los valores de rojo, verde y azul difieren:
Show code cell source
show_colormap('viridis', 16, 'Viridis LUT')
# show_colormap('inferno', 16, 'Inferno LUT')
show_colormap('coolwarm', 16, 'Coolwarm LUT')
Red | Green | Blue | Color |
---|---|---|---|
68 | 1 | 84 | |
72 | 25 | 107 | |
70 | 47 | 124 | |
64 | 67 | 135 | |
56 | 86 | 139 | |
48 | 103 | 141 | |
41 | 120 | 142 | |
35 | 136 | 141 | |
30 | 152 | 138 | |
34 | 167 | 132 | |
53 | 183 | 120 | |
83 | 197 | 103 | |
121 | 209 | 81 | |
165 | 218 | 53 | |
210 | 225 | 27 | |
253 | 231 | 36 |
Red | Green | Blue | Color |
---|---|---|---|
58 | 76 | 192 | |
78 | 105 | 216 | |
100 | 133 | 235 | |
123 | 158 | 248 | |
146 | 180 | 254 | |
170 | 198 | 253 | |
192 | 211 | 245 | |
211 | 219 | 230 | |
229 | 216 | 208 | |
241 | 202 | 182 | |
246 | 183 | 156 | |
245 | 160 | 129 | |
237 | 132 | 103 | |
223 | 100 | 79 | |
204 | 63 | 57 | |
179 | 3 | 38 |
Debido a que cada uno de los valores de rojo, verde y azul puede estar en el rango de 0 a 255, mezclarlos puede generar (al menos en teoría) hasta 256 x 256 x 256 = 16,777,216 colores diferentes, es decir, un lote.
Cuando se trata de visualización, este método de representar el color utilizando valores RGB de 8 bits debería darnos fácilmente muchos más colores de los que podríamos esperar distinguir a simple vista. No necesitamos una mayor profundidad de bits para la visualización.
Imágenes RGB#
Hasta ahora hemos considerado imágenes donde cada píxel tiene un valor único y hay una LUT asociada con la imagen para asignar estos valores a colores.
Ahora que sabemos cómo se representan los colores, podemos considerar otra opción.
En lugar de almacenar un único valor por píxel, podemos almacenar los valores RGB que representan el color utilizado para mostrar el píxel. Cada píxel tiene entonces tres valores (para rojo, verde y azul), no solo un valor único.
Cuando una imagen se almacena de esta manera, se denomina imagen RGB.
Podemos crear fácilmente una imagen RGB a partir de cualquier combinación de imagen + LUT: basta con sustituir cada valor de píxel de la imagen original por los valores RGB asociados que encontremos en la LUT. Ahora cada píxel tiene tres valores en lugar de uno, pero el resultado final parece exactamente igual.
El riesgo de RGB#
El problema con la conversión de una imagen a RGB es que, en general, ¡no podemos volver atrás! De hecho, el uso excesivo involuntario de imágenes RGB es una de las fuentes más comunes de errores de destrucción de datos en algunas ramas de imagenologia científica.
¡Cuidado con la conversión a RGB!
Convertir una imagen a RGB es otra forma de perder nuestros datos sin procesar.
Figura 26 muestra esto en acción. En el caso «menos destructivo», la imagen tiene una LUT en escala de grises. Esto significa que los valores de rojo, verde y azul son idénticos entre sí, pero no necesariamente idénticos a los valores de píxeles de la imagen original. Convertimos los datos a 8 bits y utilizamos la LUT para determinar cuánto escalar durante la conversión.
En general, no es posible recuperar los valores de píxeles originales de la imagen RGB: probablemente no sepamos exactamente qué cambio de escala se aplicó y hemos perdido información al recortar y redondear.
Show code cell content
# Show an image with a colormap, and its histogram (unchanged by the colormap)
from matplotlib import colormaps
im = load_image('sunny_cell.tif')
bins = 128
color_map = colormaps['gray']
fig = create_figure(figsize=(6, 3))
vmax = np.percentile(im, 99)
vmin = np.percentile(im, 1)
show_image(im, vmin=vmin, vmax=vmax, pos=121, cmap=color_map)
show_histogram(im, pos=122, stats='auto', bins=bins)
plt.suptitle('Original image with LUT')
plt.tight_layout()
glue_fig('fig_colors_im_grays', fig)
# Convert to 8-bit RGB by applying the colormap (some rescaling needed to handle type/bit-depth)
im_rgb = color_map((im.astype(np.float32) - vmin)/(vmax - vmin))[..., :3]
im_rgb = np.clip(im_rgb * 255, 0, 255).astype(np.uint8)
# Show RGB image and histograms - very different from the originals
# (There isn't enough room to add counts, so we remove the yticks)
fig = create_figure(figsize=(8, 4))
bins = np.arange(0, 256)
show_image(im_rgb, pos=241, cmap=color_map, title='Image converted to RGB')
show_histogram(im_rgb, pos=245, stats='right', bins=bins)
plt.yticks([])
show_image(im_rgb[...,0], vmin=0, vmax=255, pos=242, cmap='reds', title='RGB Red channel')
show_image(im_rgb[...,1], vmin=0, vmax=255, pos=243, cmap='greens', title='RGB Green channel')
show_image(im_rgb[...,2], vmin=0, vmax=255, pos=244, cmap='blues', title='RGB Blue channel')
show_histogram(im_rgb[...,0], pos=246, stats='right', bins=bins, facecolor='red')
plt.yticks([])
plt.ylabel('')
show_histogram(im_rgb[...,1], pos=247, stats='right', bins=bins, facecolor='green')
plt.yticks([])
plt.ylabel('')
show_histogram(im_rgb[...,2], pos=248, stats='right', bins=bins, facecolor='blue')
plt.yticks([])
plt.ylabel('')
plt.tight_layout()
glue_fig('fig_colors_im_grays_rgb', fig)
El impacto de convertir una imagen con cualquier otra LUT a RGB es aún más dramático, como se muestra en Figura 27. Aquí, los valores de rojo, verde y azul son diferentes y los histogramas de cada color son muy diferentes. Nuevamente, no sería posible recuperar los valores de píxeles originales de la imagen RGB.
Show code cell content
# Show an image with a colormap, and its histogram (unchanged by the colormap)
from matplotlib import colormaps
im = load_image('sunny_cell.tif')
bins = 128
color_map = colormaps['viridis']
fig = create_figure(figsize=(6, 3))
vmax = np.percentile(im, 99)
vmin = np.percentile(im, 1)
show_image(im, vmin=vmin, vmax=vmax, pos=121, cmap=color_map)
show_histogram(im, pos=122, stats='auto', bins=bins)
plt.suptitle('Original image with LUT')
plt.tight_layout()
glue_fig('fig_colors_im_colormap', fig)
# Convert to 8-bit RGB by applying the colormap (some rescaling needed to handle type/bit-depth)
im_rgb = color_map((im.astype(np.float32) - vmin)/(vmax - vmin))[..., :3]
im_rgb = np.clip(im_rgb * 255, 0, 255).astype(np.uint8)
# Show RGB image and histograms - very different from the originals
# (There isn't enough room to add counts, so we remove the yticks)
fig = create_figure(figsize=(8, 4))
bins = np.arange(0, 256)
show_image(im_rgb, pos=241, cmap=color_map, title='Image converted to RGB')
show_histogram(im_rgb, pos=245, stats='right', bins=bins)
plt.yticks([])
bins = np.arange(0, 256, 2)
show_image(im_rgb[...,0], vmin=0, vmax=255, pos=242, cmap='reds', title='RGB Red channel')
show_image(im_rgb[...,1], vmin=0, vmax=255, pos=243, cmap='greens', title='RGB Green channel')
show_image(im_rgb[...,2], vmin=0, vmax=255, pos=244, cmap='blues', title='RGB Blue channel')
show_histogram(im_rgb[...,0], pos=246, stats='right', bins=bins, facecolor='red')
plt.yticks([])
plt.ylabel('')
show_histogram(im_rgb[...,1], pos=247, stats='right', bins=bins, facecolor='green')
plt.yticks([])
plt.ylabel('')
show_histogram(im_rgb[...,2], pos=248, stats='right', bins=bins, facecolor='blue')
plt.yticks([])
plt.ylabel('')
plt.tight_layout()
glue_fig('fig_colors_im_rgb', fig)
El papel del RGB#
Uso de imágenes RGB para visualización#
Entonces, ¿qué sentido tiene tener imágenes RGB, si son tan riesgosas?
Una de las principales razones para utilizar imágenes RGB en ciencia es la presentación. Si bien las aplicaciones de software de análisis de imágenes especializadas, como ImageJ, generalmente están diseñadas para manejar una variedad de tipos de imágenes y profundidades de bits exóticos, no ocurre lo mismo con el software no científico.
If you want an image to display exactly the same way in ImageJ as in a PowerPoint® presentation or a figure in a publication, for example, we’ll probably want to convert it to RGB. If we don’t, the image might display very strangely on other software – or even not open at all.
“¿Por qué mi imagen es simplemente negra?”
A lo largo de los años, me he encontrado con un número notable de casos en los que un investigador guardó sus imágenes de microscopía de fluorescencia sólo en formato RGB.
Su justificación fue que intentaron guardar las imágenes de otra manera en el microscopio, pero «no funcionó: todas las imágenes eran negras».
La explicación es casi invariablemente que sus imágenes eran en realidad de 16 o 32 bits, pero intentaron abrirlas en un software que no maneja muy bien imágenes de 16 bits (por ejemplo, simplemente hicieron doble clic en el archivo para abrirlo en el visor de imágenes predeterminado). Todo lo que vieron fue una imagen negra y aparentemente vacía.
Cada vez que intentaron exportar desde el software de adquisición del microscopio de diferentes maneras, encontraron una opción que ofrecía una imagen visible, y se quedaron con ella.
El problema con esto es que generalmente significa que ¡no guardaron sus datos originales, sin procesar, en absoluto! Solo guardaron una copia RGB, con todo el cambio de escala y la magia LUT aplicada, lo cual es totalmente inadecuado para el análisis.
La solución es ver imágenes en ImageJ o software científico similar. Esto normalmente revela que la imagen no es «completamente negra» después de todo. Más bien, sólo es necesario ajustar el brillo y el contraste (usando el LUT) para ver los datos sin procesar en todo su esplendor.
Cuando RGB es todo lo que tienes#
Todos los comentarios anteriores sobre “no convertir a RGB antes del análisis” se basan en el supuesto de que sus datos sin procesar aún no son RGB. Este suele ser el caso de la microscopía y las imágenes médicas siempre que es importante una cuantificación precisa.
Sin embargo, no siempre es así.
Un ejemplo común son las imágenes de campo claro para histología o patología. Aquí, la cámara suele ser RGB y una imagen RGB es realmente lo más cercana a los datos sin procesar que es probable que obtengamos.
Fundamentalmente, el análisis de imágenes de campo claro en histología generalmente tiene como objetivo replicar (y a veces mejorar) la evaluación visual que un patólogo podría hacer mirando por un microscopio. A menudo se basa en detectar, clasificar y contar células, medir áreas teñidas o reconocer la presencia de patrones particulares, pero no cuantificar con precisión la intensidad de la tinción.
Imágenes multicanal#
Hasta ahora, nos hemos centrado en imágenes 2D con un canal único, es decir, un valor único para cada píxel en cada coordenada x,y de la imagen.
Estas imágenes se pueden convertir a RGB de 8 bits mediante una LUT. Si hacemos esto, obtenemos una imagen con tres canales, donde cada canal se muestra usando LUT rojo, verde y azul, con los colores combinados para su visualización. Pero no deberíamos hacer esa conversión antes del análisis en caso de que perdamos nuestros datos sin procesar.
Ahora, pasemos a considerar imágenes multicanal que no son imágenes RGB. Más bien, los datos sin procesar tienen múltiples canales.
En microscopía de fluorescencia, es común adquirir imágenes multicanal en las que los valores de píxeles de cada canal se determinan a partir de luz filtrada según su longitud de onda. Podríamos optar por visualizar estos canales como rojo, verde y azul, pero no es necesario.
En principio, se podría aplicar cualquier LUT a cada canal, pero tiene sentido elegir LUTs que de alguna manera se relacionen con la longitud de onda (es decir, el color) de la luz detectada para los canales correspondientes. Luego, los canales se pueden superponer uno encima del otro y sus colores se pueden fusionar para su visualización (por ejemplo, los valores altos en los canales verde y rojo se muestran en amarillo).
La característica importante de estas imágenes es que siempre se conserva la información real del canal y, por tanto, los valores de píxeles originales permanecen disponibles. Esto significa que aún podemos extraer canales o ajustar sus LUT según sea necesario.
Show code cell content
im = load_image('FluorescentCells.zip', volume=True)
if im.shape[0] == 3:
im = np.moveaxis(im, 0, -1)
im = im[:400, :400, ...]
fig = create_figure(figsize=(12, 4))
im_merged_rgb = create_rgb(im, ('red', 'green', 'blue'))
show_image(im_merged_rgb, title='Merged channels', pos=141)
show_image(im[..., 0], cmap='reds', title='Channel 1', pos=142)
show_image(im[..., 1], cmap='greens', title='Channel 2', pos=143)
show_image(im[..., 2], cmap='blues', title='Channel 3', pos=144)
glue_fig('fig_colors_composite_rgb', fig)
im_merged = create_rgb(im, ('magenta', 'yellow', 'cyan'))
fig = create_figure(figsize=(12, 4))
show_image(im_merged, title='Merged channels', pos=141)
show_image(im[..., 0], cmap='magentas', title='Channel 1', pos=142)
show_image(im[..., 1], cmap='yellows', title='Channel 2', pos=143)
show_image(im[..., 2], cmap='cyans', title='Channel 3', pos=144)
glue_fig('fig_colors_composite_non_rgb', fig)
fig = create_figure(figsize=(12, 4))
show_image(im_merged, title='RGB of merged channels', pos=141)
show_image(im_merged[..., 0], cmap='reds', title='Red', pos=142)
show_image(im_merged[..., 1], cmap='greens', title='Green', pos=143)
show_image(im_merged[..., 2], cmap='blues', title='Blue', pos=144)
glue_fig('fig_colors_composite_rgb_split', fig)
Al igual que con una imagen de un solo canal, podemos crear una imagen RGB que nos permita visualizar nuestra imagen multicanal, usando los LUTs para determinar qué valores RGB son necesarios para representar el color de cada píxel.
Luego, al igual que con la imagen de un solo canal, esto es problemático si no conservamos los datos sin procesar, porque nunca podremos recuperar los valores originales de la representación RGB.
Resumen de imágenes en color.#
El mensaje principal aquí se puede resumir en dos reglas:
Truco
Utiliza siempre la imagen original para el análisis
Si los datos sin procesar no son RGB, ¡no los conviertas antes del análisis!
Crea una copia RGB de tu imagen para mostrarla
Manten la copia RGB separada, para conservarla siempre y la imagen sin procesar
Las imágenes RGB no son malas: casi siempre las necesitamos para mostrarlas, y para algunas aplicaciones de imágenes (por ejemplo, histología de campo claro) son los mejores datos sin procesar que podemos obtener. Pero debemos tener cuidado si nuestros datos sin procesar no son RGB y evitar convertirlos a RGB demasiado pronto.
Al final, es normal conservar al menos dos versiones de cada conjunto de datos: una en el formato original (posiblemente multicanal) y otra como RGB para visualización. Esta imagen RGB normalmente se crea como el paso final, después de aplicar cualquier procesamiento o ajuste LUT a los datos originales.
Otros espacios de color
El color es un gran tema y hay mucho más que podría decirse sobre los diferentes espacios de color y transformaciones. Sin embargo, estos son más relevantes cuando se trabaja con datos que originalmente son RGB.
Por ejemplo, podríamos convertir una imagen RGB a una representación HSB, donde HSB significa Tono (Hue), Saturación (Saturation) y Brillo (Brightness). Esto es útil para separar el tono del brillo, p.ej. para ayudar a identificar todos los píxeles rojos independientemente de si son brillantes u oscuros.
Alternativamente, podríamos convertir una imagen RGB a CMYK, que significa cian (Cyan), magenta (Magenta), amarillo (Yellow) y negro (blacK), lo que puede ser más adecuado para impresoras que para monitores.
Pero personalmente no me parece que tales transformaciones sean muy relevantes para las áreas del análisis de bioimágenes en las que he trabajado. He tratado de centrarme aquí en los principales temas que se necesitan conocer, que impactan el análisis de imágenes científicas. Con esto en mente, creo que comprender RGB (y sus limitaciones) es crucial, mientras que otras transformaciones se pueden retomar más adelante si son necesarias.