Segmentation d’image sur Python : technique du watershed

Temps de lecture : 5 minutes

Voici ce que nous allons réaliser dans cet article :

Partir d’une image binaire en entrée pour obtenir en sortie, des régions segmentées (bassins en couleur à droite). Pour chacun de ces bassins, on disposera de nombreuses propriétés géométriques telles que l’aire, le périmètre, la longueur des axes principaux … etc. Pourquoi c’est intéressant ? Imagine que tu as à détecter toutes les particules de forme circulaire sur l’image en entrée ! La création de ces bassins et surtout l’accès à leurs propriétés géométriques te permet de réaliser rapidement le tri. J’ai déjà écrit un article complet sur la détection 😀. Ici, je vais me contenter de présenter la méthode principale pour réaliser la phase de segmentation : la technique du watershed.

L’entrée est un bout d’image .png binaire de taille 300 x 300. Les particules de forme circulaire sont en noir (intensité de pixel = 0) et l’arrière-plan en blanc (intensité de pixel = 1).

[Code complet (Jupyter Notebook) et ressources sur Github]

Concept du watershed

Watershed signifie littéralement « ligne de partage des eaux »; sans faire de l’hydrographie (je n’ai jamais été fan de ce cours en école de toute façon🙈), imagine deux bassins d’eaux adjacents : eh bien, c’est simplement la ligne qui les délimite. Le principe est pareil ici. L’arrière-plan en blanc constitue un espace ouvert qui peut être rempli par les eaux et les particules en noir sont les lignes de partage. Tu peux aussi les voir un peu comme des murs. Donc suivant ce scénario, si on déverse de l’eau dans notre domaine, celle-ci va stagner à l’intérieur des particules ou entre les espaces interstitiels entre les particules pour former nos fameux bassins ! Plutôt facile à comprendre, non 🙂 ? Maintenant voyons comment on réalise tout ça sur Python.

Implémentation du watershed

Sur Python, il faut commencer par placer des marqueurs à des endroits stratégiques de l’image. Le principe, c’est qu’à partir de ces marqueurs, nous allons propager le bassin jusqu’à ce qu’on rencontre un mur (particule noire). Sinon tant qu’on a de l’espace blanc, on continue à se propager (on se croirait presqu’à une conférence à Berlin en 1885, humour noir d’historien, bravo si tu as la référence 👊). De base, Python crée ces marqueurs automatiquement. Mais comme on aime mettre la main à la pâte et qu’on est un peu paranos, nous allons les créer nous-même.

Une solution efficace serait de placer localement les marqueurs au centre des espaces blancs comme ceci par exemple :

Mais comment trouver les coordonnées de ces marqueurs ? Bonne question ! On peut remarquer que, si on calcule pour chaque pixel de l’arrière-plan (espace blanc), la distance au pixel noir le plus proche (particules), les points au centre des espaces blancs auront localement les distances les plus grandes; parce qu’ils sont les plus éloignés des « murs ». Les mathématiciens appellent ça les extrema locaux (juste pour ta culture générale 😉).

Sur Python

Sur Python, on calculera dans un premier temps les distances avec la fonction scipy.ndimage.morphology.distance_transform_edt(). Puis on ira chercher les extréma locaux avec skimage.morphology.local_minima(). Et une fois qu’on les aura déterminés, on les labellisera : ça veut juste dire les numéroter de 1 à n avec scipy.ndimage.label().

# Calcul des distances de l'arrière-plan aux particules
distance = ndi.distance_transform_edt(image)
# Détermination des extrema locaux
l_min = morphology.local_minima(-distance)
# Labellisation des marqueurs
markers, _ = ndi.label(l_min)

Deux points pour les plus attentifs. Premièrement, j’avais parlé de maximum local dans mon explication et dans mon code, j’ai plutôt calculé un minimum local … mais remarque, le minimum est calculé sur « –distance » et pas sur « distance ». J’utilise juste le principe que max(x) = min(-x). C’est la même chose. Deuxièmement, on n’avait pas besoin de placer les marqueurs exactement au centre des espaces blancs parce que le bassin va se propager dans tous les cas. Mais c’était l’occasion de te faire découvrir un nouveau concept sur les extrema locaux. Dans d’autres cas de traitements d’image, ça peut être utile 😉!

Bon, maintenant qu’on a les marqueurs, c’est le moment de propager les bassins, excitant 😀! Pour ce faire, on utilise la fonction watershed. En premier argument on lui passe la carte des distances qu’on vient de calculer. On aurait aussi pu lui passer l’image binarisée à la place. En deuxième argument, on passe les marqueurs. Et en troisième argument, il faut un masque pour lui demander de créer les bassins uniquement dans les zones spécifiées (ici, l’arrière-plan). Notre image étant déjà binarisée, l’arrière-plan étant à 1 et les particules à 0, on peut utiliser l’image comme masque. Pour rappel, le principe du masque c’est : « si la zone est à 1, fais l’opération, si elle à 0 ne fais rien ».

# Création des bassins
labels = watershed(image, markers = markers, mask = image)

Après cette étape, on récupère tous les bassins labellisés, joli n’est-ce pas ?

Fusionner certains bassins

Je te montre une petite dernière astuce. Parfois, on peut obtenir des bassins adjacents qui se touchent mais qui sont considérés comme des bassins différents par Python ! Regarde ici par exemple, les bassins se touchent mais on a 3 couleurs différentes, donc ils ont été individualisés !

Il peut être utile de demander à Python de fusionner tous les bassins adjacents pour former de grands bassins continus ! Comment ? en une seule ligne avec scipy.ndimage.label :

# Deuxième labellisation pour fusionner les bassins adjacents
labels, _ = ndi.label(labels)

En fait, pendant la labellisation, dès que les bassins se touchent, cette ligne de code va les unir. La notion de « se toucher » peut être définie plus précisément avec une matrice d’adjacence mais on va garder les choses simples ici. Si on réaffiche la zone avec les 3 bassins adjacents, bingo, ils n’en forment qu’un désormais !

Récupération des propriétés géométriques des bassins segmentés

A cette étape, comment fait-on pour récupérer les propriétés géométriques des régions ? C’est peut-être le plus utile, hormis les belles couleurs 😑. Python propose la fonction regionprops(). Elle permet de récupérer les propriétés de tous les bassins qui ont été labellisés.

# Récupération des propriétés des bassins créés (régions)
regions = regionprops(labels)

Si tu désires récupérer l’aire du 10e bassin par exemple, il suffit de faire :

# Aire de la région 10 par exemple
regions[10].area

Je te conseille d’aller regarder la page officielle de regionprops() pour voir toutes les propriétés auxquelles tu peux avoir accès. Et si tu veux savoir comment utiliser cette méthode du watershed sur une problématique réelle de détection, c’est par ici que ça se passe !

Partager

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *