wtorek, 19 kwietnia 2011

Repozytorium dla przykładów OpenCV na GitHub

0 komentarze
W ostatnim czasie na blogu pojawiło się kilka wpisów o OpenCV. Wiele z nich zawierało przykładowy kod źródłowy, który wklejałem we fragmentach, aby nie wydłużać samych postów. Były to głównie kluczowe fragmenty opisywanych programów i nie zawierały one najczęściej początku i końca pliku. Wiem, że nie jest to żaden kłopot dla osób, które miały do czynienia z OpenCV, ale dla początkującego użytkownika może to być mały problem. Łatwo przecież zrobić drobny błąd, literówkę lub coś przeoczyć, a trudniej to później znaleźć. W związku z tym utworzyłem publiczne repozytorium kodu źródłowego GitHub, do którego wypchnąłem (push - w terminologii Git) opracowane do tej pory przykłady. Do repozytorium można przejść klikając w poniższy obrazek.

poniedziałek, 11 kwietnia 2011

Poprawiony szkielet aplikacji interaktywnej dla OpenCV

1 komentarze
Pełny kod źródłowy
Kilka dni temu zaproponowałem na tym blogu obiektowy szkielet aplikacji interaktywnej dla OpenCV. W dzisiejszym wpisie chciałbym pokazać jak można zrobić to jeszcze lepiej, aby kod był nie tylko bardziej obiektowy, ale również miał lepiej zorganizowaną strukturę. Pomimo tego, że przedstawiony tam schemat aplikacji całkiem nieźle się sprawdza, a kod jest w miarę czytelny, ma on kilka wad i usterek, które zapewne niejeden zaawansowany programista C++ szybko mógłby wytknąć. Oto dwie z nich:
  • Cały kod programu został umieszczony w jednym pliku. Dla małych projektów jest to w miarę dobre rozwiązanie, jednak przy większych aplikacjach może być uciążliwe. Umieszczanie całego kodu w jednym pliku łamie również powszechnie stosowaną zasadę umieszczania każdej klasy w osobnym pliku. Inną powszechnie znaną praktyką, która nie została tam zastosowana, jest rozdział deklaracji klasy od jej ciała i umieszczenie ich osobno w pliku nagłówkowym (*.h) i w pliku z implementacją funkcji (*.cpp).
  • Trudno nazwać przedstawiony w poprzednim wpisie szkielet za w pełni obiektowy, jeśli znajduje się tam jedna klasa, która zawiera całą funkcjonalność aplikacji. Szczególnie problem jest widoczny w braku osobnej reprezentacji obiektowej dla znajdujących się tam kulek. Problem ten nie jest szczególnie zauważalny w przypadku istnienia jednej kulki, ale jest już odczuwalny w przypadku ich większej ilości. Wystarczy spojrzeć na funkcję MainApp::update() i spróbować sobie wyobrazić, jak mogłaby ona wyglądać, jeśli trzeba by było zarządzać ruchem i rysowaniem np. 10 kulek. Widać wyraźnie, że coś jest tutaj nie tak i wręcz konieczne wydaje się rozszerzenie modelu obiektowego własnie o te obiekty, które są rysowane na ekranie, a które mają w miarę niezależny schemat poruszania się. 
Patrząc na te problemy postanowiłem szybko wprowadzić niezbędne poprawki, aby wyeliminować opisane niedogodności. Z jednego pliku powstało ich sześć. Dodatkowo utworzyłem klasę Ball reprezentująca ruch i wygląd kulek. Pozwoliło mi to na wprowadzenie łatwej personalizacji dla każdej kulki z osobna. Struktura obydwu klas (MainApp i Ball) została rozdzielona na pliki nagłówkowe i pliki z implementacją ich funkcji składowych.

Krótka charakterystyka szkieletu aplikacji interaktywnej OpenCV

interactive_app_tutorial-main.cpp - plik ten zawiera jedynie funkcję główną programu main(), której jedynym zadaniem jest uruchomienie i wyświetlenie głównego okna aplikacji reprezentowanego przez klasę MainApp. Dodatkowo na początku tego pliku inicjalizowany jest generator liczb losowych OpenCV wartością równą liczbie milisekund, która upłynęła od ustalonej daty w przeszłości (w systemie Linux jest to 1 styczeń 1970 roku).

