Vorwort
Ich stimme hauptsächlich zu, weil ich sowohl das Tutorial zur Wasserscheide in der OpenCV-Dokumentation (und im C ++ - Beispiel ) als auch die Antwort von mmgp oben als ziemlich verwirrend empfunden habe . 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. Dies ist, was ich mir ausgedacht habe, nachdem ich alle Tutorials sortiert habe, auf die ich gestoßen bin.
Abgesehen davon, dass ich ein Anfänger im Bereich Computer Vision war, 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 zu implementieren. Außerdem verachte ich die Nuancen und Inkonsistenzen in den meisten dieser Funktionsaufrufe wirklich. OpenCVSharp ist eine der fragilsten Bibliotheken, mit denen ich je gearbeitet habe. Aber hey, es ist ein Hafen, also was habe ich erwartet? Das Beste ist jedoch, dass es kostenlos ist.
Lassen Sie uns ohne weiteres über meine OpenCVSharp-Implementierung der Wasserscheide sprechen und hoffentlich einige der wichtigsten Punkte der Implementierung der Wasserscheide im Allgemeinen klären.
Anwendung
Stellen Sie zunächst sicher, dass die Wasserscheide das ist, was Sie wollen, und verstehen Sie ihre Verwendung. Ich benutze gefärbte Zellplatten wie diese:
Ich brauchte eine Weile, um herauszufinden, dass ich nicht nur einen Wassereinzugsgebietsanruf tätigen konnte, um jede Zelle auf dem Feld zu unterscheiden. Im Gegenteil, ich musste zuerst einen Teil des Feldes isolieren und dann diesen kleinen Teil als Wasserscheide bezeichnen. Ich habe meine Region of Interest (ROI) über eine Reihe von Filtern isoliert, die ich hier kurz erläutern werde:
- Beginnen Sie mit dem Quellbild (links, zu Demonstrationszwecken zugeschnitten)
- Isolieren Sie den roten Kanal (links in der Mitte)
- Adaptive Schwelle anwenden (rechts in der Mitte)
- Finden Sie Konturen und entfernen Sie diese mit kleinen Flächen (rechts).
Sobald wir die Konturen gereinigt haben, die sich aus den oben genannten Schwellenwertoperationen ergeben, ist es Zeit, Kandidaten für die Wasserscheide 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 unseren ROI isoliert:
Werfen wir einen Blick darauf, wie wir eine Wasserscheide codieren.
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 zur Wasserscheide funktioniert, sind einige "Hinweise" zum ROI erforderlich. Wenn Sie ein absoluter Anfänger wie ich sind, empfehle ich Ihnen, die CMM-Wasserscheide-Seite zu lesen, um eine kurze Einführung zu erhalten. Es genügt zu sagen, dass wir links Hinweise zum ROI erstellen, indem wir rechts die Form erstellen:
Um den weißen Teil (oder "Hintergrund") dieser "Hinweis" -Form zu erstellen, verwenden wir nur Dilate
die isolierte Form wie folgt:
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 im "Vordergrund") zu erstellen, verwenden wir eine Abstandstransformation gefolgt von einem Schwellenwert, der uns von der Form links zur Form rechts führt:
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);
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();
Cv2.Subtract(background, foreground, unknown);
Wenn wir es Cv2.ImShow
nicht wissen , würde es wieder so aussehen:
Nett! Das war leicht für mich, meinen Kopf herumzuwickeln. Der nächste Teil hat mich jedoch ziemlich verwirrt. Schauen wir uns an, wie wir unseren "Hinweis" in etwas verwandeln, das die Watershed
Funktion verwenden kann. Hierfür müssen wir verwenden ConnectedComponents
, was im Grunde eine große Matrix von Pixeln ist, die aufgrund ihres Index gruppiert sind. Wenn wir beispielsweise eine Matte mit den Buchstaben "HI" hätten, ConnectedComponents
könnte diese Matrix zurückgegeben werden:
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
0 ist also 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, empfehlen wir Ihnen , diese lehrreiche Antwort zu lesen .) So ConnectedComponents
erstellen wir nun die Markierungen (oder Beschriftungen) für die Wasserscheide:
var labels = new Mat();
Cv2.ConnectedComponents(foreground, labels);
labels = labels + 1;
for (int x = 0; x < labels.Width; x++)
{
for (int y = 0; y < labels.Height; y++)
{
var labelPixel = (int)labels.At<char>(y, x);
var borderPixel = (int)unknown.At<char>(y, x);
if (borderPixel == 255)
labels.Set(y, x, 0);
}
}
Beachten Sie, dass für die Watershed-Funktion der Randbereich mit 0 markiert sein muss. Daher haben wir alle Randpixel im Beschriftungs- / Markierungsarray auf 0 gesetzt.
An diesem Punkt sollten wir alle bereit sein anzurufen Watershed
. In meiner speziellen Anwendung ist es jedoch nützlich, nur einen kleinen Teil des gesamten Quellbilds während dieses Aufrufs zu visualisieren. Dies mag für Sie optional sein, aber ich maskiere zuerst nur einen kleinen Teil 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 magischen Ruf:
Cv2.Watershed(sourceCrop, labels);
Ergebnisse
Der obige Watershed
Aufruf wird labels
an Ort und Stelle geändert . Sie müssen sich wieder an die Matrix erinnern, die sich daraus ergibt ConnectedComponents
. Der Unterschied besteht darin, dass Wassereinzugsgebiete, die Dämme zwischen Wassereinzugsgebieten gefunden haben, in dieser Matrix als "-1" markiert werden. Wie das ConnectedComponents
Ergebnis werden verschiedene Wassereinzugsgebiete auf ähnliche Weise mit inkrementierenden Zahlen markiert. Für meine Zwecke wollte ich diese in separaten Konturen speichern, deshalb 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);
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 ergibt, wenn gezeigt wird:
Wenn wir auf dem Quellbild die Dämme zeichnen, die zuvor mit -1 markiert waren, erhalten wir Folgendes:
Bearbeitungen:
Ich habe vergessen zu beachten: Stellen Sie sicher, dass Sie Ihre Matten aufräumen, nachdem Sie damit fertig sind. Sie bleiben im Speicher und OpenCVSharp zeigt möglicherweise eine unverständliche Fehlermeldung an. Ich sollte wirklich using
oben verwenden, ist aber auch mat.Release()
eine Option.
Die obige Antwort von mmgp enthält auch die folgende Zeile: Dies dt = ((dt - dt.min()) / (dt.max() - dt.min()) * 255).astype(numpy.uint8)
ist ein Histogramm- Streckschritt , der auf die Ergebnisse der Entfernungstransformation angewendet wird. Ich habe diesen Schritt aus mehreren Gründen weggelassen (hauptsächlich, weil ich nicht dachte, dass die Histogramme, die ich gesehen habe, zu eng waren), aber Ihr Kilometerstand kann variieren.