Python: canales y colores#

Esta sección ofrece una breve descripción general de algunas consideraciones relacionadas con el color al utilizar Python.

Consulta el scikit-image color module para obtener mucha más información útil.

# First, our usual default imports
import sys
sys.path.append('../../../')

from helpers import *
import matplotlib.pyplot as plt
import numpy as np

Imágenes RGB#

Comencemos leyendo la imagen RGB utilizada en la portada de este libro.

im = load_image('title_cells.png')

plt.imshow(im)
plt.axis(False)
plt.show()
../../../_images/41dcea345612835fcdeda2eee31f42fd3b73e5ff92bdeceb8d8c41e0d4acf3b7.png

Si comprobamos la forma de la imagen, podemos ver que la información RGB parece estar unida al final: la forma tiene el formato [filas, columnas, canales].

print(im.shape)
(600, 872, 3)

RGB a monocanal#

Podemos convertir una imagen RGB en una imagen de un solo canal de muchas maneras.

Una imagen de un solo canal a menudo se denomina escala de grises, aunque si realmente se muestra usando tonos de gris o no depende del mapa de colores.

Canales divididos#

Una opción es dividir los canales RGB para obtener tres imágenes separadas de un solo canal.

# Make a wider figure so we can fit in subplots
plt.figure(figsize=(12, 4))

# Show each of the three channels
for ii in range(3):
    
    # Subplot indices start at 1
    plt.subplot(1, 3, ii+1)
    
    # Array indices start at 0
    im_channel = im[..., ii]
    plt.imshow(im_channel, cmap='gray')
    plt.title(f'Channel {ii}')
    plt.axis(False)

plt.show()
../../../_images/7b8a497ee97627a5a643334cf9315a1ad47a0e17bf8097afd3b36918d0e6b6c6.png

Canales promediados#

Una segunda opción sencilla es simplemente calcular el promedio (media) de los valores RGB.

# Use axis=-1 to 'average along the last dimension'
# Here, I could also have used im.mean(axis=2)
im_mean = im.mean(axis=-1)
plt.imshow(im_mean, cmap='gray')
plt.axis(False)
plt.show()
../../../_images/295bb756a43fa9f4087dccbdb3714302a1c31a414c9c3edc7511042ec878ccce.png

Sin embargo, es común calcular una media ponderada de los valores RGB. Comúnmente los pesos son

\[0.2125 R + 0.7154 G + 0.0721 B\]

Esto es lo que hace rgb2gray de scikit-image, como se describe aquí.

from skimage.color import rgb2gray

im_weighted_mean = rgb2gray(im)

plt.imshow(im_weighted_mean, cmap='gray')
plt.axis(False)
plt.show()
../../../_images/4740f3874ebdcd4a9f359cef0222366a0d21973325c47b5f5dfe589305897be9.png

Para ser justos, las imágenes de media simple y media ponderada no me parecen muy diferentes.

Para comprobar si realmente son diferentes, podemos mostrar uno restado del otro.

plt.imshow(im_mean - im_weighted_mean, cmap='gray')
plt.axis(False)
plt.show()
../../../_images/1d9683c4155628d86aa47b0d5d2b12f061c83ffde49c7e6ad1d615ddf64090e4.png

Hmmmmmm, esto parece muy sospechoso. El hecho de que la imagen de diferencia luzca igual me sugiere que podría haber ocurrido algún cambio de escala.

Entonces, volvamos a verificar las estadísticas de comparación de dtype.

print(f'The mean of im is: {im.mean()} (dtype={im.dtype})')
print(f'The mean of im_mean is: {im_mean.mean()} (dtype={im_mean.dtype})')
print(f'The mean of im_weighted_mean is: {im_weighted_mean.mean()} (dtype={im_mean.dtype})')
The mean of im is: 232.17774592252803 (dtype=uint8)
The mean of im_mean is: 232.17774592252803 (dtype=float64)
The mean of im_weighted_mean is: 0.9143139910430534 (dtype=float64)

Bien, eso parece sugerir que rgb2gray también cambió la escala de nuestra imagen durante la conversión.

De hecho, es razonablemente común trabajar con imágenes RGB de 8 bits en Python (uint8) con valores en el rango 0-255, pero quizás sea aún más común trabajar con imágenes RGB de punto flotante en el rango 0-1 (ya sea float32 o float64). Por lo tanto, debemos estar atentos a las conversiones furtivas.

Con eso en mente, podemos hacer que nuestras imágenes sean comparables dividiendo nuestro im_mean por 255.

im_diff = im_mean/255.0 - im_weighted_mean
plt.imshow(im_diff, cmap='gray')
plt.axis(False)
plt.show()
../../../_images/a1cb5da94cdcf3a9276f4cea30cb10b10663f652b94da449ca3d083132d55a39.png

Esto demuestra que los valores de píxeles son al menos un poco diferentes cuando se convierten utilizando la media ponderada.

Imágenes multicanal#

Trabajar con imágenes multicanal (no RGB) en Python es extremadamente fácil en algunos aspectos y muy incómodo en otros.

Aunque debido a que la mayoría de las cosas raras ocurren al principio, dominarán el resto de esta sección.

Por ejemplo, incluso el simple hecho de abrir los datos correctamente puede ser un desafío.

Lectura de imágenes multicanal#

Me gusta usar imageio para leer imágenes por su simplicidad. Sin embargo, si confiamos en imageio.imread para un tiff multicanal, tendemos a obtener solo el primer canal.

Los capítulos posteriores mostrarán otras formas de abrir imágenes.

# The custom helper function effectively uses imageio.imread(path), 
# but has some extra logic to find the image & unzip it.
im = load_image('Rat_Hippocampal_Neuron.zip')