#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>

#include "interactive_app_tutorial-MainApp.h"

using namespace cv;

RNG rng(cvGetTickCount());

int main(int, char**)
{
  MainApp::getInstance().run();
  
  return 0;
}

interactive_app_tutorial-util.h - tutaj najlepiej umieszczać funkcje ogólnego przeznaczenia oraz funcke narzędziowe, które mogą się przydać w różnych miejscach programu. Na razie znajduje się tu jedynie funkcja zwracająca składowe koloru, dla przekazanej jako parametr liczby.

#ifndef UTIL_H
#define UTIL_H

static Scalar randomColor(RNG& rng)
{
  int icolor = (unsigned)rng;
  return Scalar(icolor&255, (icolor>>8)&255, (icolor>>16)&255);
}

#endif

interactive_app_tutorial-MainApp.h - jest to plik nagłówkowy zawierający deklarację klasy MainApp i jej składowych. Na pierwszy rzut nie widać, aby coś się zmieniło względem szkieletu aplikacji opisanego w poprzednim wpisie. Jednak jak będzie to widać dalej, implementacja poszczególnych funkcji jest już zupełnie inna. Patrząc na zmienne prywatne jakie tutaj zostały zadeklarowane, można zauważyć, że klasa będzie zarządzać trzema kulkami. Jak sobie poradzić z tym ograniczeniem zostanie pokazane na końcu tego wpisu.

#ifndef MAINAPP_H
#define MAINAPP_H

#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>

#include "interactive_app_tutorial-Ball.h"

using namespace cv;

//MainApp singelton class
class MainApp
{
private:
  Mat canvas;               // Image for drawing
  Scalar bgr_color;         // Background color
  Ball *myBall_1, *myBall_2, *myBall_3;
  
public: // Some global params:
  static int DELAY;
  static int CANVAS_WIDTH;  
  static int CANVAS_HEIGHT;  
  
private:
  MainApp() {}
  MainApp(const MainApp &);
  MainApp& operator=(const MainApp&);
  
  void setup();   // Initial commands for setup processing
  void update();  // Commands to modify the parameters
  void draw();    // Drawing functions:
  
public:
  static MainApp& getInstance()
  {
    static MainApp instance;
    return instance;
  }
  
  // Main loop function with displaying image support
  // and handle mouse and keyboard events
  void run();
};

#endif

interactive_app_tutorial-MainApp.cpp - plik ten zawiera właściwą implementację funkcji zawartych w klasie MainApp. Nie są one jakość szczególnie interesujące ponieważ wywołują jedynie funkcje składowe klasy Ball o identycznych nazwach.

#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>

#include "interactive_app_tutorial-MainApp.h"

using namespace cv;

// Best place to initialize global MainApp params:
int MainApp::DELAY        = 5;
int MainApp::CANVAS_WIDTH = 500;
int MainApp::CANVAS_HEIGHT= 350;

void MainApp::run() {
  setup();
  
  const char *win_canvas = "Canvas";
  namedWindow(win_canvas, CV_WINDOW_AUTOSIZE);
  
  while (cvWaitKey(4) == -1) {
    update();
    draw();
    
    imshow(win_canvas, canvas);
    waitKey(DELAY);
  }
}

void MainApp::setup() 
{
  bgr_color = Scalar(200,200,200);
  canvas = Mat(CANVAS_HEIGHT, CANVAS_WIDTH, CV_8UC3, bgr_color);
  
  myBall_1 = new Ball(&canvas);
  myBall_2 = new Ball(&canvas);
  myBall_3 = new Ball(&canvas);
}

void MainApp::draw() {
  canvas = bgr_color;
  
  myBall_1->draw();    
  myBall_2->draw();    
  myBall_3->draw();    
}

void MainApp::update() 
{
  myBall_1->update();
  myBall_2->update();
  myBall_3->update();
}

