web-dev-qa-db-de.com

Wie definiere ich die Marker für Watershed in OpenCV?

Ich schreibe für Android mit OpenCV. Ich segmentiere ein Bild ähnlich wie unten mit einem markergesteuerten Wassereinzugsgebiet, ohne dass der Benutzer das Bild manuell markiert. Ich plane, die regionalen Maxima zu verwenden als Marker.

minMaxLoc() würde mir den Wert geben, aber wie kann ich ihn auf die Blobs beschränken, an denen ich interessiert bin? Kann ich die Ergebnisse von findContours() oder cvBlob-Blobs verwenden, um den ROI einzuschränken und Maxima auf jeden Blob anzuwenden?

input image

66
Tru

Zuallererst: Die Funktion minMaxLoc findet nur das globale Minimum und das globale Maximum für eine bestimmte Eingabe und ist daher für die Bestimmung regionaler Minima und/oder regionaler Maxima meistens unbrauchbar. Aber Ihre Idee ist richtig. Das Extrahieren von Markern basierend auf regionalen Minima/Maxima für die Durchführung einer auf Markern basierenden Wasserscheidetransformation ist völlig in Ordnung. Lassen Sie mich versuchen zu klären, was die Wasserscheidetransformation ist und wie Sie die in OpenCV vorhandene Implementierung korrekt verwenden sollten.

Eine anständige Anzahl von Artikeln, die sich mit Wasserscheide befassen, beschreiben sie ähnlich wie folgt (ich vermisse möglicherweise einige Details, wenn Sie sich nicht sicher sind: Fragen Sie). Betrachten Sie die Oberfläche einer Region, die Sie kennen. Sie enthält Täler und Gipfel (unter anderem Details, die für uns hier irrelevant sind). Angenommen, unter dieser Oberfläche ist alles, was Sie haben, Wasser, gefärbtes Wasser. Jetzt mache Löcher in jedes Tal deiner Oberfläche und dann füllt das Wasser den gesamten Bereich. Irgendwann treffen sich verschiedenfarbige Gewässer, und wenn dies passiert, bauen Sie einen Damm, so dass sie sich nicht berühren. Am Ende haben Sie eine Ansammlung von Dämmen, die die Wasserscheide ist, die alle verschiedenfarbigen Gewässer voneinander trennt.

Wenn Sie nun zu viele Löcher in diese Oberfläche bohren, entstehen zu viele Regionen: eine Über-Segmentierung. Wenn Sie zu wenig machen, erhalten Sie eine Untersegmentierung. Praktisch jedes Papier, das die Verwendung von Wasserscheide vorschlägt, bietet Techniken, um diese Probleme für die Anwendung, mit der sich das Papier befasst, zu vermeiden.

Ich habe all das geschrieben (was für jeden, der weiß, was die Wasserscheidetransformation ist, möglicherweise zu naiv ist), weil es direkt darüber nachdenkt, wie Sie Wasserscheidenimplementierungen verwenden sollten (was die derzeit akzeptierte Antwort völlig falsch macht). Beginnen wir jetzt mit dem OpenCV-Beispiel mit den Python Bindings.

Das in der Frage dargestellte Bild setzt sich aus vielen Objekten zusammen, die meist zu nah und teilweise überlappend sind. Die Nützlichkeit der Wasserscheide besteht hier darin, diese Objekte korrekt zu trennen und nicht in einer einzigen Komponente zu gruppieren. Sie benötigen also mindestens einen Marker für jedes Objekt und gute Marker für den Hintergrund. Als Beispiel, digitalisieren Sie zuerst das Eingabebild von Otsu und führen Sie eine morphologische Öffnung durch, um kleine Objekte zu entfernen. Das Ergebnis dieses Schritts ist unten in der linken Abbildung dargestellt. Wenden Sie nun mit dem Binärbild die Abstandstransformation auf das rechte Ergebnis an.

enter image description hereenter image description here

