LinkedIn YouTube Facebook
Szukaj

Newsletter

Proszę czekać.

Dziękujemy za zgłoszenie!

Wstecz
SoM / SBC

Pierwsze kroki z Raspberry Pi: aplikacje wideo bazujące na OpenCV

Jak można łatwo zauważyć na Listingu 3, do budowy  kolejnej aplikacji został wykorzystany kod źródłowy z poprzedniego przykładu. Tak więc operacje konwersji, klonowania oraz progowania obrazów nie będą już ponownie omawiane. Skupimy się jedynie na nowo wprowadzonych funkcjach. Jedną z nich jest cvSmooth(). Funkcja ta umożliwia usunięcie szumów z obrazu za pomocą czterech rodzajów filtrów. Konstrukcja funkcji wygląda następująco:

  void cvSmooth(const CvArr* src, CvArr* dst, int smoothtype = CV_GAUSSIAN, int param1 = 3, int param2 = 0, double param3 = 0, double param4 = 0)

Funkcja jako pierwszy parametr przyjmuje obraz źródłowy, drugi parametr to wskaźnik do obrazu po operacji wygładzania. Trzecim z parametrów jest rodzaj filtru jaki zostanie użyty do przetworzenia obrazu. Jako wartość domyślna wybierany jest filtr gaussowski. Przeznaczenie ostatnich czterech parametrów zależy od rodzaju wybranego filtra (zainteresowanych tą tematyką Czytelników zachęcam do zapoznania się z bogatą w przykłady dokumentacją OpenCV). Wynik działania funkcji cvSmooth() jest do przewidzenia i nie będzie się on znacznie różnił od filtracji dolnoprzepustowej wykonanej za pomocą splotu obrazu z zdefiniowaną maską filtru. Eliminacja szumu jest realizowana za pomocą filtrów dolnoprzepustowych, które odcinają składowe o wysokiej częstotliwości, a tym samym wysokoczęstotliwościowy szum. W przypadku użycia funkcji cvSmooth() programista jest pozbawiony konieczności zapamiętania macierzy masek dla poszczególnych rodzajów filtrów, a sam kod jest znacznie krótszy i przejrzysty.

Kolejną nowopoznaną funkcją jest cvFindContours(). Funkcja ta, jak sama nazwa wskazuje, realizuje wyszukiwanie konturów we wskazanym obrazie. Budowa funkcji jest następująca:

int cvFindContours(
IplImage* img,
CvMemStorage* storage,
CvSeq** firstContour,
int headerSize = sizeof(CvContour),
CvContourRetrievalMode mode = CV_RETR_LIST,
CvChainApproxMethod method = CV_CHAIN_APPROX_SIMPLE
);

Analogicznie do omawianej wyżej funkcji cvSmooth() pierwszym argumentem wywołania jest obraz źródłowy. Obraz ten powinien być jednokanałowy, stąd przed wywołaniem funkcji cvFindContours() został poddany binaryzacji. Kolejnym parametrem jest wskaźnikiem do miejsca w którym za pomocą funkcji cvCreateMemStorage() została przydzielona pamięć. Trzecim argumentem jest firstContour, który wskazuje na początek „drzewa konturów”. Ostatnie trzy parametry to odpowiednio wskaźnik na rozmiar konturów, reprezentacja zwracanej wartości oraz wybór metody aproksymacji. Funkcja cvFindContours() zwraca wartość typu całkowitego int informującą o liczbie znalezionych konturów. Ostatnią z funkcji wymagającą kilku słów omówienia jest cvDrawContours(). Kolejne argumenty funkcji to: obraz na którym zostaną narysowane kontury, drzewo konturów, kolor konturu zewnętrznego oraz wewnętrznego. Ostatnie trzy parametry określają poziom zagnieżdżenia konturów, sposób rysowania i wartość przesunięcia linii względem oryginału.

Efekt zbliżony do działania funkcji cvFindContours() można uzyskać poprzez bezpośrednie wykonanie splotu obrazu z maską tzw. filtrów krawędziowych. W rodzinie filtrów krawędziowych możemy wyróżnić filtry: przesuwania, gradientowe, kierunkowe i filtry Laplace’a. Definicje masek filtrów można łatwo odnaleźć w Internecie, tak więc zachęcam Czytelników do własnych eksperymentów z filtrami krawędziowymi w oparciu o napisanie programy i porównanie uzyskanych efektów z działaniem funkcji cvFindContours().

Na potrzeby aplikacji sample_3 zmieniono obraz źródłowy na obraz arm.jpg. Obraz ten lepiej prezentuje działanie programu oraz detekcję konturów, zarówno wewnętrznych jak i zewnętrznych. Efekt działania programu został przedstawiony na rysunku 3.