interactive_app_tutorial-Ball.h - tak naprawdę to tutaj zaczynają się większe zmiany względem poprzedniego szkieletu aplikacji. Plik ten zawiera deklarację nowej klasy Ball, która już sama będzie dbać o swój wygląd, wyrysowanie siebie na ekranie oraz wyliczenie właściwej lokalizacji dla kolejnej iteracji. Sprawą, na którą należy tutaj zwrócić szczególną uwagą jest postać konstruktorów. Konstruktor domyślny Ball() został specjalnie zablokowany, aby zachęcić (zmusić ;-)) użytkownika do korzystania z dodatkowego konstruktora, który jako parametr przyjmuje wskaźnik do struktury cv::Mat, po której będzie można rysować. Generalnie powinien być tutaj przekazywany wskaźnik do głównego obrazka z klasy MainApp, na którym odbywa się rysowanie.

#ifndef BALL_H
#define BALL_H

#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>

using namespace cv;

//Ball class
class Ball 
{
private:
  Mat *canvas;
  float x, y, dim;
  float speedX, speedY;
  Scalar color;
  
  Ball();
  void setup();
  
public:
  Ball(Mat *can)
  {
    canvas = can;
    setup();
  }
  
  void update();
  void draw();
};

#endif

interactive_app_tutorial-Ball.cpp - osoby, które już wcześniej analizowały poprzedni szkielet aplikacji zapewne zauważyły, że istotna część treści funkcji MainApp::update() i MainApp::draw() zostały przeniesione do funkcji o identycznych nazwach w klasie Ball. Jest to najważniejsza zmiana w nowym szablonie. Teraz to każda kulka samodzielnie dba o siebie i kontroluje swoje zachowanie.

#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>

#include "interactive_app_tutorial-Ball.h"
#include "interactive_app_tutorial-MainApp.h"
#include "interactive_app_tutorial-util.h"

using namespace cv;

extern RNG rng;

void Ball::setup()
{
  x = rng.uniform(0, MainApp::CANVAS_WIDTH); // give some random positioning
  y = rng.uniform(0, MainApp::CANVAS_HEIGHT);
  speedX = rng.uniform((float)-2, (float)2); // and random speed and direction
  speedY = rng.uniform((float)-2, (float)2);
  dim    = rng.uniform(20,60);
  
  color = randomColor(rng);
}

void Ball::update() 
{
  if(x < 0 ){
    x = 0;
    speedX *= -1;
  } else if(x > MainApp::CANVAS_WIDTH){
    x = MainApp::CANVAS_WIDTH;
    speedX *= -1;
  }
  
  if(y < 0 ){
    y = 0;
    speedY *= -1;
  } else if(y > MainApp::CANVAS_HEIGHT){
    y = MainApp::CANVAS_HEIGHT;
    speedY *= -1;
  } 
  
  x+=speedX;
  y+=speedY;  
}

void Ball::draw()
{
  circle(*canvas, Point(x,y), dim, color,-1,CV_AA); 
}

CMakeLists.txt - plik ten zawiera konfigurację procesu kompilacji CMake dla omówionego przykładu. Wcześniej był tylko jeden plik, teraz jest ich sześć. Aby się w tym nie pogubić warto skorzystać właśnie z CMake.

PROJECT(OpenCvInteractiveAppsAndExamples-InteractiveAppTutorial2)

cmake_minimum_required(VERSION 2.8)

FIND_PACKAGE( OpenCV REQUIRED )

ADD_EXECUTABLE(interactive_app_tutorial_2 interactive_app_tutorial-main.cpp interactive_app_tutorial-MainApp.cpp interactive_app_tutorial-Ball.cpp)
TARGET_LINK_LIBRARIES(interactive_app_tutorial_2 ${OpenCV_LIBS})

Drobne poprawki na koniec

