ImageJ: Transformada de imágenes#
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#
ImageJ dispone de excelentes transformadas de distancia 2D y watershed… aunque no todo en ellas es necesariamente lo que cabría esperar.
Transformada de distancia#
Se puede calcular una transformada de distancia 2D en ImageJ utilizando el comando
.Una característica no obvia de esto es que el tipo de output dado está determinado por la opción EDM output escondida en (donde EDM significa “Euclidean Distance Map”). Esto supone una diferencia, porque la distancia entre dos píxeles diagonales se considera \(\sqrt{2} \approx 1.414\) (por el teorema de Pitágoras) – lo que significa que una salida de 32 bits puede dar distancias en línea recta más exactas sin redondear.
Show code cell content
fig = create_figure(figsize=(8, 4))
show_image('images/imagej-blobs-binary.png', title="(A) Binary image", pos=141)
show_image('images/imagej-blobs-distance.png', title="(B) Distance Map", pos=142)
show_image('images/imagej-blobs-watershed.png', title="(C) Watershed", pos=143)
show_image('images/imagej-blobs-voronoi.png', title="(D) Voronoi", pos=144)
glue_fig('fig_distance_commands_imagej', fig)
Puntos máximos#
es un comando relacionado. Utiliza la transformada de distancia para identificar los últimos puntos que se eliminarían si los objetos se erosionaran hasta desaparecer. En otras palabras, identifica los centros. Pero no se trata simplemente de puntos centrales únicos para cada objeto, sino que son puntos máximos en el mapa de distancias y, por tanto, los píxeles más alejados del límite.
Esto significa que si una estructura tiene varias «protuberancias», entonces existe un punto final en el centro de cada una de ellas. Si la segmentación ha dado lugar a la fusión de estructuras, cada protuberancia distinta podría corresponder a algo interesante, y el número de protuberancias significa más que el número de objetos separados en la imagen binaria.
Show code cell content
"""
Rather than simply paste screenshots from ImageJ, here I've tried to use
Python/skimage/scipy to replicate the steps taken within ImageJ.
The results aren't guaranteed to be identical, but should be close -
and I hope more useful for learning.
"""
def read_blobs_and_threshold():
"""
Read the blobs.gif image & convert to binary in a 'standardized' way,
for consistency across figures.
"""
# Load & invert blobs (this compensates for the inverted LUT)
im_blobs = 255 - load_image('blobs.gif')
# Threshold blobs - we don't need to be sophisticated,
# almost any half-sensible threshold method should work
bw_blobs = im_blobs > im_blobs.mean()
return im_blobs, bw_blobs
def approx_ultimate_points(bw, bw_dist=None):
"""
Find the ultimate points from a distance map of a binary image.
These are local maxima that are non-zero in the binary image.
This *sounds* easy: check where pixel values match the result of applying a maximum filter,
and those are your maxima.
However, in practice this can detect lots of spurious peaks / saddle-points.
ImageJ's 'MaximumFinder' does *a lot* or work, and so generally ImageJ will give a
reasonable result without the user needing to know what exactly happened.
To approximate the results here, we can use h-maxima (maxima greater than a fixed threshold)
with a small dilation to merge maxima that are nearby.
Much as I'd like to eliminate this step & just use an ImageJ screenshot,
I've included it to admit the awkwardness of image processing - often methods
need to be adapted to overcome seemingly minor issues.
Returns (y, x) tuple of coordinates.
"""
if bw_dist is None:
bw_dist = ndimage.distance_transform_edt(bw)
from skimage.morphology import extrema
bw_maxima = extrema.h_maxima(bw_dist, 0.5)
bw_maxima = ndimage.binary_dilation(bw_maxima)
bw_ultimate = np.bitwise_and(bw, bw_maxima)
# Reduce all ultimate point regions to a single point
# (Here, we take the centroids - this wouldn't be robust for weird shapes with holes,
# but should be ok here)
lab_ultimate, n = ndimage.label(bw_ultimate)
com = ndimage.measurements.center_of_mass(lab_ultimate, lab_ultimate, index=range(1, n+1))
return zip(*com)
# Load images
im_blobs, bw_blobs = read_blobs_and_threshold()
# Create distance transform image
bw_dist = ndimage.distance_transform_edt(bw_blobs)
# Get ultimate points
y, x = approx_ultimate_points(bw_blobs, bw_dist)
# Show results
fig = create_figure(figsize=(8, 4))
show_image(im_blobs, title="(A) Original image", pos=141)
show_image(bw_blobs, title="(B) Thresholded image", pos=142)
show_image(bw_dist, title="(C) Distance transform", pos=143)
show_image(im_blobs, title="(D) Ultimate points", pos=144)
plt.plot(x, y, marker='o', color='r', markersize=2, markeredgewidth=0.5, markerfacecolor='none', linewidth=0)
glue_fig('fig_distance_transform_python', fig)
Prudencia
Aunque conceptualmente sencillo, y fácil de usar en ImageJ, la implementación de “Ultimate points” en otro software puede ser complicado. Aquí he intentado replicarlo en Python. Los resultados no son necesariamente idénticos a la implementación de ImageJ, pero deberían ser bastante parecidos.
Selecciona Show code cell contents arriba para ver cómo funciona.
Watershed (tras la transformada de distancia)#
Como su nombre indica, el comando Process --> Binary --> Watershed
de ImageJ {menuselection} aplica una transformada de watershed.
Sin embargo, como su nombre lo dice, la transformada de watershed se aplica siempre a un mapa de distancias, que se calcula automáticamente en segundo plano.
La única pista es que aparece en el submenú Process --> Binary
de {menuselection} y, por lo tanto, requiere una imagen binaria como entrada; una transformada de watershed normal
no se aplica normalmente a una imagen binaria.
En efecto, las semillas de la transformada de watershed son los «puntos de interés» descritos anteriormente. El efecto del comando es, por tanto, dividir los objetos «redondeados».
Observar la transformada de distancia
Si haces click en el botón de la barra de herramientas Dev y seleccionas en el menú desplegable, al ejecutar se generará una pila de imágenes que mostrara cómo se expandieron las semillas durante el procesamiento del watershed.
Voronoi#
es otro comando basado en distancias y watershed para imágenes binarias.
Hara la partición en la imagen en distintas regiones, de modo que las líneas de separación tengan la misma distancia a los objetos en primer plano más cercanos.
Imagina que has creado una imagen binaria que contiene células detectadas, pero sólo te interesa la región del interior de la célula que está cerca de la membrana, es decir, dentro de los 5 píxeles del borde de cada objeto detectado. Los píxeles situados fuera de los objetos o más cerca de sus centros no importan.
¿Cómo encontrarías estas regiones utilizando ImageJ y la transformación de distancia?
Nota: Hay otras formas de hacerlo utilizando las técnicas que hemos comentado, aunque no necesariamente dan resultados idénticos.
Este es el enfoque en el que estaba pensando:
Ejecutar
Ejecutar
Ejecutar
Elige Set e introduce Lower Threshold Level: 1 y Higher Threshold Level: 5.
Hay más formas posibles, como aplicar un filtro máximo y restar la imagen binaria original – pero creo que la transformada de distancia es más elegante. La transformada de distancia también es probable que sea un proceso mucho más rápido para grandes distancias, y más preciso (suponiendo que utilices un output de 32 bits).
Transformada de watershed#
Entonces, si ImageJ tiene una transformada de watershed, pero no es el comando llamado
, ¿dónde está?La respuesta es que está oculto en el fenomenalmente útil comando
.Digo “oculto”, porque tienes que elegir específicamente el output Segmented Particles para usarlo. Y tendrás que cambiar tus expectativas: a diferencia de la mayoría de las transformadas de watershed, empezará en los picos de intensidad de la imagen (es decir, los máximos) y se expandirá hacia fuera, en lugar de empezar en los mínimos.
Pero no dejes que esas cosas te desanimen: Recomiendo encarecidamente explorar las distintas opciones de
para ver todo lo que puede hacer. Dependiendo de las opciones seleccionadas, esto incluye encontrar estructuras usando un umbral global, un umbral local, generando ROIs de puntos y generando regiones binarias.Echa un vistazo a MorphoLibJ
Haciendo eco de mi recomendación al final del último capítulo, deberías echar un vistazo a MorphoLibJ si quieres más opciones de transformadas – particularmente cuando se trata de watersheds de varios tipos.
Consulta https://imagej.net/plugins/morpholibj para más detalles.