Rys. 3. Wynik działania programu z Listingu 3

Rys. 3. Wynik działania programu z Listingu 3

 

Detekcja twarzy w obrazie statycznym – wydajność algorytmu

O możliwości i rodzaju zastosowań mikrokomputerów w cyfrowym przetwarzaniu obrazów, może zadecydować wydajność realizowania wybranego algorytmu. Dla powyższych przykładów złożoność obliczeniowa była niewielka, stąd czas pracy programu był rzędu setek milisekund i w takim zastosowaniu nie stanowił on żadnych niedogodności. Przeanalizujmy teraz jednak bardziej złożony obliczeniowo przykład przedstawiony na listingu 4. Zaprezentowany kod źródłowy jest zmodyfikowaną wersją aplikacji Facedetect dostarczaną wraz z kodami źródłowymi OpenCV (katalog OpenCV-2.4.5/samples zawiera dużą liczbę przykładowych aplikacji przygotowanych w językach C, Java, Python, itd., które warto przeanalizować).

List. 4.

#include 
#include "cv.h"
#include "highgui.h"

using namespace std;
using namespace cv;

void detectAndDraw(Mat& img,CascadeClassifier& cascade, CascadeClassifier& nestedCascade,double scale);
String cascadeName = "/usr/local/share/OpenCV/haarcascades/haarcascade_frontalface_alt.xml";
String nestedCascadeName = "/usr/local/share/OpenCV/haarcascades/haarcascade_eye_tree_eyeglasses.xml";

int main(int argc, const char** argv){

  Mat frame, frameCopy, image;
  String inputName;
  CascadeClassifier cascade, nestedCascade;
  double scale = 1;
  inputName.assign(argv[1]);
  cascade.load(cascadeName);
  image = imread(inputName,1);
  detectAndDraw(image,cascade,nestedCascade,scale);
  return 0;
}


void detectAndDraw(Mat& img,CascadeClassifier& cascade,CascadeClassifier& nestedCascade,double scale){

  int i = 0;
  double t = 0;
  vector faces;
  const static Scalar colors[] = {CV_RGB(0,0,255),
  	CV_RGB(0,128,255),
  	CV_RGB(0,255,255),
	CV_RGB(0,255,0),
	CV_RGB(255,128,0),
	CV_RGB(255,255,0),
	CV_RGB(255,0,0),
	CV_RGB(255,0,255)};

  Mat gray, smallImg(cvRound (img.rows/scale),cvRound(img.cols/scale),CV_8UC1);
  cvtColor(img,gray,CV_BGR2GRAY);
  resize(gray,smallImg,smallImg.size(),0,0,INTER_LINEAR);
  equalizeHist(smallImg,smallImg);

  //START POMIARU CZASU
  t=(double)cvGetTickCount();
  cascade.detectMultiScale(smallImg,faces,1.1, 2, 0|CV_HAAR_SCALE_IMAGE,Size(30, 30));
  t=(double)cvGetTickCount() - t;
  printf("Czas detekcji = %g ms\n",t/((double)cvGetTickFrequency()*1000.));
  //KONIEC POMIARU I WYSWIETLENIE WYNIKU

  for(vector::const_iterator r = faces.begin(); r != faces.end(); r++, i++){

    Mat smallImgROI;
    vector nestedObjects;
    Point center;
    Scalar color=colors[i%8];
    int radius;
    center.x=cvRound((r->x + r->width*0.5)*scale);
    center.y=cvRound((r->y + r->height*0.5)*scale);
    radius=cvRound((r->width + r->height)*0.25*scale);
    circle(img, center, radius, color, 3, 8, 0);

    if(nestedCascade.empty())
    continue;

    smallImgROI = smallImg(*r);
    nestedCascade.detectMultiScale(smallImgROI, nestedObjects,1.1, 2, 0|CV_HAAR_SCALE_IMAGE,Size(30, 30));

    for( vector::const_iterator nr = nestedObjects.begin(); nr != nestedObjects.end(); nr++){
      center.x = cvRound((r->x + nr->x + nr->width*0.5)*scale);
      center.y = cvRound((r->y + nr->y + nr->height*0.5)*scale);
      radius = cvRound((nr->width + nr->height)*0.25*scale);
      circle( img, center, radius, color, 3, 8, 0 );
    }

  }

  imwrite("detekcja.jpg",img);
  puts("Wynik zapisany do pliku detekcja.jpg");
}

