Jak zdefiniować znaczniki Watershed w OpenCV?

Piszę dla Androida z OpenCV. Segmentuję obrazek podobny do poniższego za pomocą sterowanego markerem, bez ręcznego znakowania obrazu przez użytkownika. Planuję użyć Maksima Regionalnego jako znaczników.

minMaxLoc() dałoby mi wartość, ale jak mogę ograniczyć ją do blobów, co jest tym, co mnie interesuje? Czy mogę wykorzystać wyniki z obiektów blob findContours() lub cvblob, aby ograniczyć zwrot z inwestycji i zastosować maxima do każdego obiektu blob?

obraz wejściowy

Author: Harriv, 2012-07-02

3 answers

Po pierwsze: funkcja minMaxLoc Znajduje tylko globalne minimum i globalne maksimum dla danego wejścia, więc jest w większości bezużyteczna do określania regionalnych minimów i / lub regionalnych maksimum. Ale twój pomysł ma rację, wydobywanie znaczników na podstawie regionalnych minimów/maksimów do wykonywania przełomowej transformacji opartej na znacznikach jest całkowicie w porządku. Pozwól, że postaram się wyjaśnić, co to jest transformacja Watershed i jak należy poprawnie korzystać z implementacji obecnej w OpenCV.

Przyzwoita ilość papierów ta sprawa z watershed opisuje ją podobnie do tego, co poniżej (mogę pominąć jakiś szczegół, jeśli nie jesteś pewien: zapytaj). Rozważ powierzchnię jakiegoś regionu, który znasz, zawiera doliny i szczyty (między innymi szczegóły, które są dla nas nieistotne tutaj). Załóżmy, że pod tą powierzchnią wszystko, co masz, to woda, kolorowa woda. Teraz zrób otwory w każdej dolinie swojej powierzchni, a wtedy woda zaczyna wypełniać cały obszar. W pewnym momencie spotkają się różnokolorowe wody, a kiedy to się stanie, konstruujesz dam taki, że się nie dotykają. W końcu masz kolekcję zapór, która jest wododział oddzielający wszystkie różnokolorowe wody.

Jeśli zrobisz zbyt wiele dziur w tej powierzchni, skończysz z zbyt wieloma regionami: nadmierną segmentacją. Jeśli zrobisz zbyt mało, dostaniesz niedostateczną segmentację. Tak więc praktycznie każdy papier, który sugeruje użycie watershed, przedstawia techniki, aby uniknąć tych problemów w aplikacji, z którą ma do czynienia.

Napisałem to wszystko (co jest prawdopodobnie zbyt naiwne dla każdego, kto wie, czym jest transformacja Watershed), ponieważ bezpośrednio odzwierciedla to, jak powinieneś używać implementacji watershed (co obecna akceptowana odpowiedź robi w zupełnie niewłaściwy sposób). Zacznijmy teraz Od przykładu OpenCV, używając wiązań Pythona.

[3]}obraz przedstawiony w pytaniu składa się z wielu obiektów, które w większości są zbyt blisko, a w niektórych przypadkach nakładają się na siebie. Przydatność tego rozwiązania polega na prawidłowym oddzieleniu tych obiektów, aby nie grupować ich w jeden komponent. Potrzebujesz więc co najmniej jednego znacznika dla każdego obiektu i dobrych znaczników dla tła. Jako przykład, najpierw binaryzuj obraz wejściowy przez Otsu i wykonaj morfologiczne otwarcie do usuwania małych obiektów. Wynik tego kroku jest pokazany poniżej na lewym obrazku. Teraz z obrazem binarnym Rozważ zastosowanie transformacji odległości do niego, wynik po prawej stronie.

Tutaj wpisz opis obrazkaTutaj wpisz opis obrazka