Mit dem Ergebnis der Distanztransformation können wir einen bestimmten Schwellenwert berücksichtigen, sodass wir nur die Regionen betrachten, die am weitesten vom Hintergrund entfernt sind (linkes Bild unten). Auf diese Weise können wir eine Markierung für jedes Objekt erhalten, indem wir die verschiedenen Regionen nach dem früheren Schwellenwert kennzeichnen. Jetzt können wir auch den Rand einer erweiterten Version des linken Bildes oben betrachten, um unseren Marker zu erstellen. Der vollständige Marker wird unten rechts angezeigt (einige Marker sind zu dunkel, um gesehen zu werden, aber jeder weiße Bereich im linken Bild wird im rechten Bild dargestellt).

enter image description hereenter image description here

Dieser Marker, den wir hier haben, macht sehr viel Sinn. Jeder colored water == one marker wird beginnen, die Region zu füllen, und die Transformation der Wasserscheide wird Dämme errichten, um die Verschmelzung der verschiedenen "Farben" zu verhindern. Wenn wir die Transformation durchführen, erhalten wir das Bild auf der linken Seite. Berücksichtigt man nur die Dämme, indem man sie mit dem Originalbild zusammensetzt, erhält man das richtige Ergebnis.

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

Ich möchte einen einfachen Code zur Verwendung der Wasserscheide hier erläutern. Ich benutze OpenCV-Python, aber ich hoffe, Sie werden keine Schwierigkeiten haben, es zu verstehen.

In diesem Code verwende ich die Wasserscheide als Werkzeug für die Vordergrund-Hintergrund-Extraktion. (Dieses Beispiel ist das python = Gegenstück zum C++ - Code im OpenCV-Kochbuch.) Dies ist ein einfacher Fall, um die Wasserscheide zu verstehen. Abgesehen davon können Sie die Anzahl der Objekte in diesem Bild mit der Wasserscheide zählen. Dies ist eine etwas fortgeschrittenere Version dieses Codes.

1 - Zuerst laden wir unser Bild, konvertieren es in Graustufen und schwellen es auf einen geeigneten Wert. Ich nahm Otsus Binarisierung , damit er den besten Schwellenwert fand.

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)

Unten ist das Ergebnis, das ich bekommen habe:

enter image description here

(Auch dieses Ergebnis ist gut, weil der Kontrast zwischen Vordergrund- und Hintergrundbildern groß ist.)

2 - Jetzt müssen wir den Marker erstellen. Der Marker ist das Bild mit der gleichen Größe wie das Originalbild, nämlich 32SC1 (32-Bit-Single-Channel mit Vorzeichen).

Jetzt gibt es einige Bereiche im Originalbild, in denen Sie sich einfach sicher sind, dass dieser Teil zum Vordergrund gehört. Markieren Sie diesen Bereich mit 255 im Markierungsbild. Jetzt ist die Region, in der Sie sicher der Hintergrund sein werden, mit 128 markiert. Die Region, in der Sie sich nicht sicher sind, ist mit 0 markiert. Das ist, was wir als nächstes tun werden.

A - Vordergrundregion : - Wir haben bereits ein Schwellenwertbild, in dem die Pillen weiße Farbe haben. Wir erodieren sie ein wenig, so dass wir sicher sind, dass die verbleibende Region zum Vordergrund gehört.

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

fg :

enter image description here

B - Hintergrundbereich : - Hier wird das Bild mit dem Schwellenwert so erweitert, dass der Hintergrundbereich verkleinert wird. Wir sind uns aber sicher, dass der verbleibende schwarze Bereich 100% Hintergrund ist. Wir setzen es auf 128.

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

Jetzt erhalten wir bg wie folgt:

enter image description here

C - Nun addieren wir fg und bg :

marker = cv2.add(fg,bg)

Nachfolgend erhalten wir:

enter image description here

Jetzt können wir anhand des obigen Bildes klar verstehen, dass der weiße Bereich zu 100% im Vordergrund, der graue Bereich zu 100% im Hintergrund und der schwarze Bereich unsicher sind.

Dann konvertieren wir es in 32SC1:

marker32 = np.int32(marker)

3 - Zuletzt wenden wir die Wasserscheide an und konvertieren das Ergebnis zurück in uint8 :

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

m:

enter image description here

4 - Wir legen einen angemessenen Schwellenwert fest, um die Maske abzurufen und bitwise_and Mit dem eingegebenen Bild auszuführen:

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

Ich hoffe es hilft!!!

