wtorek, 22 marca 2011

Dostęp do składowych piksela w OpenCV 2.2

Pełny kod źródłowy
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 2Operowanie 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_.

2 komentarze:

fantom pisze...

Obrazy wielokanałowe->Podejście tradycyjne->kod: trzecia pętla for jest zbędna. Dzięki za wpis, zaoszczędziłem czas na szukaniu, pozdrawiam serdecznie

Anonimowy pisze...

Jeżeli weżmiesz pod uwagę czasy dostępu to można zauwazyc, że najprostsze oraz najwygodniejsze sposoby są niestety zarazem najwolniejsze...
Najefektywniej działa:

/ Method 2. process the first chroma plane using pre-stored row pointer.

Prześlij komentarz