Wydawać by się mogło, ze w opisanym powyżej szkielecie aplikacji OpenCv wszystko jest OK, ale jeden szczegół na dłuższą metę może okazać się bardzo uciążliwy. W tym momencie w klasie MainApp zosłao z góry określone ile kulek będzie rysowane na ekranie. Trzy kulki to trochę mało. Co by było gdyby trzeba ich było narysować np. trzysta? Najlepiej w takiej sytuacji skorzystać z dynamicznie rozszerzanych kontenerów dostępnych w C++. Klasa vector będzie w zupełności wystarczająca. Na początek warto zmienić deklarację klasy MainApp usuwając z niej zmienne lokalne *myBall_1, *myBall_2, *myBall_3, a wprowadzając na to miejsce deklarację wektora vector <Ball> myBall;. Ja dodatkowo wprowadziłem jeszcze jedną zmienną statyczną, która będzie regulować liczbę kulek wyświetlanych na ekranie. Po tych zmianach kod kod może wyglądać jak poniżej:

interactive_app_tutorial-MainApp.h

private:
  vector <Ball> myBall;
  //...
  
public:
  static int INITIAL_BALLS_NUMBER;
  //...

Kolejna zmiana, która należy wprowadzić to aktualizacja funkcji setup(), update() i draw() klasy MainApp. Teraz one powinny wyglądać następująco:

interactive_app_tutorial-MainApp.cpp

void MainApp::setup() 
{
  bgr_color = Scalar(200,200,200);
  canvas = Mat(CANVAS_HEIGHT, CANVAS_WIDTH, CV_8UC3, bgr_color);
  
  for (int i = 0; i < INITIAL_BALLS_NUMBER; i++){
    myBall.push_back( Ball(&canvas) );
  }

}

void MainApp::draw() {
  canvas = bgr_color;
  
  for (int i = 0; i < myBall.size(); i++){
    myBall[i].draw();
  }  
}

void MainApp::update() 
{
  for (int i = 0; i < myBall.size(); i++){
    myBall[i].update();
  } 
}

Ostatecznie aplikacja po powyższych zmianach i ustawieniu liczby kulek na 30 może dać rezultat podobny jak ten na obrazku poniżej:

W miarę wolnego czasu w kolejnych wpisach będę chciał pokazać jak wprowadzić do powyższego szablonu możliwości interakcji za pomocą klawiatury i myszki oraz za pomocą ruchu zarejestrowanego kamerą.

czwartek, 7 kwietnia 2011

Obiektowy szkielet aplikacji interaktywnej dla OpenCV

2 komentarze
Pełny kod źródłowy
Biblioteka OpenCV pomimo tego, że zawiera sporo konstrukcji obiektowych i pozwala pisać programy w C++, sama w sobie nie precyzuje i nie podpowiada jaką strukturę powinien mieć dobrze napisany kod programu. Wpływa to oczywiście na morale wielu programistów, którzy widząc, że można napisać kod liniowo w całości w funkcji main, idą na skróty i własnie tak robią. Nie chciałbym tutaj nikogo szczególnie piętnować ;), ale większość przykładowych programów, które ostatnio analizowałem tak własnie jest napisana. Dotyczy to również standardowych przykładów dołączonych do biblioteki OpenCV. Oczywiście tak nie musi być. Programy napisane w OpenCV przy zastosowaniu podstawowych konstrukcji obiektowych mogą być nie tylko czytelniejsze dla osoby, która je pierwszy raz przegląda, ale również znacznie łatwiejsze w pielęgnacji i dalszym rozwoju dla osoby lub zespołu, który taką aplikację napisał.

W tym wpisie chciałem się podzielić szablonem aplikacji OpenCV, którego ostatnio używam. Jest to szablon, który może być szczególnie użyteczny dla aplikacji interaktywnych, w których użytkownik ma wpływ na zachowanie wyświetlanych elementów. Przygotowując ten szablon starałem się zachować podejście znane z programowania aplikacji okienkowych, gdzie pracujemy raczej na komponentach odnoszących się do interfejsu użytkownika, a nie na wielu obiektach z mniej lub bardziej złożoną strukturą powiązań. Jak widać z analizy poniższego kodu funkcja main została zredukowana do minimum, a cała logika aplikacji została przeniesiona do klasy MainApp. Klasa ta nie powinna być wielokrotnie tworzona, dlatego została ona skonstruowana w oparciu o wzorzec Singelton, który tego pilnuje.

