Kursy • Poradniki • Inspirujące DIY • Forum
« Poprzedni artykuł z seriiNastępny artykuł z serii »
Przestrzenie barw
Najpierw postaram się przybliżyć zagadnienie przestrzeni barw. Większość z nas miała już pewnie styczność lub słyszała o modelu RGB, w którym każdy kolor jest reprezentowany przez trzy składowe (czerwoną, zieloną, niebieską) przy czym każda z nich przyjmuje wartość od 0 do 255.
Model RGB możemy traktować jako przestrzeń trójwymiarową, gdzie kolejnym osiom przypisujemy kolejne składowe kolory. Reprezentuje to poniższa ilustracja, na której zaznaczono umiejscowienie koloru (80, 200, 130) w przestrzeni RGB.
Model ten jest bardzo często używany w informatyce, przy zapisie obrazów i ich wyświetlaniu, również w technice cyfrowej: telewizory, monitory, aparaty itp. W OpenCV obrazy przechowywane są w obiektach typu cv:Mat, które budowane są na wzór macierzy, a kolor zapisany jest w formacie BGR, a więc trzeba pamiętać, że mamy tu do czynienia z zamianą miejscami koloru niebieskiego i czerwonego.
Przy wykrywaniu kolorowych obiektów, znacznie lepszy okazuje się model HSV, którego składowymi są ( hue – brawa, saturation – nasycenie, value – wartość ). Reprezentacje geometryczną tego modelu stanowi stożek lub cylinder. Choć na pierwszy rzut oka wydaje się to dziwne, jest jednak jak najbardziej naturalne gdyż model ten nawiązuje do sposobu, w jakim widzi ludzki narząd wzroku. Zgodnie z tym modelem wszystkie barwy wywodzą się ze światła białego (środek stożka ).
Jak widzimy na powyższym rysunku składowa H – czyli barwa (hue) jest określana jako kąt od 0 do 360 stopni i określa barwę jaką ma dany kolor. Składowa S – nasycenie ( saturation), czyli odległość od środka na promieniu podstawy odkreśla nam nasycenie koloru, a więc im mniejsza wartość tym bledszy (bliższy białemu) nasz kolor. Natomiast składowa V – wartość ( value ) oznacza wysokość na stożku, a właściwie możemy traktować tą wartość jako jasność naszego koloru, im mniejsza jej wartość tym kolor ciemniejszy.
Co czyni model HSV lepszym od RGB w tym przypadku?
Jeśli popatrzymy chwilę na sposób w jakim grupowane są poszczególne barwy w model RGB i HSV szybko znajdziemy wyjaśnienie.
Otóż chodzi o to, że dla modelu HSV, aby określić dany kolor wystarczy jedna składowa H. Na przykład, kolor żółty na stożku wyżej jest zgrupowany w jednym miejscu, wystarczy wskazać zakres kątów odpowiadających żółtemu, przykładowo od 15 do 30 stopni, aby wyselekcjonować obiekty o danym kolorze z naszego obrazu. W programach graficznych takich jak np. GIMP przy wyborze kolorów możemy przejść do trybu HSV, zobaczymy wówczas następujący diagram.
Jest to nic innego jak nasz stożek rozłożony na dwa wymiary. Obręcz stanowi reprezentację podstawy stożka, a trójkąt przekrój przez stożek wzdłuż jego wysokości. Widzimy, że odcienie koloru żółtego są zgrupowane w jednym miejscu i odpowiednio dobierając zakres kątów można określić obszar występowania koloru żółtego. Poniżej zostało to zobrazowane.
Gdybyśmy chcieli zrobić to samo przy użyciu modelu RGB mielibyśmy pewien trójwymiarowy rejon w którym zgrupowany jest kolor żółty. Musielibyśmy podać ograniczenia jako skomplikowane funkcje, aby nasz program działał zgodnie z oczekiwaniami, co utrudnia sprawę i obliczenia.
Aby zrozumieć sprawę jeszcze lepiej popatrzmy jak wygląda poniższy obrazek w wersji hsv:
Natomiast po rozbiciu obrazka na jego poszczególne składowe i wyświetleniu każdej z nich z osobna otrzymujemy co następuje:
Szczególnie pierwszy obrazek reprezentujący składową hue jest dla nas istotny, gdyż pokazuje, że różne odcienie żółtego na liściach słonecznika, na obrazku reprezentującym barwę hue stanowią praktyczne ten sam odcień.
Oczywiście model HSV nie jest też idealny. Ma także swoje wady, gdyż nie pozwala na pełne wyeliminowanie wpływu zmieniającego się oświetlenia na reprezentację kolorów. Jednak jego zalety są na tyle duże, że posłużymy się nim aby wykrywać obiekt o określonym kolorze.
Konwersja przestrzeni barw w OpenCV
Program ilustruje sposób przejścia z przestrzeni barw BGR do HSV w OpenCV.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
#include <opencv2/highgui/highgui.hpp> //Dołączenie potrzebnych nagłówków #include "opencv2/imgproc/imgproc.hpp" #include <string> #include <iostream> using namespace cv; //Przestrzeń nazw OpenCV using namespace std; int main() { const string file_name = "sunflower.jpg"; //Nazwa obrazka Mat img, hsv; //Miejsce na obrazki vector<Mat> img_split; //Miejsce na składowe HSV img = imread(file_name); //Wczytanie obrazka if ( !img.data ) //Sprawdzenie poprawnosci odczytu { cout << "Nie znaleziono pliku " << file_name << "."; return -1; } const string named_window[] = {"BGR", "HSV", "HUE", "SAT", "VAL" }; namedWindow(named_window[0], CV_WINDOW_AUTOSIZE); //Utworzenie okien namedWindow(named_window[1], CV_WINDOW_AUTOSIZE); namedWindow(named_window[2], CV_WINDOW_AUTOSIZE); namedWindow(named_window[3], CV_WINDOW_AUTOSIZE); namedWindow(named_window[4], CV_WINDOW_AUTOSIZE); cvtColor(img, hsv, CV_BGR2HSV ); /Konwersja BGR -> HSV split(hsv, img_split); //Rozdzielenie HSV na poszczególne kanały imshow(named_window[0], img); //Obraz oryginalny imshow(named_window[1], hsv); //Obraz w wersji HSV imshow(named_window[2], img_split[0]); /Barwa imshow(named_window[3], img_split[1]); //Nasycenie imshow(named_window[4], img_split[2]); //Wartosc vector<int> compression_params; //Element przechowujący dane o sposobie zapisu compression_params.push_back(CV_IMWRITE_JPEG_QUALITY); //Konwersja jpg compression_params.push_back(100); //Jakosc 100 imwrite("hsv.jpg", hsv, compression_params); //Zapis poszczegolnych obrazow imwrite("hue.jpg", img_split[0], compression_params); imwrite("sat.jpg", img_split[1], compression_params); imwrite("val.jpg", img_split[2], compression_params); waitKey(); //Oczekiwanie na wciesniecie klawisza return 0; } |
Na początek do naszego programu musimy dołączyć odpowiednie moduły OpenCV.
1 2 |
#include <opencv2/highgui/highgui.hpp> #include "opencv2/imgproc/imgproc.hpp" |
W highgui znajdują się funkcje pomocne przy tworzeniu interfejsu: okien, trackbar'ów, obsługa myszki itp. W imgproc znajdują się funkcje wykorzystywane do przetwarzania obrazów.
1 |
using namespace cv; |
Używamy przestrzeni nazw opencv.
1 |
cvtColor(img, hsv, CV_BGR2HSV ); |
Funkcja służy do konwersji między wybranymi modelami kolorów. Pierwszym argumentem jest obraz, z którego chcemy dokonać konwersji, drugi to obraz, który otrzymany w wyniku konwersji, ostatnim argumentem jest stała określająca pomiędzy jakimi przestrzeniami barw zostanie dokonana konwersja. CV_BGR2HSV oznacza konwersję z modelu BGR, domyślnego dla OpenCV na model HSV, jest jeszcze czwarty parametr oznaczający ilość kanałów w obrazku skonwertowanym, jeśli go nie podamy ilość kanałów będzie wyliczona automatycznie.
Inne stałe konwersji CV_RGB2GRAY ( RGB → Odcienie szarości ) więcej można znaleźć w dokumentacji OpenCV lub w podpowiedziach eclipse.
1 |
split(hsv, img_split); |
Powoduje podział obrazka, który podajemy jako pierwszy argument na poszczególne kanały. Czyli np. dla RGB otrzymamy trzy obrazki, każdy zawierający jedną składową R, G, B. Drugim argumentem powinien być wektor typu Mat lub tablica elementów typu Mat.
Wykrywanie kolorowych obiektów w OpenCV
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
#include <opencv2/highgui/highgui.hpp> #include "opencv2/imgproc/imgproc.hpp" #include <string> #include <iostream> using namespace cv; using namespace std; int main() { VideoCapture capture = VideoCapture(0);//Przechwycienie uchwytu kamery o nr. 0 string window_name [] = { "Kamera", "Binary" };//Nazwy dla okien Mat frame, img, hsv_img, binary; //Miejsce na obrazki vector<Mat> hsv_split; //Miejsce na kanały hsv for ( int i = 0; i < 2; i++ ) namedWindow(window_name[i], CV_WINDOW_AUTOSIZE); //Utworzenie 2 okien int lowerb = 100, upperb = 109; //Ustalenie wartosci poczatkowych dla progowania createTrackbar( "lb", window_name[1], &lowerb, 255, NULL ); //Utworzenie trackbar'ow createTrackbar( "ub", window_name[1], &upperb, 255, NULL ); while ( waitKey(20) != 27 ) //Odczekanie 20 ms { capture >> frame; //Pobranie kolejnej klatki frame.copyTo(img); /Skopiowanie klatki do img cvtColor(img, hsv_img, CV_BGR2HSV); //Konwrsja do HSV split(hsv_img, hsv_split); //Podzial HSV na poszczegolne kanaly inRange(hsv_split[0], lowerb, upperb, binary); //Progowanie zgodnie z wartosciami lowerb, i upperb cv::Mat element(3,3,CV_8U,cv::Scalar(1)); //Okreslenie opcji erozji blur(binary, binary, cv::Size(3,3) ); //Rozmycie erode(binary, binary, element); //Erozja imshow(window_name[0], img ); //Obrazek Orginalny imshow(window_name[1], binary); //Obraz binarny } capture.release(); //Zwolnienie uchwytu kamery return 0; } |
Objaśnienia
1 |
createTrackbar( "lb", window_name[1], &lowerb, 255, NULL ); |
Tworzy TrackBar u góry okna. W programie mamy dwa takie paski, za pomocą których określamy wartość dolnego i górnego progu (zakres danego koloru). Pierwszy parametr to nazwa pojawiająca się po lewej stronie paska. Drugi parametr to nazwa okna, do którego zostanie podpięty trackbar, trzeci parametr to zmienna, której odwzorowaniem będzie wartość ustawiona na pasku. Kolejny parametr określa maksymalną możliwą do ustalenia wartość na pasku. Ostatni parametr to wskaźnik na funkcję, która zostanie wywołana przy przesuwaniu suwaka na pasku, tutaj nie jest nam taka funkcja potrzebna więc podajemy wskaźnik pusty.
1 |
frame.copyTo(img); |
Metoda kopiująca jeden obrazek do drugiego.
1 |
inRange(hsv_split[0], lowerb, upperb, binary); |
Funkcja realizująca progowanie. Pierwszym parametrem jest obrazek, który poddamy progowaniu, u nas jest to obrazek reprezentujący składową hue, HSV. Kolejnymi argumentami są dolna i górna granica przedziału do którego musi należeć dany piksel aby w obrazie wyjściowym (binary) miał kolor biały, jeżeli piksel nie należy do tego zakresu przypisywany mu jest kolor czarny.
1 |
erode(binary, binary, cv::Mat() ); |
Funkcja ta powoduje przefiltrowanie naszego obrazka z zastosowaniem filtru erozji. Aby zobrazować działanie filtru wyobraźmy sobie pojedynczy piksel naszego obrazka, nazwijmy go kotwicą natomiast wszystkie piksele otaczające naszą kotwicę. Takie, że się z nią stykają, są jej sąsiedztwem mamy wówczas element 3x3 piksele, gdzie środkowy piksel to kotwice, a pozostałe 8 pikseli wokół niego to sąsiedztwo.
Erozja działa w ten sposób, że zastępuje wartość naszego piksela-kotwicy, minimalną wartością znalezioną wśród pikseli sąsiedztwa. Jeżeli nasz obraz jest binarny, tzn. piksele przyjmują wartości 0 lub 255 to możemy to sobie wyobrazić tak, że w momencie gdy wśród sąsiedztwa kotwicy jest piksel tła (0 – czarny), to kotwica przyjmie kolor czarny, stanie się tłem.
Powoduje to zredukowanie rozmiarów obiektu, ale też pozwala na zredukowanie lub usunięcie szumów z obrazka, które pojawiają się często w postaci białych pojedynczych pikseli lub ich małych grupek otoczonych przez piksele tła. W OpenCV domyślnie element filtru ma wartość 3x3 piksele (przy podaniu cv::Mat() jako trzeci argument), można jednak zwiększyć jego rozmiary np. do 7x7 w następujący sposób:
1 2 |
cv::Mat element(7,7,CV_8U,cv::Scalar(1)); erode(binary, binary, element); |
Mamy wówczas element 7x7 lub możemy zrobić coś takiego:
1 |
erode(binary,binary,cv::Mat(),cv::Point(-1,-1), 3); |
Gdzie ostatni parametr wskazuje, że erozja zostanie na obrazku dokonana 3-krotnie.
1 |
dilate(binary, binary, cv::Mat()); |
Operacją odwrotną do erozji jest dylacja. Jeżeli jeden z pikseli sąsiedztwa ma wartość białą, to kotwica też taką przyjmie.
1 |
blur(binary, binary, cv::Size(3,3)); |
Funkcja powoduje rozmycie obrazka podanego jako pierwszy argument, drugim argumentem jest obrazek, do którego zostanie wpisany obraz powstały w wyniku rozmycia. Trzecim parametrem jest pewien kwadratowy obszar, do kotwicy tego obszaru zostanie wpisana średnia wartość pikseli sąsiedztwa.
Wykrywanie konturu
Możemy teraz przystąpić do kolejnej rzeczy jaką będzie wykrycie kontur obiektu na otrzymanym przez progowanie obrazie binarnym.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
#include <opencv2/highgui/highgui.hpp> #include "opencv2/imgproc/imgproc.hpp" #include <string> #include <iostream> using namespace cv; using namespace std; int main() { VideoCapture capture = VideoCapture(0); string window_name [] = { "Kamera", "Contour", "Binary" }; Mat frame, img, hsv_img, binary; //*** Mat cont; //*** vector<Mat> hsv_split; for ( int i = 0; i < 3; i++ ) namedWindow(window_name[i], CV_WINDOW_AUTOSIZE); int lowerb = 100, upperb = 109; createTrackbar( "Thresh lb", window_name[2], &lowerb, 255, NULL ); createTrackbar( "Thresh ub", window_name[2], &upperb, 255, NULL ); while ( waitKey(20) != 27 ) { capture >> frame; frame.copyTo(img); cvtColor(img, hsv_img, CV_BGR2HSV); split(hsv_img, hsv_split); inRange(hsv_split[0], lowerb, upperb, binary); blur(binary, binary, cv::Size(3,3) ); erode(binary, binary, cv::Mat() ); //*** vector<vector<Point> > contours; vector<Point> contours_poly; Rect boundRect; binary.copyTo(cont); findContours( cont, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE, Point(0, 0) ); int max = 0, i_cont = -1; Mat drawing = Mat::zeros( cont.size(), CV_8UC3 ); for( int i = 0; i< contours.size(); i++ ) { if ( abs(contourArea(Mat(contours[i]))) > max ) { max = abs(contourArea(Mat(contours[i]))); i_cont = i; } } if ( i_cont >= 0 ) { approxPolyDP( Mat(contours[i_cont]), contours_poly, 3, true ); boundRect = boundingRect( Mat(contours_poly) ); fillConvexPoly(img, contours_poly, contours_poly.size() ); rectangle( img, boundRect.tl(), boundRect.br(), Scalar(125, 250, 125), 2, 8, 0 ); line( img, boundRect.tl(), boundRect.br(), Scalar(250, 125, 125), 2, 8, 0); line( img, Point(boundRect.x + boundRect.width, boundRect.y), Point(boundRect.x, boundRect.y + boundRect.height), Scalar(250, 125, 125), 2, 8, 0); string s; stringstream out; out << boundRect.x + boundRect.width/2 << "x" << boundRect.y + boundRect.height/2; s = out.str(); putText( img, s, Point(50, 50), CV_FONT_HERSHEY_COMPLEX, 1, Scalar(20, 40, 80), 3, 8 ); drawContours( drawing, contours, i_cont, Scalar(125, 125, 250), 2 ); } imshow(window_name[1], drawing); //*** imshow(window_name[0], img ); imshow(window_name[2], binary); } capture.release(); return 0; } |
Nowy kod obejmuje dodanie dodatkowej zmiennej cont do przechowywania kopii obrazu binarnego.
1 |
vector<vector<Point> > contours; |
Tutaj będziemy przechowywać wszystkie kontury obiektów jakie znajdziemy w naszym obrazie binarnym. Kontury te są przechowywane jako punkty. Potrzebujemy więc wektora punktów, jednak z racji, że na obrazie może pojawić się więcej niż jeden obiekt, a więc możemy mieć kilka oddzielnych konturów, potrzebujemy wektora do ich przechowania a więc ostatecznie nasze kontury będziemy przechowywać w wektorze, wektorów punktów.
1 |
vector<Point> contours_poly; |
Naszym celem będzie stworzenie obrysu prostokątnego naszego konturu, aby tego dokonać będziemy kontur aproksymować za pomocą wielokąta, tutaj definiujemy wektor punktów, które będą reprezentować ten wielokąt.
1 |
Rect boundRect; |
Definiujemy obiekt, który posłuży do przechowania danych o obrysie prostokątnym naszego konturu.
1 |
findContours( cont, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE, Point(0, 0)); |
Główna funkcja znajdująca kontury w naszym obrazku binarnym. Dane o konturach są umieszczane w zmiennej contours. Trzeci parametr CV_RETR_EXTERNAL oznacza że, interesują nas tylko kontury zewnętrzne obiektów, w dokumentacji można znaleźć inne stałe umożliwiające np. zwrócenie konturów wewnętrznych w postaci hierarchii.
Pozostałe parametry określają sposób aproksymacji konturów oraz przesunięcie każdego z punktów konturu. Więcej informacji jak zwykle można znaleźć w dokumentacji, albo zadając pytania.
1 |
Mat drawing = Mat::zeros( cont.size(), CV_8UC3); |
Tworzymy pusty obrazek wypełniony zerami o rozmiarze obrazka cont. Będziemy w nim rysować kontury.
1 2 3 4 5 6 7 8 |
for( int i = 0; i< contours.size(); i++ ) { if ( abs(contourArea(Mat(contours[i]))) > max ) { max = abs(contourArea(Mat(contours[i]))); i_cont = i; } } |
W pętli przechodzimy po wszystkich konturach, funkcja contourArea(Mat(contours[i]) zwraca nam obszar zajmowany przez dany kontur, wyszukujemy największy obszar a następnie zapamiętujemy dla niego indeks konturu.
Używamy wartości bezwzględnych w przypadku gdyby obszary konturów miały wartości ujemne, co zdarza się dla konturów wewnętrznych których tu nie mamy ale jak wiadomo licho nie śpi.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
if ( i_cont >= 0 ) { approxPolyDP( Mat(contours[i_cont]), contours_poly, 3, true ); boundRect = boundingRect( Mat(contours_poly) ); fillConvexPoly(img, contours_poly, contours_poly.size() ); rectangle( img, boundRect.tl(), boundRect.br(), Scalar(125, 250, 125), 2, 8, 0 ); line( img, boundRect.tl(), boundRect.br(), Scalar(250, 125, 125), 2, 8, 0); line( img, Point(boundRect.x + boundRect.width, boundRect.y), Point(boundRect.x, boundRect.y + boundRect.height), Scalar(250, 125, 125), 2, 8, 0); string s; stringstream out; out << boundRect.x + boundRect.width/2 << "x" << boundRect.y + boundRect.height/2; s = out.str(); putText( img, s, Point(50, 50), CV_FONT_HERSHEY_COMPLEX, 1, Scalar(20, 40, 80), 3, 8 ); drawContours( drawing, contours, i_cont, Scalar(125, 125, 250), 2 ); } |
Testujemy czy w ogóle wykryto jakiś kontur, jeśli tak to kolejno przybliżamy go wielokątem.
1 |
approxPolyDP(Mat(contours[i_cont]), contours_poly, 3, true); |
Gdzie parametrami są: przybliżany kontur, zbiór punktów do których zostanie wpisany otrzymany w wyniku przybliżenia wielokąt, dokładność przybliżenia. Czwarty parametr określa czy mamy do czynienia z kształtem zamkniętym.
Następnie za pomocą poniższej funkcji zwracamy prostokąt otaczający nasz kontur.
1 |
boundingRect( Mat(contours_poly) ); |
Natomiast funkcja:
1 |
fillConvexPoly(img, contours_poly, contours_poly.size() ); |
Powoduje wypełnienie środka wielokąta kolorem i narysowanie go na naszym oryginalnym obrazku. Kolejne funkcje odpowiedzialne są za narysowanie kwadratu i linii na obrazie z kamery. Następnie umieszczamy napis wyświetlający współrzędne środka kwadratu otaczającego kontur, a więc w pewnym sensie środka naszego obiektu. Na koniec rysujemy kontur naszego obiektu w stworzonym obrazku drawing.
1 |
drawContours( drawing, contours, i_cont, Scalar(125, 125, 250), 2 ); |
Parametrami są obrazek na którym zostanie narysowany kontur, wektor z konturami, indeks konturu do narysowania, kolor konturu w formacie BGR oraz grubość linii.
Efekt działania programu
Program działa znacznie lepiej w przypadku światła naturalnego. Przy świetle sztucznym efekty nie są już tak zadowalające. Metoda tu opisana stanowi jedynie wprowadzenie do rozległego tematu segmentacji obrazów, w przyszłości postaram się opisać jeszcze jakieś inne, o większym stopniu złożoności, dające lepsze wyniki.
Jeżeli wystąpiły by jakieś problemu ze zrozumieniem artykułu lub z działaniem programów, proszę o informacje, postaram się wówczas niezwłocznie to naprawić.
Dokumentacja OpenCV 2.4.2 http://opencv.itseez.com/index.html
« Poprzedni artykuł z seriiNastępny artykuł z serii »
To nie koniec, sprawdź również
Przeczytaj powiązane artykuły oraz aktualnie popularne wpisy lub losuj inny artykuł »
barwy, hsv, kolory, krawędzie, kurs, OpenCV, rgb
Trwa ładowanie komentarzy...