Z wynikiem przekształcenia odległości możemy rozważyć jakiś próg taki, że rozważamy tylko regiony najbardziej odległe do tła(lewy obrazek poniżej). W ten sposób możemy uzyskać znacznik dla każdego obiektu, oznaczając różne regiony po wcześniejszym progu. Teraz możemy również rozważyć obramowanie rozszerzonej wersji lewego obrazu powyżej, aby skomponować nasz znacznik. Kompletny znacznik jest pokazany poniżej po prawej stronie (niektóre znaczniki są zbyt ciemne, aby je zobaczyć, ale każdy biały obszar na lewym obrazku jest reprezentowany po prawej stronie obraz).

Tutaj wpisz opis obrazkaTutaj wpisz opis obrazka

Ten znacznik ma sens. Każdy colored water == one marker zacznie wypełniać region, a transformacja wododziałowa zbuduje zapory, aby utrudnić łączenie się różnych "kolorów". Jeśli zrobimy transformację, otrzymamy obraz po lewej stronie. Biorąc pod uwagę tylko zapory, komponując je z oryginalnym obrazem, otrzymujemy wynik w prawo.

Tutaj wpisz opis obrazkaTutaj wpisz opis obrazka

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
Author: mmgp,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-03-10 17:50:13

Chciałbym wyjaśnić prosty kod, Jak korzystać z watershed tutaj. Używam OpenCV-Python, ale mam nadzieję, że nie będziesz miał trudności ze zrozumieniem.

W tym kodzie użyję watershed jako narzędzia do ekstrakcji foreground-background. (ten przykład jest odpowiednikiem kodu C++ w OpenCV cookbook). Jest to prosty przypadek do zrozumienia. Poza tym możesz użyć watershed, aby zliczyć liczbę obiektów na tym obrazie. To będzie nieco zaawansowana wersja tego kodu.

1 - najpierw załadujemy nasz obraz, przekonwertujemy go na skalę szarości i dodajemy odpowiednią wartość. Wziąłem binaryzację Otsu , aby znaleźć najlepszą wartość progową.

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)

Poniżej otrzymałem wynik:

Tutaj wpisz opis obrazka

( nawet ten wynik jest dobry, ponieważ świetny kontrast między obrazami pierwszego planu i tła)

2 - Teraz musimy stworzyć znacznik. znacznik jest obrazem o tej samej wielkości podobnie jak oryginalny obraz, który jest 32SC1 (32 bit signed single channel).

Teraz na oryginalnym obrazie pojawią się regiony, w których będziesz po prostu pewien, że ta część należy do pierwszego planu. Zaznacz taki obszar za pomocą 255 na obrazku znacznika. Teraz region, w którym na pewno będziesz tłem, jest oznaczony liczbą 128. Region, którego nie jesteś pewien, jest oznaczony jako 0. To zrobimy teraz.

A-region pierwszoplanowy: - mamy już obraz progu, w którym są koloru białego. Erodujemy je trochę, tak, że jesteśmy pewni, że pozostały region należy do pierwszego planu.

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

Fg :

Tutaj wpisz opis obrazka

B-obszar tła: - tutaj rozszerzamy próg obrazu, aby zmniejszyć obszar tła. Ale jesteśmy pewni, że pozostały czarny region to 100% tło. Ustawiliśmy na 128.

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

Teraz otrzymujemy bg następująco:

Tutaj wpisz opis obrazka

C - teraz dodajemy zarówno fg jak i bg :

marker = cv2.add(fg,bg)

Poniżej otrzymujemy :

Tutaj wpisz opis obrazka

Teraz możemy wyraźnie zrozumieć z powyższego obrazu, że biały obszar to 100% pierwszy plan, szary obszar to 100% tło, a czarny obszar nie jesteśmy pewni.

Następnie zamieniamy go na 32SC1:

marker32 = np.int32(marker)

3-W końcu zastosujemy i przekonwertujemy wynik z powrotem na uint8 image:

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

M:

Tutaj wpisz opis obrazka

4 - we threshold it right to get maska i wykonaj {[7] } z obrazem wejściowym:

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

Res:

Tutaj wpisz opis obrazka