Polecam teraz dokładnie przeanalizować podany szablon, a dalsze wyjaśnienia będzie można przeczytać poniżej.

#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>

using namespace cv;

//MainApp singelton class
class MainApp
{
private:
  Mat canvas;               // Image for drawing
  Scalar bgr_color;         // Background color
  int x, y, speedX, speedY; // Circle local params
  
public: // Some global params:
  static int DELAY;
  static int CANVAS_WIDTH;  
  static int CANVAS_HEIGHT;  
  
private:
  MainApp() {}
  MainApp(const MainApp &);
  MainApp& operator=(const MainApp&);
  
  // Initial commands for setup processing
  void setup() 
  {
    x = 0;
    y = 0;
    speedX = 1;
    speedY = 1;
    
    bgr_color = Scalar(120,235,139);
    canvas = Mat(CANVAS_HEIGHT, CANVAS_WIDTH, CV_8UC3, bgr_color);
  }
  
  // Commands to modify the parameters
  void update() 
  {
    if(x < 0 ){
      x = 0;
      speedX *= -1;
    } else if(x > CANVAS_WIDTH){
      x = CANVAS_WIDTH;
      speedX *= -1;
    }
    
    if(y < 0 ){
      y = 0;
      speedY *= -1;
    } else if(y > CANVAS_HEIGHT){
      y = CANVAS_HEIGHT;
      speedY *= -1;
    } 
    
    x+=speedX;
    y+=speedY;    
  }
  
  // Drawing functions:
  void draw() {
    canvas = bgr_color;
    circle(canvas, Point(x,y), 30, Scalar(0,255,255),-1,CV_AA);
  }
  
public:
  static MainApp& getInstance()
  {
    static MainApp instance;
    return instance;
  }
  
  // Main loop function with displaying image support
  // and handle mouse and keyboard events
  void run() {
    setup();

    const char *win_canvas = "Canvas";
    namedWindow(win_canvas, CV_WINDOW_AUTOSIZE);

    while (cvWaitKey(4) == -1) {
      update();
      draw();
      
      imshow(win_canvas, canvas);
      waitKey(DELAY);
    }
  }
};

// Best place to initialize global MainApp params:
int MainApp::DELAY        = 5;
int MainApp::CANVAS_WIDTH = 500;
int MainApp::CANVAS_HEIGHT= 350;

int main(int, char**)
{
  MainApp::getInstance().run();
  
  return 0;
}

Patrząc na powyższy kod nie trudno zauważyć, że został on podzielony na kilka części. Dotyczy to szczególnie klasy MainApp. Została ona podzielona na funkcje, które mają ściśle określone role:
  • run() - publicznie dostępna funkcja, która obsługuje główną pętlę programu. Program będzie się w niej wykonywał do momentu, aż użytkownik wciśnie klawisz Escape. Funkcja ta nie tylko wywołuje kolejne, niżej opisane funkcje, ale również odpowiada za wyświetlanie głównego okienka programu i obsługę zdarzeń nadchodzących z klawiatury i myszki.
  • setup() - funkcja wywoływana tylko raz, na początku działania programu, której zadaniem jest inicjalizacja zmiennych lokalnych i utworzenie obrazka, na którym będą rysowane pozostałe elementy.
  • update() - funkcja modyfikująca wybrane parametry działania programu. Tak naprawdę to tutaj powinna być zapisana główna logika działania programu. 
  • draw() - zawiera wszystkie operacje rysowania na głównej scenie programu.
Aby jeszcze dokładniej prześledzić organizację kodu w podanym szablonie przeanalizujmy przykład, który jest do niego dołączony. Mi wygodnie będzie to zrobić za pomocą następującego dialogu:

A. Tych funkcji jest kilka. Kodu samego nie ma wiele. Czy robi on coś konkretnego?

B. Wszystko co możemy zobaczyć na ekranie odbywa się w funkcji draw(). W tym przykładzie widać wyraźnie, że rysuje ona okręgi o promieniu 30 w punkcie o współrzędnych x, y.

A. Nie wydaje się to nic szczególnie ciekawego.

