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!

Hide 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:

  1. Imágenes RGB: que se utilizan ampliamente para visualización, pero normalmente no son muy buenas para el análisis cuantitativo.

  2. 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:

Hide 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')
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:

Hide code cell source
show_colormap('reds', 16, 'Red LUT')
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:

Hide code cell source
show_colormap('viridis', 16, 'Viridis LUT')
# show_colormap('inferno', 16, 'Inferno LUT')
show_colormap('coolwarm', 16, 'Coolwarm LUT')
Viridis 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
Coolwarm LUT
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.

Hide 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)
../../../_images/04e4170986d08c575841c1886d980e0a82aed0e5679a0f1fb5d52ff251d1eaff.png
../../../_images/6428ff21a96091735421af9fa60f3eb93b9e47aeba786bb4116b72c9a78e2b52.png

Figura 26 Al convertir una imagen en escala de grises a RGB se puede perder información. Podemos separar los valores rojo, verde y azul de la imagen RGB y visualizar cada uno como imágenes separadas para explorar la información que contienen. Aunque la imagen RGB parece no haber cambiado con respecto a la original y los tres canales de color tienen histogramas similares a los originales, la profundidad de bits se ha reducido y las estadísticas de la imagen se han modificado. También hay un gran pico en el histograma que indica un recorte sustancial.#

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.

Hide 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)
../../../_images/44db6e9b93c484ff8f4ba48072f6c23ae2e7af7aba69aa92fabb159ad434a797.png
../../../_images/b609e4a3d4b8b705fb79da0bed331ca34f693b5855b3b1d522f1d8045c0a96dc.png

Figura 27 La conversión a RGB puede perder información de una manera particularmente dramática si la LUT no está en escala de grises. Los histogramas de cada canal ahora pueden verse completamente diferentes.#

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.

../../../_images/cmu-1-small-region.jpg

Figura 28 Ejemplo de imagen de histología RGB, de https://openslide.org#

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.

Hide 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)
../../../_images/c1167486010498b8cb5ba0f43769211c1b9af0ed079bb1c758a848366e4ace59.png

Figura 29 Imagen multicanal utilizando LUTs rojos, verdes y azules. Aunque se parece mucho a una imagen RGB, cada canal todavía contiene los datos sin procesar (que pueden ser de 16 o 32 bits). Los valores de píxeles originales se pueden extraer si es necesario y se pueden utilizar diferentes LUT.#

../../../_images/6defa7d7ae25b02e438a177c0aff1ae689cc9e5cca9c06d5e652de296185497c.png

Figura 30 Imagen multicanal de Figura 29 usando diferentes LUTs. Nuevamente, no se pierde información: podemos acceder a los valores de píxeles originales y actualizar las LUT si es necesario.#

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.

../../../_images/8dc97d5c495329c3c423e41b2b67774817056560d8cffb11c9e22602d45374fa.png

Figura 31 Podemos crear una imagen RGB desde Figura 30, pero luego tenemos tres canales bloqueados en rojo, verde y azul, que han convertido los canales originales a 8- bit, y han mezclado la información debido a los colores de LUTs utilizados. Ya no podemos recuperar los valores de píxeles originales después de convertir a RGB.#

Resumen de imágenes en color.#

El mensaje principal aquí se puede resumir en dos reglas:

Truco

  1. Utiliza siempre la imagen original para el análisis

    • Si los datos sin procesar no son RGB, ¡no los conviertas antes del análisis!

  2. 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.