Comment définir les marqueurs pour Watershed dans OpenCV?

J’écris pour Android avec OpenCV. Je segmente une image similaire à celle ci-dessous en utilisant un bassin hydrographique contrôlé par un marqueur, sans que l’utilisateur ne marque manuellement l’image. Je prévois d’utiliser les maxima régionaux comme marqueurs.

minMaxLoc() me donnerait la valeur, mais comment puis-je la limiter aux blobs qui m’intéressent? Puis-je utiliser les résultats des findContours() ou cvBlob pour restreindre le retour sur investissement et appliquer des maxima à chaque blob?

image d'entrée

Tout d’abord, la fonction minMaxLoc ne trouve que le maximum global minimum et global pour une entrée donnée, il est donc essentiellement inutile pour déterminer les minima régionaux et / ou maxima régionaux. Mais votre idée est juste, l’extraction de marqueurs basés sur les minima / maxima régionaux pour effectuer une transformation de bassin hydrographique basée sur des marqueurs est tout à fait correcte. Permettez-moi d’essayer de clarifier ce qu’est la transformation de bassin versant et comment vous devriez utiliser correctement l’implémentation présente dans OpenCV.

Une quantité décente de documents traitant du bassin hydrographique le décrit de la même manière que ci-après (je pourrais manquer certains détails si vous n’êtes pas certain: demandez). Considérez la surface d’une région que vous connaissez, elle contient des vallées et des pics (parmi d’autres détails qui ne sont pas pertinents pour nous ici). Supposons que sous cette surface, il n’y ait que de l’eau, de l’eau colorée. Maintenant, faites des trous dans chaque vallée de votre surface, puis l’eau commence à remplir toute la zone. À un moment donné, des eaux de couleurs différentes se rencontreront et, lorsque cela se produira, vous construirez un barrage de telle sorte qu’elles ne se touchent pas. À la fin, vous avez une collection de barrages, qui est la ligne de partage séparant toutes les différentes eaux colorées.

Maintenant, si vous faites trop de trous dans cette surface, vous vous retrouvez avec trop de régions: la sur-segmentation. Si vous en faites trop, vous obtenez une sous-segmentation. Ainsi, pratiquement tous les articles suggérant l’utilisation du bassin versant présentent en fait des techniques permettant d’éviter ces problèmes pour l’application utilisée dans le document.

J’ai écrit tout cela (ce qui est peut-être trop naïf pour quiconque sait ce qu’est la transformation des bassins versants) parce qu’il reflète directement la façon dont vous devez utiliser les implémentations de bassin versant (que la réponse acceptée actuelle fait d’une manière complètement erronée). Commençons par l’exemple OpenCV maintenant, en utilisant les liaisons Python.

L’image présentée dans la question est composée de nombreux objects qui sont pour la plupart trop proches et, dans certains cas, se chevauchent. L’utilité du bassin versant ici est de séparer correctement ces objects, et non de les regrouper en un seul composant. Vous avez donc besoin d’au moins un marqueur pour chaque object et de bons marqueurs pour l’arrière-plan. À titre d’exemple, commencez par numériser l’image d’entrée par Otsu et effectuez une ouverture morphologique pour supprimer les petits objects. Le résultat de cette étape est indiqué ci-dessous dans l’image de gauche. Maintenant, avec l’image binary, appliquez la transformation de distance à celle-ci, résultat à droite.

entrer la description de l'image icientrer la description de l'image ici

Avec le résultat de la transformation de distance, on peut considérer un seuil tel que l’on ne considère que les régions les plus éloignées de l’arrière-plan (image de gauche ci-dessous). En faisant cela, nous pouvons obtenir un marqueur pour chaque object en étiquetant les différentes régions après le seuil précédent. Maintenant, nous pouvons également considérer la bordure d’une version dilatée de l’image de gauche ci-dessus pour composer notre marqueur. Le marqueur complet est indiqué ci-dessous à droite (certains marqueurs sont trop sombres pour être vus, mais chaque région blanche de l’image de gauche est représentée à l’image de droite).

