Comment définir les marqueurs pour le bassin versant dans OpenCV?

J'écris pour Android avec OpenCV. Je segmente une image similaire à 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 est ce qui m'intéresse? Puis-je utiliser les résultats des blobs findContours() ou cvBlob pour restreindre le retour sur investissement et appliquer des maxima à chaque blob?

l'image d'entrée

59
demandé sur Harriv 2012-07-02 17:37:56

3 réponses

Tout d'abord: la fonction minMaxLoc ne trouve que le minimum global et le maximum global pour une entrée donnée, donc elle est surtout inutile pour déterminer les minima régionaux et/ou les maxima régionaux. Mais votre idée est juste, extraire des marqueurs basés sur des minima/maxima régionaux pour effectuer une transformation de bassin versant basée sur des marqueurs est tout à fait bien. Permettez-moi d'essayer de clarifier Quelle est la transformation du bassin versant et comment vous devez utiliser correctement L'implémentation présente dans OpenCV.

Une quantité décente de papiers cet accord avec le bassin versant le décrit de la même manière que ce qui suit (je pourrais manquer quelques 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 sommets (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, de l'eau colorée. Maintenant, faire des trous dans chaque vallée de votre surface et l'eau commence à remplir toute la région. À un moment donné, des eaux de couleur différente se rencontreront, et lorsque cela se produira, vous construirez un un barrage tel qu'ils ne se touchent pas. En fin de compte, vous avez une collection de barrages, qui est le bassin versant 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: sur-segmentation. Si vous faites trop peu, vous obtenez une sous-segmentation. Ainsi, pratiquement n'importe quel document qui suggère d'utiliser un bassin versant présente en fait des techniques pour éviter ces problèmes pour l'application à laquelle le document traite.

J'ai écrit tout ça (ce qui est peut-être trop naïf pour quiconque sait ce qu'est la transformation du bassin versant) parce qu'il reflète directement sur la façon dont vous devriez utiliser les implémentations du bassin versant (ce que la réponse acceptée actuelle fait de manière complètement fausse). 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 objets 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 objets, de ne pas les regrouper en un seul composant. Si vous avez besoin d'au moins un marqueur pour chaque objet et de bons marqueurs pour l'arrière-plan. A titre d'exemple, d'abord binariser L'image d'entrée par Otsu et effectuer une ouverture morphologique pour enlever les petits objets. Le résultat de cette étape est illustrée ci-dessous dans l'image de gauche. Maintenant, avec l'image binaire, envisagez d'appliquer la transformation de distance, résultat à droite.

entrez la description de l'image icientrez la description de l'image ici

Avec le résultat de transformation de distance, nous pouvons considérer un certain seuil tel que nous ne considérons que les régions les plus éloignées de l'arrière-plan (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 affiché 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 à droite image).

entrez la description de l'image icientrez 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 versant construira des barrages pour empêcher que les différentes "couleurs" fusionnent. Si nous faisons la transformation, nous obtenons l'image à gauche. Considérant que les barrages en composant avec l'image originale, nous obtenons le résultat à droite.

entrez la description de l'image icientrez 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)
96
répondu mmgp 2018-03-10 17:50:13

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, je vais utiliser watershed comme outil pour extraction de premier plan-arrière-plan. (cet exemple est la contrepartie python du code C++ dans OpenCV cookbook). C'est un cas simple pour comprendre le bassin versant. En dehors de cela, vous pouvez utiliser watershed pour compter le nombre d'objets dans cette image. Qui sera un version légèrement avancée de ce code.

1 - nous chargeons d'abord notre image, la convertissons en niveaux de gris et la définissons avec une valeur appropriée. J'ai pris la binarisation de Otsu , donc il trouverait 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:

entrez 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. Marqueur est l'image avec la même taille comme celle de l'image originale qui est 32SC1 (32 bits signé canal unique).

Maintenant, il y aura des régions dans l'image originale où vous êtes simplement sûr, 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 sont marqués avec 128. La région que vous n'êtes pas sûr sont marqués avec 0. 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 sommes sûrs que la région restante appartient au premier plan.

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

Fg :

entrez la description de l'image ici

B-Région D'arrière - plan: - Ici, nous dilatons l'image seuil afin que la région d'arrière-plan soit réduite. Mais nous sommes sûrs que la région noire restante est 100% fond. Nous l'avons mis à 128.

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

, nous en arrivons Maintenant bg comme suit :

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

entrez la description de l'image ici

Maintenant, nous pouvons clairement comprendre de l'image ci-dessus, que la région blanche est 100% de premier plan, la région grise est 100% de fond, et la région noire nous ne sommes pas sûrs.

Ensuite, nous le convertissons en 32SC1:

marker32 = np.int32(marker)

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

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

M :

entrez la description de l'image ici

4 - Nous avons seuil correctement pour obtenir le masque et effectuer 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 :

entrez la description de l'image ici

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

Arche

43
répondu Abid Rahman K 2013-05-08 15:40:00

Avant-propos

Je suis principalement parce que j'ai trouvé à la fois le tutoriel watershed dans la documentation OpenCV (et C++ example ) ainsi que la réponse de mmgp ci-dessus être assez déroutant. J'ai revisité une approche de bassin versant à plusieurs reprises pour finalement abandonner par 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 venez à travers.

En plus d'être un novice en vision par ordinateur, la plupart de mes problèmes avaient probablement à voir avec mon exigence d'utiliser la bibliothèque OpenCVSharp plutôt que Python. C# n'a pas d'opérateurs de tableau haute puissance comme ceux trouvés dans NumPy (bien que je réalise que cela a été porté via IronPython), donc j'ai eu beaucoup de mal à comprendre et à implémenter ces opérations en C#. Aussi, pour mémoire, je méprise vraiment les nuances de, et les incohérences dans la plupart d'entre eux les appels de fonction. OpenCVSharp est l'une des bibliothèques les plus fragiles avec lesquelles j'ai jamais travaillé. Mais bon, c'est un port, alors qu'est-ce que je m'attendais? Le meilleur de tous, cependant-c'est gratuit.

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

Application

Tout d'abord, assurez-vous que le bassin versant Est ce que vous voulez et comprenez son utilisation. Je suis à l'aide de plaques cellulaires tachées, comme celle-ci:

entrez la description de l'image ici

Il m'a fallu du temps 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 le bassin versant sur 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:

entrez la description de l'image ici

  1. commencez par l'image source (gauche, recadrée à des fins de démonstration)
  2. isoler le canal rouge (Milieu gauche)
  3. appliquer le seuil adaptatif (Milieu droit)
  4. Trouvez les 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 itéré à travers tous les contours supérieurs à une certaine zone.

Code

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

entrez la description de l'image ici

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

Nous commencerons par un tapis vierge et ne dessinerons que 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 "conseils" sur le retour sur investissement. Si vous êtes un débutant complet comme moi, je vous recommande de consulter la page CMM watershed pour une introduction rapide. Autant dire que nous allons créer astuces à propos du ROI sur la gauche en créant la forme sur la droite:

entrez la description de l'image ici

Pour créer la partie blanche (ou "arrière-plan") de cette forme "indice", nous allons juste 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 utiliserons une transformée de distance suivie d'un seuil, qui nous amène de la forme de gauche à la forme de droite:

entrez la description de l'image ici

Cela prend quelques étapes, et vous peut avoir besoin de jouer avec la limite inférieure de votre seuil pour obtenir des résultats qui fonctionnent pour vous:

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, il devrait ressembler à ceci:

entrez la description de l'image ici

Joli! C'était facile pour moi d'envelopper ma tête. La partie suivante, cependant, m'a assez perplexe. Regardons transformer notre "indice" en quelque chose de la fonction Watershed pouvez utiliser. Pour cela, nous devons utiliser ConnectedComponents, qui est essentiellement une grande matrice de pixels regroupés par la vertu de leur index. Par exemple, si nous avions un tapis avec les lettres "SALUT", ConnectedComponents renvoie 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

Ainsi, 0 est l'arrière-plan, 1 est la lettre "H", et 2 est la lettre "I". (Si vous arrivez à ce point et que vous voulez visualiser votre matrice, je vous recommande de vérifier cette réponse instructive. Maintenant, voici comment nous allons utiliser ConnectedComponents pour créer des marqueurs (ou étiquettes) pour 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<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 bassin versant exige que la zone frontalière soit marquée par 0. Nous avons donc défini les pixels de bordure sur 0 dans le tableau label/marker.

À ce stade, nous devrions ê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 masque d'abord un petit peu 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 puis faites l'appel magique:

Cv2.Watershed(sourceCrop, labels);

Les Résultats

L'appel Watershed ci-dessus modifiera labels en place . Vous devrez revenir à vous souvenir de la matrice résultant de ConnectedComponents. La différence ici est que si le bassin versant a trouvé des barrages entre les bassins versants, ils seront marqués comme " -1 " dans cette matrice. Comme le résultat ConnectedComponents, les différents bassins versants seront marqués de façon similaire en incrémentant les nombres. Pour mes besoins, je voulais les stocker séparément contours, j'ai donc créé cette boucle pour les diviser:

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));

    }
}

, je voulais imprimer ces contours avec des couleurs aléatoires, j'ai donc créé le tapis:

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);
    }
}

Qui donne ce qui suit lorsqu'il est montré:

entrez la description de l'image ici

Si nous dessinons sur l'image source les barrages qui ont été marqués par un -1 plus tôt, nous obtenons ceci:

entrez la description de l'image ici

Modifications:

J'ai oublié de noter: Assurez-vous de nettoyer vos tapis après vous avez terminé avec eux. 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.

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 une étape d'étirement d'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'ai vus étaient trop étroits pour commencer), mais votre kilométrage peut varier.

1
répondu Daniel 2018-06-22 22:13:07