Mam nadzieję, że to pomoże!!!

ARK

 43
Author: Abid Rahman K,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-05-08 15:40:00

Przedmowa

Włączam się głównie dlatego, że zarównowatershed tutorial w dokumentacji OpenCV (iC++ example ), jak iodpowiedź mmgp powyżej jest dość myląca. Wielokrotnie powracałem do przełomowego podejścia, aby ostatecznie zrezygnować z frustracji. W końcu zdałem sobie sprawę, że muszę przynajmniej spróbować tego podejścia i zobaczyć go w akcji. To jest to, co wymyśliłem po uporządkowaniu wszystkich samouczków, które mam natknąć się.

Poza byciem początkującym komputerowym widzeniem, większość moich problemów prawdopodobnie miała związek z moim wymogiem używania biblioteki OpenCVSharp zamiast Pythona. C# nie ma takich operatorów jak te Znalezione w NumPy (choć zdaję sobie sprawę, że zostało to przeportowane przez IronPython), więc ciężko mi było zrozumieć i zaimplementować te operacje w C#. Ponadto, dla przypomnienia, naprawdę gardzę niuansami i niespójnościami w większości z tych wywołania funkcji. OpenCVSharp jest jedną z najbardziej kruchych bibliotek, z którymi pracowałem. Ale hej, to port, więc czego się spodziewałem? Najlepsze jest to, że jest za darmo.

Bez zbędnych ceregieli, porozmawiajmy o mojej implementacji opencvsharp w watershed i miejmy nadzieję, że wyjaśnię niektóre bardziej stickerowe punkty implementacji watershed w ogóle.

Zastosowanie

Przede wszystkim upewnij się, że watershed jest tym, czego chcesz i zrozum jego użycie. Używam poplamione płytki komórkowe, jak ta:

Tutaj wpisz opis obrazka

Zajęło mi to sporo czasu, zanim zorientowałem się, że nie mogę wykonać jednego połączenia, aby odróżnić każdą komórkę w terenie. Wręcz przeciwnie, najpierw musiałem wyizolować część pola, a następnie wywołać podział na tę małą część. Odizolowałem Mój region zainteresowania (ROI) za pomocą wielu filtrów, które wyjaśnię krótko tutaj: [27]}

Tutaj wpisz opis obrazka

  1. zacznij od obrazu źródłowego( w lewo, przycięty do celów demonstracyjnych)
  2. odizoluj czerwony kanał (lewy środkowy)
  3. Apply Adaptive threshold (right middle)
  4. Znajdź kontury, a następnie wyeliminuj te z małymi obszarami (po prawej)

Po oczyszczeniu konturów wynikających z powyższych operacji progowych, nadszedł czas, aby znaleźć kandydatów do rozlewu wody. W moim przypadku po prostu przeszedłem przez wszystkie kontury większe niż określony obszar.

Kod

Powiedzmy, że odizolowaliśmy to kontur z powyższego pola jako nasz ROI:

Tutaj wpisz opis obrazka

[24]}przyjrzyjmy się, jak zakodujemy dział wodny.

Zaczniemy od pustej Maty i narysujemy tylko kontur określający nasz zwrot z inwestycji:

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

Aby zadzwonić do działu wodnego do pracy, będzie potrzebował kilku "wskazówek" na temat zwrotu z inwestycji. Jeśli jesteś kompletnym początkującym jak ja, polecam zajrzeć na stronę CMM watershed page dla szybkiego podkładu. Wystarczy powiedzieć, że stworzymy podpowiedzi po lewej stronie, tworząc kształt po prawej:

Tutaj wpisz opis obrazka

Aby utworzyć białą część (lub" tło") tego kształtu" podpowiedzi", po prostu Dilate odizolowany kształt w ten sposób:
var kernel = Cv2.GetStructuringElement(MorphShapes.Ellipse, new Size(2, 2));
var background = new Mat();
Cv2.Dilate(isolatedContour, background, kernel, iterations: 8);