entrer la description de l'image icientrer la description de l'image ici

Ce marqueur que nous avons ici a beaucoup de sens. Chaque colored water == one marker commencera à remplir la région, et la transformation du bassin hydrographique construira des barrages pour empêcher que les différentes “couleurs” fusionnent. Si nous faisons la transformation, nous obtenons l’image à gauche. En ne considérant que les barrages en les composant avec l’image originale, on obtient le résultat à droite.

entrer la description de l'image icientrer la description de l'image ici

 import sys import cv2 import numpy from scipy.ndimage import label def segment_on_dt(a, img): border = cv2.dilate(img, None, iterations=5) border = border - cv2.erode(border, None) dt = cv2.distanceTransform(img, 2, 3) dt = ((dt - dt.min()) / (dt.max() - dt.min()) * 255).astype(numpy.uint8) _, dt = cv2.threshold(dt, 180, 255, cv2.THRESH_BINARY) lbl, ncc = label(dt) lbl = lbl * (255 / (ncc + 1)) # Completing the markers now. lbl[border == 255] = 255 lbl = lbl.astype(numpy.int32) cv2.watershed(a, lbl) lbl[lbl == -1] = 0 lbl = lbl.astype(numpy.uint8) return 255 - lbl img = cv2.imread(sys.argv[1]) # Pre-processing. img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) _, img_bin = cv2.threshold(img_gray, 0, 255, cv2.THRESH_OTSU) img_bin = cv2.morphologyEx(img_bin, cv2.MORPH_OPEN, numpy.ones((3, 3), dtype=int)) result = segment_on_dt(img, img_bin) cv2.imwrite(sys.argv[2], result) result[result != 255] = 0 result = cv2.dilate(result, None) img[result == 255] = (0, 0, 255) cv2.imwrite(sys.argv[3], img) 

Je voudrais expliquer un code simple sur la façon d’utiliser le bassin versant ici. J’utilise OpenCV-Python, mais j’espère que vous n’aurez aucune difficulté à comprendre.

Dans ce code, j’utiliserai le bassin versant comme outil d’ extraction en arrière-plan. (Cet exemple est la contrepartie python du code C ++ dans le livre de recettes OpenCV). C’est un cas simple pour comprendre le bassin versant. En dehors de cela, vous pouvez utiliser le bassin versant pour compter le nombre d’objects dans cette image. Ce sera une version légèrement avancée de ce code.

1 – Nous commençons par charger notre image, la convertir en niveaux de gris et la définir avec une valeur appropriée. J’ai pris la binarisation d’Otsu pour trouver la meilleure valeur de seuil.

 import cv2 import numpy as np img = cv2.imread('sofwatershed.jpg') gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY) ret,thresh = cv2.threshold(gray,0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU) 

Voici le résultat que j’ai obtenu:

entrer la description de l'image ici

(même ce résultat est bon, car un grand contraste entre les images de premier plan et d’arrière-plan)

2 – Maintenant, nous devons créer le marqueur. Le marqueur est l’image de même taille que celle de l’image originale, à savoir 32SC1 (canal unique signé 32 bits).

Maintenant, il y aura des régions dans l’image d’origine où vous êtes simplement sûr que cette partie appartient au premier plan. Marquez cette région avec 255 dans l’image du marqueur. Maintenant, la région où vous êtes sûr d’être à l’arrière-plan est marquée par 128. La région dont vous n’êtes pas sûr est marquée par 0. C’est ce que nous allons faire ensuite.

A – Région de premier plan : – Nous avons déjà une image de seuil où les pilules sont de couleur blanche. Nous les érodons un peu, de sorte que nous sums certains que la région restante appartient au premier plan.

 fg = cv2.erode(thresh,None,iterations = 2) 