ARCHE

42
Abid Rahman K

Vorwort

Ich mische mich hauptsächlich ein, weil ich sowohl das Wasserscheiden-Tutorial in der OpenCV-Dokumentation (und C++ - Beispiel ) als auch die Antwort von mmgp oben gefunden habe sei ziemlich verwirrend. Ich habe einen Wendepunkt-Ansatz mehrmals wiederholt, um letztendlich aus Frustration aufzugeben. Endlich wurde mir klar, dass ich diesen Ansatz zumindest ausprobieren und in Aktion sehen musste. Das ist es, worauf ich gekommen bin, nachdem ich alle Tutorials aussortiert habe, die mir begegnet sind.

Abgesehen davon, dass ich ein Computer Vision-Neuling bin, hatte der größte Teil meiner Probleme wahrscheinlich damit zu tun, dass ich die OpenCVSharp-Bibliothek anstelle von Python verwenden musste. In C # gibt es keine eingebauten Hochleistungs-Array-Operatoren wie in NumPy (obwohl mir klar ist, dass dies über IronPython portiert wurde). Daher hatte ich große Probleme, diese Operationen in C # zu verstehen und umzusetzen. Außerdem verachte ich die Nuancen und Inkonsistenzen der meisten dieser Funktionsaufrufe. OpenCVSharp ist eine der anfälligsten Bibliotheken, mit denen ich je gearbeitet habe. Aber hey, es ist ein Hafen, was habe ich erwartet? Das Beste aber - es ist kostenlos.

Sprechen wir ohne weiteres über meine OpenCVSharp-Implementierung der Wasserscheide und klären hoffentlich einige der schwierigeren Punkte der Wasserscheidenimplementierung im Allgemeinen.

Bewerbung

Stellen Sie zunächst sicher, dass die Wasserscheide Ihren Wünschen entspricht, und verstehen Sie ihre Verwendung. Ich benutze gefärbte Zellplatten, wie diese:

enter image description here

Es dauerte eine Weile, bis ich herausfand, dass ich nicht nur einen einzigen Anruf tätigen konnte, um jede Zelle auf dem Feld zu unterscheiden. Im Gegenteil, ich musste zuerst einen Teil des Feldes isolieren und dann die Wasserscheide für diesen kleinen Teil aufrufen. Ich habe meine Region of Interest (ROI) über eine Reihe von Filtern isoliert, die ich hier kurz erläutern werde:

enter image description here

  1. Beginnen Sie mit dem Quellbild (links, zu Demonstrationszwecken beschnitten)
  2. Isoliere den roten Kanal (linke Mitte)
  3. Anwenden der adaptiven Schwelle (rechte Mitte)
  4. Finden Sie Konturen und beseitigen Sie diese mit kleinen Flächen (rechts)

Sobald wir die Konturen, die sich aus den obigen Schwellenoperationen ergeben, gesäubert haben, ist es Zeit, Kandidaten für Wassereinzugsgebiete zu finden. In meinem Fall habe ich einfach alle Konturen durchlaufen, die größer als ein bestimmter Bereich sind.

Code

Angenommen, wir haben diese Kontur aus dem obigen Feld als ROI isoliert:

enter image description here

Werfen wir einen Blick darauf, wie wir eine Wasserscheide verschlüsseln.

Wir beginnen mit einer leeren Matte und zeichnen nur die Kontur, die unseren ROI definiert:

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

Damit der Aufruf der Wasserscheide funktioniert, sind einige "Hinweise" zum ROI erforderlich. Wenn Sie wie ich ein absoluter Anfänger sind, empfehle ich, die CMM-Seite für eine schnelle Einführung zu lesen. Es genügt zu sagen, dass wir auf der linken Seite Hinweise zum ROI erstellen, indem wir die Form auf der rechten Seite erstellen:

enter image description here

Um den weißen Teil (oder "Hintergrund") dieser "Hinweis" -Form zu erstellen, werden wir einfach die isolierte Form wie folgt Dilate:

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

Um den schwarzen Teil in der Mitte (oder "Vordergrund") zu erstellen, verwenden wir eine Abstandstransformation gefolgt von einem Schwellenwert, der uns von der Form links zur Form rechts führt:

enter image description here

Dies dauert einige Schritte, und Sie müssen möglicherweise mit der Untergrenze Ihres Schwellenwerts herumspielen, um Ergebnisse zu erzielen, die für Sie funktionieren:

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

Dann subtrahieren wir diese beiden Matten, um das Endergebnis unserer "Hinweis" -Form zu erhalten:

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

Auch hier würde es so aussehen, wenn wir Cv2.ImShow unknown

enter image description here

Nett! Das war einfach für mich, meinen Kopf herum zu wickeln. Der nächste Teil hat mich allerdings ziemlich verwirrt. Schauen wir uns an, wie wir unseren "Hinweis" in etwas verwandeln, das die Funktion Watershed verwenden kann. Dafür müssen wir ConnectedComponents verwenden, was im Grunde genommen eine große Matrix von Pixeln ist, die aufgrund ihres Indexes gruppiert sind. Wenn wir zum Beispiel eine Matte mit den Buchstaben "HI" hätten, könnte ConnectedComponents diese Matrix zurückgeben:

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

Also, 0 ist der Hintergrund, 1 ist der Buchstabe "H" und 2 ist der Buchstabe "I". (Wenn Sie an diesem Punkt angelangt sind und Ihre Matrix visualisieren möchten, empfehle ich das Auschecken von diese instruktive Antwort .) Nun verwenden wir ConnectedComponents, um die Marker zu erstellen (oder Etiketten) für Wasserscheide:

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

Beachten Sie, dass die Watershed-Funktion erfordert, dass der Randbereich mit 0 markiert ist. Daher haben wir alle Randpixel im Label/Marker-Array auf 0 gesetzt.

Zu diesem Zeitpunkt sollten wir alle so eingestellt sein, dass wir Watershed aufrufen. In meiner speziellen Anwendung ist es jedoch hilfreich, während dieses Aufrufs nur einen kleinen Teil des gesamten Quellbilds zu visualisieren. Dies kann für Sie optional sein, aber ich maskiere zunächst nur ein kleines Stück der Quelle, indem ich sie erweitere:

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

Und dann machen Sie den Zauberruf:

Cv2.Watershed(sourceCrop, labels);

Ergebnisse

Der obige Aufruf von Watershed ändert labels an Ort und Stelle. Sie müssen sich wieder an die Matrix erinnern, die sich aus ConnectedComponents ergibt. Der Unterschied besteht darin, dass Wassereinzugsgebiete, die Staudämme zwischen den Wassereinzugsgebieten gefunden haben, in dieser Matrix mit "-1" gekennzeichnet werden. Wie das Ergebnis ConnectedComponents werden verschiedene Wassereinzugsgebiete in ähnlicher Weise mit inkrementierenden Zahlen markiert. Für meine Zwecke wollte ich diese in separaten Konturen speichern, daher habe ich diese Schleife erstellt, um sie aufzuteilen:

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

    }
}

Dann wollte ich diese Konturen mit zufälligen Farben drucken, also habe ich die folgende Matte erstellt:

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

Was bei der Darstellung folgendes ergibt:

enter image description here

Wenn wir auf dem Quellbild die Staudämme zeichnen, die zuvor mit -1 markiert waren, erhalten wir Folgendes:

enter image description here

Bearbeitungen:

Ich habe vergessen zu beachten: Stellen Sie sicher, dass Sie Ihre Matten aufräumen, nachdem Sie mit ihnen fertig sind. Sie bleiben im Speicher und OpenCVSharp zeigt möglicherweise eine unverständliche Fehlermeldung an. Ich sollte wirklich using verwenden, aber mat.Release() ist auch eine Option.

Die obige Antwort von mmgp enthält auch die folgende Zeile: dt = ((dt - dt.min()) / (dt.max() - dt.min()) * 255).astype(numpy.uint8), ein Schritt zum Dehnen des Histogramms, der auf die Ergebnisse der Abstandstransformation angewendet wird. Ich habe diesen Schritt aus einer Reihe von Gründen weggelassen (hauptsächlich, weil ich nicht dachte, dass die Histogramme, die ich sah, zu eng waren), aber Ihr Kilometerstand kann variieren.

5
Daniel