B. Warto teraz zajrzeć do funkcji update(). To tutaj znajduje się właściwe centrum sterowania aplikacji. To tutaj iteracyjnie modyfikowane są wybrane parametry elementów, które są wyświetlane na ekranie. W przypadku okręgu, o którym mowa, własnie w tej funkcji jest zaprogramowane, aby poruszał się on w linii prostej i odbijał się od krawędzi.


Domyślam się, że przedstawiony szablon nie pokazuje nic szczególnie odkrywczego ;-). Wielu zaawansowanych programistów OpenCV, do których ja się raczej nie zaliczam, pewnie stosuje podobne i lepsze rozwiązania od dawna. Mam jedynie nadzieje, że okaże się on przydatny dla osób, które dopiero zaczynają przygodę z OpenCV i chcą napisać swój pierwszy interaktywny program.

Czekam oczywiście na uwagi i komentarze co można poprawić i co można zrobić lepiej.

Aktualizacja 2011.04.11: Opisany w tym wpisie szkielet aplikacji OpenCV ma kilka niedoskonałości. Opisałem je we wpisie Poprawiony szkielet aplikacji interaktywnej dla OpenCV. Zaproponowałem tam również jego udoskonaloną wersję. Szablon opisany w tym wpisie może być jednak z powodzeniem stosowany do prostszych aplikacji.

środa, 6 kwietnia 2011

Detekcja ruchu w OpenCV - porównanie klatek

4 komentarze
Pełny kod źródłowy
OpenCV zostało stworzone i jest głównie używane tam, gdzie jest potrzebna zaawansowana analiza sekwencji wideo. Jest to narzędzie, które zawiera nie tylko funkcje obsługujące pobieranie obrazu z kamery, ale również funkcje, które potrafią ten obraz przetwarzać i wydobywać z niego ważne dla nas informacje. W przypadku analizy sekwencji wideo jednym z ważniejszych zadań jest detekcja miejsc, gdzie wystąpił ruch. Sama detekcja nie musi być szczególnie problematyczna, jeśli chodzi nam jedynie o proste wskazanie obszarów, na których coś się rusza. Wystarczy jedynie sprawdzić, które piksele na kolejnych klatkach obrazu zmieniają swoją wartość i podjąć decyzję na ile ta zmiana powinna być istotna, aby wykryć rzeczywisty ruch obiektu, a nie np. zaburzenia oświetlenia, bądź też ruch cienia obiektu. Oczywiście istnieje wiele problemów, które wymagają bardziej zaawansowanych algorytmów, aniżeli proste porównywanie obrazów. Możemy do nich mi.in zaliczyć rozpoznawanie i śledzenie wybranych obiektów.

W dzisiejszym wpisie chciałem pokazać proste rozwiązanie problemu detekcji ruchu z wykorzystaniem biblioteki OpenCV. Skorzystam tutaj z opisanej powyżej koncepcji sprawdzania, które punkty obrazu zmieniają swoją wartość na kolejnych obrazach w sekwencji wideo. Najłatwiej w tym celu skorzystać z operacji odejmowania obrazów, a następnie binaryzacji obrazu wynikowego w celu określenia, jaka zmiana wartości pikseli powinna zostać uznana za ruch obiektu, a jaka za brak ruchu. Najczęściej stosuje się tutaj jedną z dwoch strategii odejmowania obrazów:
  • odejmowanie dwóch sąsiednich obrazów, tzn. odejmowanie od bieżącej klatki, klatki poprzedniej,
  • odejmowanie od bieżącej klatki, pierwszej klatki, która została zarejestrowana i która jest uznawana jako tło (zakładamy oczywiście, że na pierwszej klatce nie ma obiektów, które później będą zmieniać swoje położenie).
Oba powyższe podejścia pomimo tego, że są bardzo zbliżone do siebie dają całkiem różne rezultaty. Pierwszy sposób wskazuje jedynie, na fragmenty obiektu, które zmieniły swoją lokalizację w sekwencji ruchu, podczas gdy drugi powinien wskazać cały obszar poruszającego się obiektu.

