Segmentation d’image 3D#

La segmentation d’image en 3D est difficile pour plusieurs raisons : Dans de nombreuses techniques d’imagerie microscopique, la qualité de l’image varie dans l’espace : Par exemple, l’intensité et/ou le contraste se dégradent plus on image profondément à l’intérieur d’un échantillon. De plus, les noyaux en contact sont difficiles à différencier de manière automatisée. Enfin, l’anisotropie est difficile à gérer en fonction des algorithmes appliqués et des paramètres respectifs donnés. Certains algorithmes, comme l’approche Voronoi-Otsu-Labeling démontrée ici, ne fonctionnent que pour des données isotropes.

from skimage.io import imread
from pyclesperanto_prototype import imshow
import pyclesperanto_prototype as cle
import matplotlib.pyplot as plt

import napari
from napari.utils import nbscreenshot

# For 3D processing, powerful graphics
# processing units might be necessary
cle.select_device('TX')
<NVIDIA GeForce GTX 1650 with Max-Q Design on Platform: NVIDIA CUDA (1 refs)>

Pour démontrer le flux de travail, nous utilisons des données d’image recadrées et rééchantillonnées du Broad Bio Image Challenge : Ljosa V, Sokolnicki KL, Carpenter AE (2012). Annotated high-throughput microscopy image sets for validation. Nature Methods 9(7):637 / doi. PMID: 22743765 PMCID: PMC3627348. Disponible sur http://dx.doi.org/10.1038/nmeth.2083

input_image = imread("../../data/BMP4blastocystC3-cropped_resampled_8bit.tif")

voxel_size_x = 0.202
voxel_size_y = 0.202
voxel_size_z = 1

À des fins de visualisation, nous montrons les projections d’intensité le long de X, Y et Z.

def show(image_to_show, labels=False):
    """
    Cette fonction génère trois projections : dans les directions X, Y et Z et les affiche.
    """
    projection_x = cle.maximum_x_projection(image_to_show)
    projection_y = cle.maximum_y_projection(image_to_show)
    projection_z = cle.maximum_z_projection(image_to_show)

    fig, axs = plt.subplots(1, 3, figsize=(15, 15))
    cle.imshow(projection_x, plot=axs[0], labels=labels)
    cle.imshow(projection_y, plot=axs[1], labels=labels)
    cle.imshow(projection_z, plot=axs[2], labels=labels)
    plt.show()

show(input_image)
print(input_image.shape)
../_images/0a2cddd15382c1c2c1470813f4265e0c6f9f480dcfa1902ae24c92caaa8daade.png
(86, 396, 393)

Évidemment, la taille des voxels n’est pas isotrope. Ainsi, nous mettons à l’échelle l’image en utilisant la taille des voxels comme facteur d’échelle pour obtenir une pile d’images avec des voxels isotropes.

resampled = cle.scale(input_image, factor_x=voxel_size_x, factor_y=voxel_size_y, factor_z=voxel_size_z, auto_size=True)

show(resampled)
print(resampled.shape)
../_images/58f72b6777f9ce1e481d6de34b7562d1febc7092946fe83d283cc4e8cefa120a.png
(86, 79, 79)

Correction d’intensité et de fond#

Comme nous pouvons le voir, l’intensité diminue dans la direction Z (de coupe en coupe) et le contraste également. Au moins la décroissance d’intensité peut être corrigée. Dans CLIJx, cette méthode est connue sous le nom de equalize_mean_intensities_of_slices.

equalized_intensities_stack = cle.create_like(resampled)
a_slice = cle.create([resampled.shape[1], resampled.shape[0]])

num_slices = resampled.shape[0]
mean_intensity_stack = cle.mean_of_all_pixels(resampled)

