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?
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.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).
Ten znacznik ma sens. Każdycolored 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.
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)
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:
( 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 :
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:
C - teraz dodajemy zarówno fg jak i bg :
marker = cv2.add(fg,bg)
Poniżej otrzymujemy :
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:
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:
Mam nadzieję, że to pomoże!!!
ARK
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:
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]}- zacznij od obrazu źródłowego( w lewo, przycięty do celów demonstracyjnych)
- odizoluj czerwony kanał (lewy środkowy)
- Apply Adaptive threshold (right middle)
- 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:
[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:
Aby utworzyć białą część (lub" tło") tego kształtu" podpowiedzi", po prostuDilate
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:
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:
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:
Jeśli narysujemy na źródłowym obrazie zapory, które wcześniej oznaczono a -1, otrzymamy to:
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" /
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