web-dev-qa-db-fra.com

Comment définir les marqueurs pour Watershed dans OpenCV?

J'écris pour Android avec OpenCV. Je segmente une image semblable à celle ci-dessous à l'aide d'un bassin versant contrôlé par un marqueur, sans que l'utilisateur ne la marque manuellement. Je compte utiliser les maxima régionaux. comme marqueurs.

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

input image

66
Tru

Tout d'abord: la fonction minMaxLoc ne trouve que le minimum global et le maximum global pour une entrée donnée, elle est donc inutile pour la détermination des minima régionaux et/ou des maxima régionaux. Mais votre idée est bonne, extraire des marqueurs basés sur des minima/maxima régionaux pour effectuer une transformation de bassin versant à partir de marqueurs est tout à fait correct. Permettez-moi d'essayer de clarifier ce qu'est la transformation du bassin hydrographique et comment utiliser correctement la mise en œuvre présente dans OpenCV.

Une quantité décente de documents traitant des bassins versants le décrit de la même manière que ce qui suit (il se peut que je manque des détails si vous n'êtes pas sûr: demandez). Considérez la surface d'une région que vous connaissez, elle contient des vallées et des pics (entre autres détails qui ne sont pas pertinents pour nous ici). Supposons que sous cette surface, tout ce que vous avez est de l'eau colorée. Maintenant, faites des trous dans chaque vallée de votre surface, puis l’eau commence à remplir toute la surface. À un moment donné, des eaux de couleurs différentes se rencontreront et lorsque cela se produira, vous construirez un barrage de manière à ce qu'elles ne se touchent pas. À la fin, vous avez une collection de barrages, qui est le bassin versant séparant toutes les eaux de couleurs différentes.

Maintenant, si vous faites trop de trous dans cette surface, vous vous retrouvez avec trop de régions: sur-segmentation. Si vous en faites trop peu, vous obtenez une sous-segmentation. Ainsi, pratiquement tout document suggérant l’utilisation de bassin versant présente des techniques permettant d’éviter ces problèmes pour l’application qu’il traite.

J'ai écrit tout cela (ce qui est peut-être trop naïf pour quiconque sait ce qu'est la transformation du bassin hydrographique), car cela reflète directement la manière dont vous devriez utiliser les implémentations de bassin hydrographique (ce que la réponse acceptée actuellement fait d'une manière complètement fausse). Commençons par l'exemple OpenCV en utilisant les liaisons Python.

L'image présentée dans la question est composée de nombreux objets pour la plupart trop proches et se chevauchant parfois. Dans ce cas, l’utilité de la gestion des bassins hydrographiques est de séparer correctement ces objets et non de les regrouper en un seul composant. Il faut donc au moins un marqueur pour chaque objet et de bons marqueurs pour l’arrière-plan. À titre d'exemple, commencez par binariser l'image d'entrée par Otsu et effectuez une ouverture morphologique pour supprimer les petits objets. Le résultat de cette étape est présenté ci-dessous dans l'image de gauche. Maintenant, avec l’image binaire, envisagez d’appliquer la transformation de distance, résultat à droite.

enter image description hereenter image description here

Avec le résultat de la transformation de distance, nous pouvons considérer un seuil tel que nous ne considérons que les régions les plus éloignées du fond (image de gauche ci-dessous). Ce faisant, nous pouvons obtenir un marqueur pour chaque objet 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 montré en bas à droite (certains marqueurs sont trop sombres pour être vus, mais chaque zone blanche dans l'image de gauche est représentée dans l'image de droite).

enter image description hereenter image description here

Ce marqueur que nous avons ici a beaucoup de sens. Chaque colored water == one marker commenceront à remplir la région et la transformation des bassins versants construira des barrages pour empêcher la fusion des différentes "couleurs". Si nous faisons la transformation, nous obtenons l'image à gauche. En ne considérant que les barrages en les composant avec l'image d'origine, nous obtenons le résultat à droite.

enter image description hereenter image description here

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)
108
mmgp

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

Dans ce code, je vais utiliser watershed comme outil pour extraction d'avant-plan/arrière-plan. (Cet exemple est le pendant python du code C++ dans le livre de recettes OpenCV). C’est un cas simple pour comprendre les bassins versants. En dehors de cela, vous pouvez l’utiliser pour compter le nombre d’objets dans cette image. Ce sera une version légèrement avancée de ce code.

1 - Nous chargeons d'abord notre image, la convertissons en niveaux de gris et la seuil avec une valeur appropriée. J'ai pris binarisation d'Ots, afin de 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)

Ci-dessous le résultat que j'ai obtenu:

enter image description here