# This image should have 5 channels... let's see if it does
print(im.shape)
(5, 512, 512)

Podemos hacerlo mejor si cambiamos a imageio.volread para tratar la imagen como un volumen.

# This effectively uses imageio.volread(path)
im = load_image('Rat_Hippocampal_Neuron.zip', volume=True)

# Check the dimensions again
print(im.shape)
(5, 512, 512)

El problema ahora es que los canales aparecen al inicio de nuestra forma, mientras que para la imagen RGB los canales estaban al final.

Eso puede importar o no, porque Python no tiene una convención completamente fija sobre dónde deberían estar los canales.

Algunas bibliotecas asumen “canales al principio” y otras asumen “canales al final”.

La buena noticia es que, si sabes lo que se necesita, puedes mover fácilmente los canales al lugar correcto.

im_channels_first = im.copy()
im_channels_last = np.moveaxis(im, 0, -1)

print(f'Channels first shape:\t {im_channels_first.shape}')
print(f'Channels last shape:\t {im_channels_last.shape}')
Channels first shape:	 (5, 512, 512)
Channels last shape:	 (512, 512, 5)

Visualización de imágenes multicanal#

Desafortunadamente, no se puede mostrar fácilmente ninguna imagen multicanal arbitraria utilizando canales al principio o canales al final.

try:
    plt.imshow(im_channels_first)
except TypeError as err:
    print(f'I knew this wouldn\'t work! {err}')
    
try:
    plt.imshow(im_channels_last)
except TypeError as err:
    print(f'And this doesn\'t work either! {err}')
I knew this wouldn't work! Invalid shape (5, 512, 512) for image data
And this doesn't work either! Invalid shape (512, 512, 5) for image data
../../../_images/e82dfb4532c2f7c32e2f12be8654b2e638a95717fb42b9d7ed6ffc8568f1b0fa.png

Centrémonos en los canales al final, ya que es más similar a cómo se comportan las imágenes RGB.

Podemos mostrar los canales uno por uno, como en el caso RGB, o promediar los canales si queremos, aunque la media que obtengamos probablemente no sea muy significativa.

im = im_channels_last.copy()

# Make a wider figure so we can fit in subplots
plt.figure(figsize=(15, 6))

# Show each of the three channels
n_channels = im.shape[-1]
for ii in range(n_channels):
    
    # Subplot indices start at 1
    plt.subplot(1, n_channels, ii+1)
    
    # Array indices start at 0
    im_channel = im[..., ii]
    plt.imshow(im_channel)
    plt.title(f'Channel {ii}')
    plt.axis(False)

plt.show()
../../../_images/ab748637aadbef1c1c698de9574ed14e33e63dfa7e04bca12a714913002253eb.png
# Not a very meaningful mean - the pixel values in the channels mean quite different things, 
# so it doesn't make much sense to combine them in this way
plt.imshow(im.mean(axis=-1))
plt.title('Mean of channels')
plt.axis(False)
plt.show()
../../../_images/9ff79846f8b764ec7a7b6821db243a75bbe97dcdcd4316eb33286f73cdb288de.png

Añadiendo color#

Lamentablemente, actualmente no contamos con la información de color que podría usarse para mostrar cada canal. Simplemente podemos elegir un mapa de colores, como con cualquier otra imagen de un solo canal, pero no podemos visualizar fácilmente los canales fusionados (al menos con matplotlib… hasta donde yo sé).

Pero una cosa que podemos hacer es convertir cada uno de nuestros canales a RGB, usando un color fijo. Aquí hay una función auxiliar para hacer eso.

def colorize(im, color, clip_percentile=0.1):
    """
    Helper function to create an RGB image from a single-channel image using a 
    specific color.
    """
    # Check that we do just have a 2D image
    if im.ndim > 2 and im.shape[2] != 1:
        raise ValueError('This function expects a single-channel image!')
        
    # Rescale the image according to how we want to display it
    im_scaled = im.astype(np.float32) - np.percentile(im, clip_percentile)
    im_scaled = im_scaled / np.percentile(im_scaled, 100 - clip_percentile)
    im_scaled = np.clip(im_scaled, 0, 1)
    
    # Need to make sure we have a channels dimension for the multiplication to work
    im_scaled = np.atleast_3d(im_scaled)
    
    # Reshape the color (here, we assume channels last)
    color = np.asarray(color).reshape((1, 1, -1))
    return im_scaled * color
    
# The color we provide gives RGB values in order, between 0 and 1
im_red = colorize(im[..., 0], (1, 0, 1))
plt.imshow(im_red)
plt.axis(False)
plt.title('Magenta')
plt.show()

im_green = colorize(im[..., 2], (0, 1, 0))
plt.imshow(im_green)
plt.axis(False)
plt.title('Green')
plt.show()
../../../_images/d34f562df602a6161d1e108ca76e65a62a8ef8fdf02d0493a2c9c6599331772a.png ../../../_images/4bc74b268f4eefe4a0be3d50a1a51bcb97c3fc26eecf874755a0f11f440c47c4.png

Creando compuestos#

Una vez hecho esto, podemos simplemente agregar nuestras imágenes RGB para crear una imagen compuesta, recordando recortar el resultado, de modo que nos mantengamos dentro del rango 0-1.

¡Importante! Nos hemos alejado de los mapas de colores aquí para cambiar los valores de los píxeles al crear imágenes RGB. Esto nos da más control, pero debemos recordar que ninguna de estas imágenes modificadas es adecuada para el análisis cuantitativo.

im_composite = np.clip(im_red + im_green, 0, 1)
plt.imshow(im_composite)
plt.title('Composite image')
plt.axis(False)
plt.show()
../../../_images/856949382a01a6ce3b382ed26846c8f6741752904a1f4950ce3a04f858f48171.png