Implementacja opisanych metod w OpenCV nie nastręcza wiele problemów. Obydwa sposoby mogą być zaimplementowane z pomocą poniższego fragmentu kodu:

//TESTED WITH OpenCV 2.2

int diffType = atoi( argv[1] );
int thresval = atoi( argv[2] );

Mat cam_frame, img_gray, img_prev, img_diff, img_bin;

bool first_frame = true;

// LOOP FOR GRABBING IMAGE FROM WEBCAM 

cap >> cam_frame;
cvtColor(cam_frame, img_gray, CV_BGR2GRAY);

if (first_frame) {
    img_prev=img_gray.clone();
    first_frame = false;
    continue;
}

absdiff(img_gray, img_prev, img_diff);
threshold(img_diff, img_bin, thresval, 255, THRESH_BINARY);
erode(img_bin,  img_bin, Mat(), Point(-1,-1), 3);
dilate(img_bin, img_bin, Mat(), Point(-1,-1), 1);

imshow(win_cam,  cam_frame);
imshow(win_gray, img_gray);
imshow(win_diff, img_bin);

if (diffType == 1) img_prev=img_gray.clone();

// END LOOP

Analizując powyższy kod łatwo zauważyć, że jego działaniem możemy sterować za pomocą dwóch zmiennych:
  • diffType - określa, którą klatkę należy odejmować od klatki bieżącej, czy poprzednią (wartość = 1), czy też pierwszą (wartość = 2). To sprawdzanie odbywa się w ostatniej linijce powyższego kodu, gdzie jest ustawiana klatka, która będzie odejmowana.
  • thresval - zmienna ta będzie służyć do określania, co uznajemy za ruch obiektu, a co nie. Polecam ustawiać ją w przedziale od 30 do 100 (60 powinno być w miarę bezpieczną, a zarazem skuteczną wartością progu).
Aby nie wchodzić w szczegółową analizę działania powyższego kodu, opiszę jedynie w skrócie jego najważniejsze fragmenty:
  • W pierwszym przebiegu pętli zapamiętujemy jedynie pierwszą klatkę obrazu. Dzięki temu w drugiej iteracji będziemy mieli co odejmować od klatki bieżącej.
  • absdiff - bezwzględne odjęcie (pomijamy znak) dwóch klatek od siebie.
  • threshold - operacja progowania wykonywana na obrazie różnicowym. Pomijamy punkty, które nieznacznie zmieniły swoją wartość.
  • erode - usunięcie drobnych elementów, oraz wygładzenie krawędzi tych większych. Skutkiem ubocznym tej operacji jest zmniejszenie rozmiaru wszystkich obiektów.
  • dilate - dylatację stosujemy po etapie erozji, aby delikatnie powiększyć obiekty, tak aby miały podobny rozmiar jak to było przed erozją. 

piątek, 1 kwietnia 2011

Obsługa parametrów wywołania programu w C++

6 komentarze
Pisząc programy konsolowe niejednokrotnie spotykamy się problemem przekazywania parametrów startowych. Najczęstszym rozwiązaniem jest ich wpisywanie zaraz po nazwie programu, dzięki czemu będą one później dostępne w tablicy argv funkcji main. Innym rozwiązaniem, rzadziej stosowanym jest ich zapisywanie do pliku tekstowego, np. XML i dalsze jego parsowanie zaraz po starcie programu. To drugie rozwiązanie zalecane jest raczej w przypadku naprawdę złożonych parametrów wywołania. W większości przypadków w zupełności wystarcza proste przekazywanie parametrów po nazwie programu w trakcie jego wywołania.

Podejście klasyczne o przykładowym zapisie:
int zmienna1 = atoi (argv[1]);
int zmienna2 = atoi (argv[3]);
może być kłopotliwe w stosowaniu, ponieważ wymaga dokładnego pamiętania kolejności poszczególnych parametrów. Znacznie lepszym rozwiązaniem są parametry nazwane, co do których ten wymóg nie jest już konieczny. Parametry tego typu znane są na pewno każdemu, kto jakiś czas pracował w konsoli tekstowej i potrzebował zmodyfikować domyślne parametry działania programu lub podać własne.