fg :

entrer la description de l'image ici

B – Région d’arrière-plan : – Ici, nous dilatons l’image seuillée pour réduire la région d’arrière-plan. Mais nous sums sûrs que la région noire restante est 100% de fond. Nous l’avons mis à 128.

 bgt = cv2.dilate(thresh,None,iterations = 3) ret,bg = cv2.threshold(bgt,1,128,1) 

Maintenant, nous obtenons bg comme suit:

entrer la description de l'image ici

C – Maintenant, nous ajoutons à la fois fg et bg :

 marker = cv2.add(fg,bg) 

Voici ce que nous obtenons:

entrer la description de l'image ici

Maintenant, nous pouvons clairement comprendre à partir de l’image ci-dessus, que la région blanche est à 100% en avant-plan, la région grise est à 100% en arrière-plan et la région noire n’est pas sûre.

Ensuite, nous le convertissons en 32SC1:

 marker32 = np.int32(marker) 

3 – Enfin, nous appliquons le bassin versant et convertissons le résultat en image uint8 :

 cv2.watershed(img,marker32) m = cv2.convertScaleAbs(marker32) 

m:

entrer la description de l'image ici

4On le positionne correctement pour obtenir le masque et exécuter bitwise_and avec l’image d’entrée:

 ret,thresh = cv2.threshold(m,0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU) res = cv2.bitwise_and(img,img,mask = thresh) 

Res:

entrer la description de l'image ici

J’espère que cela aide!!!

ARCHE

Avant-propos

Je retentis surtout parce que j’ai trouvé le didacticiel sur les bassins versants dans la documentation d’OpenCV (et l’ exemple de C ++ ), ainsi que la réponse de mmgp ci-dessus, assez déroutant. J’ai revisité une approche décisive à plusieurs resockets pour finalement renoncer à la frustration. J’ai finalement réalisé que je devais au moins essayer cette approche et la voir en action. C’est ce que j’ai trouvé après avoir sortingé tous les tutoriels que j’ai rencontrés.

En plus d’être un novice en vision par ordinateur, la plupart de mes problèmes étaient probablement liés à mon besoin d’utiliser la bibliothèque OpenCVSharp plutôt que Python. C # n’a pas d’opérateurs de tableaux de grande puissance intégrés tels que ceux trouvés dans NumPy (bien que je réalise que cela a été porté via IronPython), j’ai donc beaucoup lutté pour comprendre et implémenter ces opérations en C #. Aussi, pour mémoire, je méprise vraiment les nuances et les incohérences de la plupart de ces appels de fonctions. OpenCVSharp est l’une des bibliothèques les plus fragiles avec lesquelles j’ai travaillé. Mais bon, c’est un port, alors à quoi je m’attendais? Mieux encore, c’est gratuit.

Sans plus tarder, parlons de ma mise en œuvre du bassin versant OpenCVSharp et espérons clarifier certains des points les plus difficiles de la mise en œuvre des bassins versants en général.

Application

Tout d’abord, assurez-vous que le bassin versant est ce que vous voulez et comprenez son utilisation. J’utilise des plaques de cellules colorées, comme celle-ci:

entrer la description de l'image ici

Il m’a fallu du temps pour comprendre que je ne pouvais pas faire un appel décisif pour différencier chaque cellule sur le terrain. Au contraire, je devais d’abord isoler une partie du champ, puis appeler le bassin hydrographique sur cette petite partie. J’ai isolé ma région d’intérêt (ROI) via un certain nombre de filtres, que j’expliquerai brièvement ici:

entrer la description de l'image ici

  1. Commencez avec l’image source (à gauche, recadrée à des fins de démonstration)
  2. Isoler le canal rouge (centre gauche)
  3. Appliquer un seuil adaptatif (milieu droit)
  4. Trouvez des contours puis éliminez ceux avec de petites zones (à droite)