corrected_slice = None
for z in range(0, num_slices):
    # extraire une seule coupe de la pile
    cle.copy_slice(resampled, a_slice, z)
    # mesurer son intensité
    mean_intensity_slice = cle.mean_of_all_pixels(a_slice)
    # corriger l'intensité
    correction_factor = mean_intensity_slice/mean_intensity_stack
    corrected_slice = cle.multiply_image_and_scalar(a_slice, corrected_slice, correction_factor)
    # copier la coupe dans une pile
    cle.copy_slice(corrected_slice, equalized_intensities_stack, z)

show(equalized_intensities_stack)
../_images/0ff9ddcbd0db72ddd57b31a905ef3a00f68c9171121a0835e7dbc05d91fc02f4.png

De plus, l’intensité de fond semble augmenter, potentiellement à cause d’une diffusion plus importante en profondeur dans l’échantillon. Nous pouvons compenser cela en utilisant une technique de soustraction de fond :

backgrund_subtracted = cle.top_hat_box(equalized_intensities_stack, radius_x=5, radius_y=5, radius_z=5)
show(backgrund_subtracted)
../_images/00a8a05d6025d67a555f336b09aaaeeef8c4b66680392c49584aff62462a10da.png

Segmentation#

segmented = cle.voronoi_otsu_labeling(backgrund_subtracted, spot_sigma=3, outline_sigma=1)
show(segmented, labels=True)
../_images/064562d799ff01029b225d87b88e6f9983e7834cc72ad7e532ece7f9dae09b57.png

Comme les résultats de segmentation sont difficiles à inspecter en 3D, nous générons une pile d’images avec les intensités originales + les contours de la segmentation. Nous montrons cette pile pour quelques coupes.

a_slice = cle.create([resampled.shape[1], resampled.shape[0]])
segmented_slice = cle.create([resampled.shape[1], resampled.shape[0]])

for z in range(0, resampled.shape[2], 20):
    label_outlines = None
    combined = None

    # obtenir une seule coupe de l'image d'intensité et de l'image d'étiquettes segmentée
    cle.copy_slice(resampled, a_slice, z)
    cle.copy_slice(segmented, segmented_slice, z)

    # déterminer les contours autour des objets étiquetés
    label_outlines = cle.detect_label_edges(segmented_slice, label_outlines)

    # combiner les deux images
    outline_intensity_factor = cle.maximum_of_all_pixels(a_slice)
    combined = cle.add_images_weighted(a_slice, label_outlines, combined, 1.0, outline_intensity_factor)

    # visualisation
    fig, axs = plt.subplots(1, 3, figsize=(15, 15))
    cle.imshow(a_slice, plot=axs[0])
    cle.imshow(segmented_slice, plot=axs[1], labels=True)
    cle.imshow(combined, plot=axs[2])
../_images/ea0964200ca75e5cf1e5d7696169a7f6cd01d81474357fa098c3606a5e047b6d.png ../_images/f4c25939efaaa0f4a0fe1783ee7dfd02a2f4e134e0c9c57b32974ec735a7e55b.png ../_images/d9c0f0ce35e5d4fa4b33a23d77a5312d1769bfa2fd6538ae5ca0ee03c8b5b65d.png ../_images/b7340fef733850571838662755952cc6ef6a79af74ccfb36b25afb0c5f5dc50f.png

Visualisation en 3D#

Pour une visualisation réelle en 3D, vous pouvez également utiliser napari.

# démarrer napari
viewer = napari.Viewer()

# afficher les images
viewer.add_image(cle.pull(resampled))
viewer.add_image(cle.pull(equalized_intensities_stack))
viewer.add_labels(cle.pull(segmented))
INFO:xmlschema:Resource 'XMLSchema.xsd' is already loaded
<Labels layer 'Labels' at 0x1eba6d51dc0>
viewer.dims.current_step = (40, 0, 0)
nbscreenshot(viewer)
INFO:OpenGL.acceleratesupport:No OpenGL_accelerate module loaded: No module named 'OpenGL_accelerate'

Nous pouvons passer à une vue 3D en cliquant sur le bouton 3D dans le coin inférieur gauche.

nbscreenshot(viewer)

Nous pouvons ensuite aussi incliner et faire pivoter la vue.

nbscreenshot(viewer)