Wraz ze zmianą wersji biblioteki
OpenCV z
2.1 na
2.2 doszło do wielu zmian nie tylko w zakresie organizacji modułów, ale również w zakresie sposobu programowania. Dokładnie nie śledziłem jakie zaszły zmiany, więc nie będę ich dokładnie opisywał, jednak zauważyłem, że został położony większy nacisk na programowanie obiektowe. Widoczne jest to szczególnie w strukturach przechowujących obraz. Od zawsze do tego celu korzystało się ze struktury
IplImage, natomiast teraz zalecane jest używanie klasy
cv::Mat, która reprezentuje wielowymiarowe macierze danych. Aby szczegółowo zapoznać się z konstrukcją tej klasy najlepiej zajrzeć do oficjalnej dokumentacji pod hasło
Basic Structures → Mat.
Pierwszym problemem podczas przejścia ze struktury
IplImage do używania klasy
Mat będzie zapewne to, jak odczytać i zmodyfikować zawartość poszczególnych składowych piksela. Do tej pory było kilka sposobów, żeby sobie z tym poradzić (polecam wpis na oficjalnym
Wiki pod hasłem:
How to access image pixels). Teraz podobnie, mamy kilka możliwości, aby dostać się do piksela, jednak dokumentacja opisuje to bardzo pobieżnie. W rozdziale
Introduction → Fast Element Access zostały pokazane 3 różne sposoby operowania na pikselach obrazu, jednak dla mnie wydają się one kłopotliwe w użyciu. To właśnie było powodem przygotowania niniejszego poradnika, w którym chciałem przedstawić własne i znalezione w Internecie wygodne sposoby dostępu do składowych piksela.
Obrazy w skali szarości
Na początek, aby nabrać wprawy najlepiej zrobić proste testy z wykorzystaniem obrazów 1-kanałowych. W tym celu obrazy pozyskane z kamery można przekonwertować na obraz szary i na nim wykonywać dalsze operacje.
Poniżej został pokazany fragment kodu, który zawiera 3 sposoby dostępu do piksela, począwszy od tego najbardziej tradycyjnego, gdzie operujemy bezpośrednio na tablicy z danymi (
metoda 1), poprzez użycie funkcji
at(), której przekazujemy współrzędne interesującego nas punktu (
metoda 2), a skończywszy na zastosowaniu wskaźnika do każdego analizowanego wiersza obrazu (
metoda 3). Z tych 3 metod podejście obiektowe jest reprezentowane przez sposób 2, podczas gdy pozostałe 2 przypadki działają bardziej tradycyjnie.
Aby skupić się na tym co istotne, poniższy kod zawiera jedynie najważniejszą część programu, z deklaracją używanych struktur reprezentujących obraz, oraz środkiem pętli przetwarzającej obraz z kamery. Aby uruchomić ten program można skorzystać z szablonu, który podałem w poprzednim wpisie
OpenCV - instalacja i pierwszy przykład w Ubuntu w sekcji
Ten sam przykład po nowemu.
Mat cam_frame, img_gray_v1, img_gray_v2, img_gray_v3;
// LOOP FOR GRABBING IMAGE FROM WEBCAM
cap >> cam_frame;
// Method 1: Old style method operating on data array
cvtColor(cam_frame, img_gray_v1, CV_BGR2GRAY);
for(int i = 0; i < img_gray_v1.rows; i++)
for(int j = 0; j < img_gray_v1.cols; j++) {
img_gray_v1.data[img_gray_v1.step*i + j] = 255 - img_gray_v1.data[img_gray_v1.step*i + j];
}
// Method 2: Assign pixel using .at() function
cvtColor(cam_frame, img_gray_v2, CV_BGR2GRAY);
for(int i = 0; i < img_gray_v2.rows; i++)
for(int j = 0; j < img_gray_v2.cols; j++)
img_gray_v2.at(i,j) = 255 - img_gray_v2.at(i,j);
// Method2: Use plain C operator []. More efficient than method 2
// if you need to process a whole row of a 2d array
cvtColor(cam_frame, img_gray_v3, CV_BGR2GRAY);
for(int i = 0; i < img_gray_v3.rows; i++)
{
uchar* img_gray_v3_i = img_gray_v3.ptr(i);
for(int j = 0; j < img_gray_v3.cols; j++)
img_gray_v3_i[j] = 255 - img_gray_v3_i[j];
}
// END LOOP
Obrazy wielokanałowe
Większy problem aniżeli obrazy 1-kanałowe sprawiają obrazy kolorowe, domyślnie uzyskiwane z tradycyjnych kamer cyfrowych. Należy tutaj pamiętać nie tylko o współrzędnych punktu, który jest analizowany, ale również o jego składowej koloru. na szczęście jest na to kilka sposobów, które można prześledzić poniżej.
Podejście tradycyjne
Podobnie jak w przypadku obrazów szarych, dla obrazów wielokanałowych można również skorzystać z podejścia znanego z obróbki struktur
IplImage i przeliczać wskaźnik do elementu, który nas interesuje. Przykład poniżej:
Mat frame, result;
// LOOP FOR GRABBING IMAGE FROM WEBCAM
cap >> frame;
result = frame.clone();
// invert the image
for(i=0; i < frame.rows; i++)
for(j=0; j < frame.cols; j++)
for(k=0; k < channels; k++) {
uchar* temp_ptr = &((uchar*)(result.data + result.step*i))[j*3];
temp_ptr[0] = 255 - temp_ptr[0];
temp_ptr[1] = 255 - temp_ptr[1];
temp_ptr[2] = 255 - temp_ptr[2];
}
// END LOOP
Sposoby opisane w oficjalnej dokumentacji
Tak jak wspomniałem na początku tego wpisu, w oficjalnej dokumentacji we wpisie
Introduction → Fast Element Access pokazano 3 sposoby na to jak dostać się do składowych piksela. Dla mnie osobiście dziwne wydaje się rozbijanie obrazu wielokanałowego na obrazy zawierające jeden kanał i po wykonaniu obliczeń ponowne ich scalanie. Nie dość, że wydłuża to zapis algorytmu obróbki obrazu, to mam wrażenie, że powoduje dodatkowy narzut obliczeniowy. Kod z drobnymi poprawkami względem wersji oryginalnej znajduje się poniżej:
Mat img_frame, img_resultA, img_resultB, img_resultC;
vector planesA,planesB,planesC;
// LOOP FOR GRABBING IMAGE FROM WEBCAM
cap >> img_frame;
// Method 1. process Y plane using an iterator
split(img_frame, planesA); // split the image into separate color planes
MatIterator_
it1 = planesA[0].begin(),
it1_end = planesA[0].end(),
it2 = planesA[1].begin(),
it3 = planesA[2].begin();
for(; it1 != it1_end; ++it1,++it2,++it3 )
{
*it1 = 255 - *it1;
*it2 = 255 - *it2;
*it3 = 255 - *it3;
}
merge(planesA, img_resultA);
// Method 2. process the first chroma plane using pre-stored row pointer.
split(img_frame, planesB);
for( int y = 0; y < img_frame.rows; y++ )
{
uchar* Uptr1 = planesB[0].ptr(y);
uchar* Uptr2 = planesB[1].ptr(y);
uchar* Uptr3 = planesB[2].ptr(y);
for( int x = 0; x < img_frame.cols; x++ )
{
Uptr1[x] = 255 - Uptr1[x];
Uptr2[x] = 255 - Uptr2[x];
Uptr3[x] = 255 - Uptr3[x];
}
}
merge(planesB, img_resultB);
// Method 3. process the second chroma plane using
// individual element access operations
split(img_frame, planesC);
for( int y = 0; y < img_frame.rows; y++ )
{
for( int x = 0; x < img_frame.cols; x++ )
{
uchar& Vxy1 = planesC[0].at(y, x);
uchar& Vxy2 = planesC[1].at(y, x);
uchar& Vxy3 = planesC[2].at(y, x);
Vxy1 = 255 - Vxy1;
Vxy2 = 255 - Vxy2;
Vxy3 = 255 - Vxy3;
}
}
merge(planesC, img_resultC);
// END LOOP
Ostatnia deska ratunku
Powyżej opisane sposoby dla obrazów wielokanałowych nie wydają się nazbyt wygodne i raczej zniechęcają, a nie zachęcają do stosowania klasy
Mat. Na szczęście światełkiem w tunelu jest klasa pochodna po
cv::Mat o niemal identycznej nazwie:
cv::Mat_. Klasa ta ma opracowane operatory dostępu do składowych piksela poprzez zastosowanie konstrukcji
( x , y ). Możemy w ten sposób uzyskać wskaźnik do konkretnego punktu obrazu, i dalej stosując zapis tablicowy
[] możemy dostać się do konkretnej wartości danego kanału.
Przykład 1:
Operowanie na oryginalnym obrazie:
Mat frame;
// LOOP FOR GRABBING IMAGE FROM WEBCAM
cap >> frame;
Mat_& frame_ = (Mat_&)frame;
for(int i = 0; i < frame_.rows; i++)
for(int j = 0; j < frame_.cols; j++)
for(int k = 0; k < 3; k++)
frame_(i,j)[k] = 255 - frame_(i,j)[k];
// END LOOP
Przykład 2:
Operowanie na obrazie pomocniczym:
Mat frame;
Mat_ rgb;
// LOOP FOR GRABBING IMAGE FROM WEBCAM
cap >> frame;
cvtColor(frame, rgb, CV_BGR2RGB);
for(int i = 0; i < rgb.rows; i++)
for(int j = 0; j < rgb.cols; j++)
for(int k = 0; k < 3; k++)
rgb(i,j)[k] = 255 - rgb(i,j)[k];
// END LOOP
Podsumowując zebrane w tym poradniku sposoby dostępu do składowych piksela, najwygodniejsze wydaje się użycie operatora
at() dla obrazów jednokanałowych, natomiast dla obrazów wielokanałowych najprostszy i najbardziej czytelny zapis daje klasa
cv::Mat_.