Une fois que nous avons nettoyé les contours résultant des opérations de seuillage ci-dessus, il est temps de trouver des candidats pour le bassin versant. Dans mon cas, j’ai simplement parcouru tous les contours supérieurs à une certaine zone.

Code

Disons que nous avons isolé ce contour du champ ci-dessus comme notre ROI:

entrer la description de l'image ici

Jetons un coup d’oeil à la façon dont nous allons coder un bassin versant.

Nous allons commencer avec un tapis vierge et dessiner uniquement le contour définissant notre ROI:

 var isolatedContour = new Mat(source.Size(), MatType.CV_8UC1, new Scalar(0, 0, 0)); Cv2.DrawContours(isolatedContour, new List> { contour }, -1, new Scalar(255, 255, 255), -1); 

Pour que l’appel du bassin versant fonctionne, il faudra quelques “indications” sur le retour sur investissement. Si vous êtes un débutant complet comme moi, je vous recommande de consulter rapidement la page des bassins versants du CMM . Autant dire que nous allons créer des indices sur le retour sur investissement à gauche en créant la forme à droite:

entrer la description de l'image ici

Pour créer la partie blanche (ou “arrière-plan”) de cette forme “indice”, nous allons simplement Dilate la forme isolée de la manière suivante:

 var kernel = Cv2.GetStructuringElement(MorphShapes.Ellipse, new Size(2, 2)); var background = new Mat(); Cv2.Dilate(isolatedContour, background, kernel, iterations: 8); 

Pour créer la partie noire au milieu (ou “premier plan”), nous utiliserons une transformée de distance suivie d’un seuil, qui nous amène de la forme à gauche à la forme à droite:

entrer la description de l'image ici

Cela prend quelques étapes et vous devrez peut-être jouer avec la limite inférieure de votre seuil pour obtenir des résultats qui vous conviennent:

 var foreground = new Mat(source.Size(), MatType.CV_8UC1); Cv2.DistanceTransform(isolatedContour, foreground, DistanceTypes.L2, DistanceMaskSize.Mask5); Cv2.Normalize(foreground, foreground, 0, 1, NormTypes.MinMax); //Remember to normalize! foreground.ConvertTo(foreground, MatType.CV_8UC1, 255, 0); Cv2.Threshold(foreground, foreground, 150, 255, ThresholdTypes.Binary); 

Ensuite, nous allons soustraire ces deux tapis pour obtenir le résultat final de notre forme “indice”:

 var unknown = new Mat(); //this variable is also named "border" in some examples Cv2.Subtract(background, foreground, unknown); 

Encore une fois, si nous Cv2.ImShow inconnu , cela ressemblerait à ceci:

entrer la description de l'image ici

Agréable! C’était facile pour moi d’envelopper ma tête. La partie suivante m’a cependant laissé perplexe. Regardons comment transformer notre “indice” en quelque chose que la fonction Watershed peut utiliser. Pour cela, nous devons utiliser ConnectedComponents , qui est essentiellement une grande masortingce de pixels regroupés en vertu de leur index. Par exemple, si nous avions un tapis avec les lettres “HI”, ConnectedComponents pourrait renvoyer cette masortingce:

 0 0 0 0 0 0 0 0 0 0 1 0 1 0 2 2 2 0 0 1 0 1 0 0 2 0 0 0 1 1 1 0 0 2 0 0 0 1 0 1 0 0 2 0 0 0 1 0 1 0 2 2 2 0 0 0 0 0 0 0 0 0 0 