Aby utworzyć czarną część w środku( lub "na pierwszym planie"), użyjemy transformacji odległości, po której następuje próg, który przenosi nas od kształtu po lewej do kształtu po prawej:

Tutaj wpisz opis obrazka

To wymaga kilku kroków, a Ty może być konieczne pobranie dolnej granicy progu, aby uzyskać wyniki, które działają dla Ciebie]}

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

Następnie odejmujemy te dwie maty, aby uzyskać ostateczny wynik naszego kształtu "podpowiedzi":

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

Ponownie, jeśli Cv2.ImShow unknown , wyglądałoby to tak:

Tutaj wpisz opis obrazka

Nieźle! Łatwo było mi to ogarnąć. Kolejna część mnie jednak zaskoczyła. Przyjrzyjmy się zamianie naszej "podpowiedzi" w coś Watershed funkcja może się przydać. W tym celu musimy użyć ConnectedComponents, która jest w zasadzie dużą macierzą pikseli pogrupowanych według wartości ich indeksu. Na przykład, jeśli mamy matę z literami "cześć", ConnectedComponents może zwrócić tę macierz:
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

Zatem 0 to tło, 1 to litera "H", A 2 to litera"I". (Jeśli dojdziesz do tego punktu i chcesz wizualizować swoją matrycę, polecam sprawdzenie tej pouczającej odpowiedzi.) Teraz, oto jak wykorzystamy ConnectedComponents do tworzenia znaczników (lub etykiet) dla wątek:

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

Zauważ, że funkcja Watershed wymaga, aby obszar graniczny był oznaczony przez 0. Tak więc ustawiliśmy dowolne piksele obramowania na 0 w tablicy etykiet/znaczników.

W tym momencie powinniśmy być gotowi do wywołania Watershed. Jednak w mojej konkretnej aplikacji przydatne jest tylko wizualizacja niewielkiej części całego obrazu źródłowego podczas tego wywołania. To może być opcjonalne dla ciebie, ale najpierw zamaskuję mały fragment źródła, rozszerzając go: {]}

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

I wtedy wykonaj magiczny Telefon:

Cv2.Watershed(sourceCrop, labels);

Wyniki

Powyższe wywołanie Watershed zmieni labels na miejscu . Będziesz musiał wrócić do pamiętania o matrycy wynikającej z ConnectedComponents. Różnica polega na tym, że jeśli wododział znajdzie jakieś zapory między wodami, będą one oznaczone jako " -1 " w tej macierzy. Podobnie jak wynik ConnectedComponents, różne zbiorniki wodne będą oznaczone w podobny sposób przyrostami liczb. Dla swoich celów, chciałem przechowywać je w osobnych kontury, więc stworzyłem tę pętlę, aby je rozdzielić:

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

    }
}
Następnie chciałem wydrukować te kontury przypadkowymi kolorami, więc stworzyłem następującą matę:]}
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);
    }
}

Co daje następujące po pokazaniu:

Tutaj wpisz opis obrazka

Jeśli narysujemy na źródłowym obrazie zapory, które wcześniej oznaczono a -1, otrzymamy to:

Tutaj wpisz opis obrazka

Edits:

Zapomniałem zauważyć: upewnij się, że czyścisz maty po skończyłeś z nimi. Pozostaną w pamięci, a OpenCVSharp może wyświetlać niezrozumiały komunikat o błędzie. Naprawdę powinienem używać using powyżej, ale {[22] } jest również opcją.

Odpowiedź mmgp powyżej zawiera również tę linię: dt = ((dt - dt.min()) / (dt.max() - dt.min()) * 255).astype(numpy.uint8), która jest histogramem rozciągającym stosowanym do wyników transformacji odległości. Pominąłem ten krok z wielu powodów (głównie dlatego, że nie sądziłem, że histogramy, które widziałem, są zbyt wąskie, aby zacząć), ale twój przebieg może / align = "left" /

 1
Author: Daniel,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-06-22 22:13:07