Obecnie jest kilka mechanizmów obsługi tego typu parametrów w języku C++. Niestety nie są one wbudowane do biblioteki standardowej i wymagają stosowania zewnętrznych bibliotek, które mają nie tylko mocne, ale również słabe strony. Oto niektóre z nich:
  • Boost.Program_options - jeśli ktoś nie miał do tej pory potrzeby korzystania z bibliotek Boost to nie wydaje się dobrym pomysłem włączanie ich do projektu tylko ze względu na obsługę parametrów wejściowych. Bardziej szczegółowy opis biblioteki można znaleźć tutaj.
  • GNU getopt - użycie tego narzędzia w praktyce wymaga od użytkownika napisania kodu zawierającego instrukcję pętli while i przełącznik switch, przez co implementacja programu niepotrzebnie się wydłuża i może tracić czytelność (szczególnie w przypadku krótkich programów). Przykładowy program można zobaczyć w oficjalnej dokumentacji
  • Do innych rozwiązań tego typu można zaliczyć: bibliotekę google-gflags, bibliotekę CLPPargstreamclp.
Każde z powyższych rozwiązań, nawet jeśli w praktyce działa bardzo dobrze, wymaga korzystania z dodatkowych bibliotek, co do których należy mieć pewność, że zostały poprawnie zainstalowane w systemie. Dodatkowo w przypadku, niedużych programów, nie wydaje się sensowne korzystanie z bibliotek, które swoim rozmiarem znacznie przewyższają kod implementowanego algorytmu. Ja osobiście nie przekonałem się do żadnej z powyższych bibliotek, dlatego postanowiłem sam zaprogramować obsługę parametrów wywołania programu.

Poniższe rozwiązanie przygotowałem na podstawie przykładu ze strony: http://stackoverflow.com/questions/865668/parse-command-line-arguments/868894#868894. Przykład ten niestety nie działał do końca poprawnie i ostateczne rozwiązanie udało mi sie przygotowac analizując kod biblioteki CImg, którą używam do przetwarzania obrazu, a która to miała zaimplementowany podobny mechanizm. Poniższego kodu nie będe szczegółowo opisywał. Mam nadzieje, że jest on wystarczająco czytelny :-).

Funkcja parsująca parametry wywołania programu w C++:

inline const char*  getCmdOption(const char * name, const char * defaut, const int argc, const char *const * argv)
{
  const char *res = 0;
  
  if (argc > 0) {
    int k = 0;
    while (k < argc && strcmp(argv[k],name)) ++k;
    res = (k++==argc?defaut:(k==argc?argv[--k]:argv[k]));
  } else res = defaut;
  
  return res;
}

Przykładowe użycie funkcji getCmdOption:

Funkcja getCmdOption już okazała mi się pomocna w kilku programach z zakresu przetwarzania obrazu. Poniżej fragment programu do wczytywania danych binarnych, ze szczególnym uwzględnieniem obsługi parametrów wejściowych:

if (argc == 1) {
  printf("Usage: %s -f filename.raw -dx [value] -dy [value] -dz [value]\n", argv[0]);
  exit(0);
}

const char * filename  = getCmdOption("-f",    (char*)0, argc, argv );
const char * _dx       = getCmdOption("-dx",   (char*)0, argc, argv );
const char * _dy       = getCmdOption("-dy",   (char*)0, argc, argv );
const char * _dz       = getCmdOption("-dz",   (char*)0, argc, argv );

if ((char*)0 == filename) { 
  printf("Please specify input data file name (-f)\n"); 
  exit(0);
}
if ((char*)0 == _dx || (char*)0 == _dx || (char*)0 == _dx) { 
  printf("Please specify input data dimension sizes (-dx, -dy, -dz)\n"); 
  exit(0);
}

int dx = atoi( _dx );
int dy = atoi( _dy );
int dz = atoi( _dz );

To co było dla mnie najważniejsze i co mam nadzieje udało mi się uzyskać, to duża czytelność kodu. Mam nadzieje, że proponowana funkcja getCmdOption, okaże się również przydatna dla innych.