czwartek, 7 kwietnia 2011

Obiektowy szkielet aplikacji interaktywnej dla OpenCV

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.

2 komentarze:

Mateusz Siewniak pisze...

Ostatnio trafiłem na świetną aplikację związaną z analizowaniem sekwencji wideo w czasie rzeczywistym. Autor miał naprawdę dobry pomysł i efekt jest bardziej niż zachwycający. Polecam obejrzeć: http://www.wired.com/gadgetlab/2011/04/predator-smart-camera-locks-on-tracks-anything-mercilessly/

Pozdrawiam.

Rafał Petryniak pisze...

Widziałem już ten filmik kilka dni temu :-) Pomimo tego, że system również zrobił na mnie wrażenie, autor jeszcze kilka dni temu nie udostępniał kodów źródłowych. Teraz widzę, że są one już dostępne. Jak znajdę chwile czasu to je przetestuję. Szkoda tylko, że kod jest pisany w C i Matlabie. Zawsze to czyni go mniej przenośnym.

Prześlij komentarz