(Même ce résultat est bon, car le contraste entre les images de premier plan et d'arrière-plan)

2 - Nous devons maintenant créer le marqueur. Le marqueur est l’image de même taille que celle de l’image originale, soit 32SC1 (canal unique signé 32 bits).

Maintenant, il y aura certaines 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 avec 128. La région dont vous n'êtes pas sûr est marquée avec 0. C'est ce que nous allons faire ensuite.

A - Région au 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 sommes sûrs que la région restante appartient au premier plan.

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

fg:

enter image description here

B - Région d’arrière-plan: - Nous dilatons ici l’image seuillée afin de réduire la région d’arrière-plan. Mais nous sommes 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:

enter image description here

C - Maintenant nous ajoutons fg et bg:

marker = cv2.add(fg,bg)

Voici ce que nous obtenons:

enter image description here

Nous pouvons maintenant clairement comprendre, à partir de l'image ci-dessus, que la région blanche est à 100% au premier plan, la région grise à 100% à l'arrière-plan et que la région noire est incertaine.

Ensuite, nous le convertissons en 32SC1:

marker32 = np.int32(marker)

3 - Enfin nous appliquons un tournant et convertissons le résultat en int8 image:

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

m:

enter image description here

4 - Nous le seuillons correctement pour obtenir le masque et effectuons 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:

enter image description here

J'espère que ça aide!!!

ARCHE

42
Abid Rahman K

Avant-propos

J'interviens surtout parce que j'ai trouvé à la fois le tutoriel sur les points d'eau dans la documentation OpenCV (et exemple en C++ ) ainsi que la réponse de mmgp ci-dessus à être assez déroutant. Je suis revenu plusieurs fois sur une approche décisive pour finalement abandonner 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 trié tous les tutoriels que j’ai rencontrés.

En plus d'être novice en vision par ordinateur, la plupart de mes problèmes ont probablement trait à la nécessité d'utiliser la bibliothèque OpenCVSharp plutôt que Python. C # n'a pas intégré d'opérateurs de tableaux haute puissance comme ceux trouvés dans NumPy (bien que je sache que cela a été porté via IronPython), j'ai donc eu pas mal de difficulté à comprendre et à mettre en œuvre ces opérations en C #. De plus, pour le compte rendu, 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 m'attendais-je? Le meilleur de tous, cependant - c'est gratuit.

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

Application

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

enter image description here

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

enter image description here

  1. Commencez avec l'image source (à gauche, recadrée à des fins de démonstration)
  2. Isoler le canal rouge (centre gauche)
  3. Appliquer le seuil adaptatif (milieu droit)
  4. Trouver les contours puis éliminer 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 hydrographique. Dans mon cas, j'ai simplement itéré à travers tous les contours supérieurs à une certaine zone.

Code

Supposons que nous ayons isolé ce contour du champ ci-dessus en tant que retour sur investissement:

enter image description here

Voyons comment nous allons coder un bassin versant.

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

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

Pour que l'appel du bassin versant fonctionne, il faudra quelques "astuces" sur le retour sur investissement. Si vous êtes un débutant comme moi, je vous recommande de consulter le page de CMM Watershed pour un bref aperçu. Autant dire que nous allons créer des indices sur le retour sur investissement de gauche en créant la forme de droite:

enter image description here

Pour créer la partie blanche (ou "fond") de cette forme "hint", nous allons simplement Dilate la forme isolée comme ceci:

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 allons utiliser une transformation de distance suivie d'un seuil, qui nous conduit de la forme à gauche à la forme à droite:

enter image description here

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 soustrayons 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:

enter image description here

Agréable! C’était facile pour moi de tourner la tête autour de moi. La partie suivante, cependant, m'a beaucoup intriguée. Voyons comment transformer notre "indice" en quelque chose que la fonction Watershed peut utiliser. Pour cela, nous devons utiliser ConnectedComponents, qui est en gros une grande matrice de pixels regroupés par la vertu de leur index. Par exemple, si nous avions un tapis avec les lettres "HI", ConnectedComponents pourrait renvoyer cette matrice:

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 souhaitez visualiser votre matrice, 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 les bassins versants:

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<char>(y, x);    //note: x and y are inexplicably 
        var borderPixel = (int)unknown.At<char>(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 les pixels de bordure sur 0 dans le tableau label/marqueur.

À ce stade, nous devrions tous être prêts à appeler Watershed. Cependant, dans mon application particulière, il est utile de visualiser une petite partie de l’image source entière pendant cet appel. Cela peut être facultatif pour vous, mais je commence par masquer une petite partie de la source en la 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 ensuite faire l'appel magique:

Cv2.Watershed(sourceCrop, labels);

Résultats

L'appel Watershed ci-dessus modifiera labels à la place. Vous devrez vous souvenir de la matrice résultant de ConnectedComponents. La différence ici est que, si les bassins versants trouvent des barrages entre eux, ils seront marqués "-1" dans cette matrice. Comme pour le résultat ConnectedComponents, différents bassins versants seront marqués de la même façon, en incrémentant les nombres. Pour mes besoins, je souhaitais les stocker dans des contours séparés. J'ai donc créé cette boucle pour les séparer:

var watershedContours = new List<Tuple<int, List<Point>>>();

for (int x = 0; x < labels.Width; x++)
{
    for (int y = 0; y < labels.Height; y++)
    {
        var labelPixel = labels.At<Int32>(y, x); //note: x, y switched 

        var connected = watershedContours.Where(t => t.Item1 == labelPixel).FirstOrDefault();
        if (connected == null)
        {
            connected = new Tuple<int, List<Point>>(labelPixel, new List<Point>());
            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 le suivant quand montré:

enter image description here

Si nous dessinons sur l'image source les barrages marqués d'un -1 précédemment, nous obtenons ceci:

enter image description here

Modifications:

J'ai oublié de noter: assurez-vous de nettoyer vos tapis une fois que vous en avez fini. Ils resteront en mémoire et OpenCVSharp peut présenter un message d'erreur inintelligible. Je devrais vraiment utiliser using ci-dessus, mais mat.Release() est également une option.

De plus, la réponse de mmgp ci-dessus comprend la ligne suivante: dt = ((dt - dt.min()) / (dt.max() - dt.min()) * 255).astype(numpy.uint8), qui est une étape d'étirement de l'histogramme 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 au début), mais votre kilométrage peut varier.

5
Daniel