Donc, 0 est le fond, 1 est la lettre “H” et 2 est la lettre “I”. (Si vous arrivez à ce point et que vous souhaitez visualiser votre masortingce, je vous recommande de vérifier cette réponse instructive .) Maintenant, voici comment nous allons utiliser ConnectedComponents pour créer les marqueurs (ou étiquettes) pour le bassin versant:

 var labels = new Mat(); //also called "markers" in some examples Cv2.ConnectedComponents(foreground, labels); labels = labels + 1; //this is a much more verbose port of numpy's: labels[unknown==255] = 0 for (int x = 0; x < labels.Width; x++) { for (int y = 0; y < labels.Height; y++) { //You may be able to just send "int" in rather than "char" here: var labelPixel = (int)labels.At(y, x); //note: x and y are inexplicably var borderPixel = (int)unknown.At(y, x); //and infuriatingly reversed if (borderPixel == 255) labels.Set(y, x, 0); } } 

Notez que la fonction Watershed nécessite que la zone de bordure soit marquée par 0. Nous avons donc défini tous les pixels de bordure à 0 dans le tableau label / marker.

À ce stade, nous devrions tous être prêts à appeler Watershed . Cependant, dans mon application particulière, il est utile de simplement visualiser une petite partie de l’image source au cours de cet appel. Cela peut être facultatif pour vous, mais je commence par masquer un petit bout de la source en le dilatant:

 var mask = new Mat(); Cv2.Dilate(isolatedContour, mask, new Mat(), iterations: 20); var sourceCrop = new Mat(source.Size(), source.Type(), new Scalar(0, 0, 0)); source.CopyTo(sourceCrop, mask); 

Et puis faites l’appel magique:

 Cv2.Watershed(sourceCrop, labels); 

Résultats

L’appel Watershed ci-dessus modifiera les labels en place . Vous devrez vous rappeler de la masortingce résultant de ConnectedComponents . La différence ici est que, si le bassin versant trouve des barrages entre les bassins versants, ils seront marqués “-1” dans cette masortingce. Comme le résultat ConnectedComponents , différents bassins hydrographiques seront marqués de la même manière que les nombres incrémentés. Pour mes besoins, je voulais les stocker dans des contours séparés, donc j’ai créé cette boucle pour les séparer:

 var watershedContours = new List>>(); for (int x = 0; x < labels.Width; x++) { for (int y = 0; y < labels.Height; y++) { var labelPixel = labels.At(y, x); //note: x, y switched var connected = watershedContours.Where(t => t.Item1 == labelPixel).FirstOrDefault(); if (connected == null) { connected = new Tuple>(labelPixel, new List()); watershedContours.Add(connected); } connected.Item2.Add(new Point(x, y)); if (labelPixel == -1) sourceCrop.Set(y, x, new Vec3b(0, 255, 255)); } } 

Ensuite, j’ai voulu imprimer ces contours avec des couleurs aléatoires, j’ai donc créé le tapis suivant:

 var watershed = new Mat(source.Size(), MatType.CV_8UC3, new Scalar(0, 0, 0)); foreach (var component in watershedContours) { if (component.Item2.Count < (labels.Width * labels.Height) / 4 && component.Item1 >= 0) { var color = GetRandomColor(); foreach (var point in component.Item2) watershed.Set(point.Y, point.X, color); } } 

Ce qui donne les éléments suivants lorsque affichés:

entrer la description de l'image ici

Si on dessine sur l’image source les barrages marqués d’un -1 plus tôt, on obtient ceci:

entrer la description de l'image ici

Edits:

J’ai oublié de noter: assurez-vous de nettoyer vos tapis après en avoir fini avec eux. Ils restront en mémoire et OpenCVSharp peut présenter un message d’erreur incompréhensible. Je devrais vraiment utiliser en using ci-dessus, mais mat.Release() est également une option.

En outre, la réponse de mmgp ci-dessus inclut cette ligne: dt = ((dt - dt.min()) / (dt.max() - dt.min()) * 255).astype(numpy.uint8) , qui est un histogramme étape d’étirement appliquée aux résultats de la transformation de distance. J’ai omis cette étape pour un certain nombre de raisons (principalement parce que je ne pensais pas que les histogrammes que j’avais vus étaient trop étroits pour commencer), mais votre kilométrage peut varier.