Detekcja twarzy w dostarczonym przykładzie Facedetect odbywa się na zasadzie klasyfikatora kaskad Haar’a (ang. Haar Cascade Classifier). Rozpoznawanie twarzy realizowane jest na podstawie danych zapisanych w pliku XML. Na potrzeby aplikacji Facedetect wczytujemy klasyfikatory: haarcascade_frontalface_alt.xml oraz haarcascade_eye_tree_eyeglasses.xml, dzięki którym wykrywamy twarze frontalnie skierowane do kamery. Wczytanie innych klasyfikatorów umożliwia nam detekcję np. profili twarzy, ale także sylwetek ludzkich (poprzez „treningi” można przygotować własne pliki .xml dla wybranych obiektów). Detekcja właściwa wykorzystywana w bibliotece OpenCV używa metody Viola-Jones’a, której opis wykracza poza zakres niniejszego artykułu.

W stosunku do oryginału, w kodzie zaprezentowanym na Listingu 4, usunięto przechwytywanie obrazu z kamery, ograniczono liczbę przyjmowanych parametrów do jednego (plik z obrazem), a także na „sztywno” określono ścieżki umiejscowienia klasyfikatorów Haar’a. Rezultat pracy zapisywany jest do pliku detekcja.jpg poprzez funkcję imwrite(), o czym użytkownik informowany jest poprzez wywołanie funkcji printf().

Algorytm przedstawiony na Listingu 4 jest dość złożony i wymaga wielu obliczeń zmiennoprzecinkowych. Uzyskane rezultaty czasowe przetwarzania nie mogą być porównywalne z wynikami osiąganymi na komputerze klasy PC. Jednak aby zobrazować dysproporcje,  w algorytmie detekcji twarzy umieszczono funkcje cvGetTickCount() oraz cvGetTickFrequency() umożliwiające określenie czasu działania algorytmu.

Aplikacja sample_4 jako jedyny parametr przyjmuje nazwę plik obrazu do przetworzenia. Wynik swojej pracy zapisuje w pliku detekcja.jpg – zakończenie przetwarzania jest sygnalizowane poprzez wyświetlenie informacji o czasie działania algorytmu.

./sample_4 lena.jpg 

Czas detekcji = 4717.86 ms
Wynik zapisany do pliku detekcja.jpg

Proces przetwarzania może trwać nawet do kilku minut, co ściśle uzależnione jest od jakości wskazanego obrazu. Dla obrazu lena.jpg obraz wynikowy detekcja.jpg prezentuje się jak na rysunku 4.

Rys. 4. Wynik działania aplikacji z Listingu 4

Rys. 4. Wynik działania aplikacji z Listingu 4

Czasy przetwarzania:

lena.jpg [512 x 512] lena2.jpg [256×256]
ASUS 1201n 1106,61 [ms] 198,486 [ms]
Raspberry Pi 4717.86 [ms] 1027.33 [ms]
FriendlyARM Mini2440 39453.5 [ms] 6753.24 [ms]

 

Uzyskane wyniki charakteryzują się dużym rozrzutem pomiędzy porównywanymi platformami. Niski wynik dla Raspberry Pi wynika z ograniczonych możliwości arytmetyki zmiennoprzecinkowej i braku optymalizacji funkcji pod kątem zasobów sprzętowych mikroprocesora (OpenCV posiada jedynie optymalizację dla procesorów obsługujących instrukcje wektorowe NEON w rdzeniach ARMv7). Biorąc również pod uwagę fakt, że detekcja twarzy w programie jest dość złożonym procesem, uzyskane wyniki utrudniają bardzo płynne przeprowadzenie jej w czasie rzeczywistym. Nie wyklucza to jednak innych zastosowań minikomputerów w przetwarzaniu obrazów w czasie rzeczywistym. Dobrym przykładem może być algorytm śledzenia obiektów o wybranym kolorze (przykład aplikacji zaprezentowano na rysunku 5).

 

Rys. 5. Przykład aplikacji realizującej śledzenie obiektu o zadanym kolorze

Rys. 5. Przykład aplikacji realizującej śledzenie obiektu o zadanym kolorze

 

W takim przypadku złożoność przeprowadzanych operacji jest znacznie mniejsza (idea działania opiera się między innymi na poznanej już operacji progowania), stąd możemy liczyć na uzyskanie zadowalającej wydajności, o czym przekonamy się w kolejnym cyklu artykułów poświęconym Raspberry Pi.

Do pobrania

Łukasz Skalski - absolwent Politechniki Gdańskiej, miłośnik FLOSS, autor książki "Linux. Podstawy i aplikacje dla systemów embedded" oraz szeregu artykułów dotyczących programowania systemów wbudowanych. Zawodowo związany z firmą Samsung. Wszystkie wolne chwile poświęca projektowaniu i programowaniu urządzeń wyposażonych w mikroprocesory 8-/16- i 32-bitowe. Szczególnym zainteresowaniem obejmuje tematykę systemu Linux w aplikacjach na urządzenia embedded.