poniedziałek, 13 czerwca 2011

"Form+Code" w mojej biblioteczce

0 komentarze
Po kilku tygodniach czekania w końcu doszła do mnie nowa książka. "Form+Code" zamówiłem jakiś czas temu przez Amazon razem z innym tytułem, o którym napiszę jaki mi go prześlą (ma być pod koniec czerwca, bo na razie nie mają go na stanie). Książka, która do mnie dotarła kilka dni temu, tak jak przewidywałem, nie jest za bardzo techniczno - informatyczna. Zawiera ona za to wiele opisów wdrożeń i instalacji światowej czołówki artystów zajmujących się sztuką generowaną (Generative Art) i kreatywnym kodowaniem (Creative Coding). Na szczęści książka zawiera również algorytmiczny opis kilkunastu ciekawych efektów, dzięki czemu można trochę podejrzeć jakich metod i narzędzi używają najlepsi w branży :-)


Bardzo cennym uzupełnieniem książki jest również obszerna strona internetowa jej poświęcona. Można na niej zobaczyć niektóre strony z książki - jest ona głównie ilustrowana więc jest co oglądać, przejrzeć spis treści, a co najważniejsze pobrać przykładowe programy dołączone do książki.


Polecam!

poniedziałek, 16 maja 2011

OpenCV - obsługa klawiatury i myszki

0 komentarze
Pełny kod źródłowy
Kontynuując rozpoczęty ponad miesiąc temu cykl wpisów dotyczących pisania aplikacji interaktywnych nadszedł czas na wprowadzenie kolejnego elementu do wcześniej zaproponowanego szablonu. Dzisiaj zostanie omówiona zasadnicza kwestia pozwalająca na wprowadzenie przynajmniej podstawowej interakcji do programu: obsługa klawiatury i myszy. Oczywiście dla każdego programisty naturalnym jest to, że program z definicji coś takiego posiada, tzn. że możemy klikać na przyciski, przesuwać suwaki, wybierać zakładki i wpisywać tekst do formularzy. Jednak w przypadku OpenCV, należy pamiętać, że jest to biblioteka głównie do przetwarzania obrazu i chociaż obsługa klawiatury i myszy jest dostępna, to nie jest ona widoczna na pierwszy rzut oka.

W tym wpisie poza omówieniem mechanizmów API do obsługi klawiatury i myszy, zostanie dodatkowo rozszerzony omawiany we wcześniejszych wpisach przykład z kulkami. Zachęcam również do samodzielnych testów i modyfikacji udostępnionego kodu.

Obsługa zdarzeń myszy

Myszka w OpenCV jest w pełni obsługiwana za pomocą modułu highgui, który jest również odpowiedzialny za wyświetlanie obrazków. Obsługę taką możemy włączyć za pomocą funkcji cvSetMouseCallback, w której przywiązujemy do danego okienka funkcję obsługującą zdarzenia myszy. Dodatkowo, ponieważ jest to funkcja globalna, a nie lokalna naszej klasy MainApp, należy do niej przekazać wskaźnik do aktualnego obiektu. Dzięki temu będziemy mieć dostęp do jego funkcji składowych. Podsumowując: należy dodać do funkcji MainApp::run() następującą linijkę:

cvSetMouseCallback(win_canvas, mouse_callback, this);

Drugi parametr tej funkcji jest wskaźnikiem do właściwej funkcji obsługującej myszkę. Funkcja ta odbiera nie tylko zdarzenie jakie wystąpiło (naciśnięcie i zwolnienie klawisza, podwójny klik, przesunięcie myszki), ale również bieżące współrzędne wskaźnika myszki.

void mouse_callback(int event, int x, int y, int flags, void* param)
{
  MainApp *app = ((MainApp *)param);
  switch (event) {
    case  CV_EVENT_LBUTTONDBLCLK:
      app->mouseDoubleClick(x,y,APP_MOUSE_LEFT);   break;
    case  CV_EVENT_RBUTTONDBLCLK:
      app->mouseDoubleClick(x,y,APP_MOUSE_RIGHT);  break;
    case  CV_EVENT_MBUTTONDBLCLK:
      app->mouseDoubleClick(x,y,APP_MOUSE_MIDDLE); break;
    case  CV_EVENT_LBUTTONDOWN:
      app->mousePressed(x,y,APP_MOUSE_LEFT);       break;
    case  CV_EVENT_RBUTTONDOWN:
      app->mousePressed(x,y,APP_MOUSE_RIGHT);      break;
    case  CV_EVENT_MBUTTONDOWN:
      app->mousePressed(x,y,APP_MOUSE_MIDDLE);     break;
    case  CV_EVENT_LBUTTONUP:
      app->mouseReleased(x,y,APP_MOUSE_LEFT);      break;
    case  CV_EVENT_RBUTTONUP:
      app->mouseReleased(x,y,APP_MOUSE_RIGHT);     break;
    case  CV_EVENT_MBUTTONUP:
      app->mouseReleased(x,y,APP_MOUSE_MIDDLE);    break;
    case  CV_EVENT_MOUSEMOVE:
      app->mouseMoved(x,y);                        break;
  }
}

Oczywiście w tym miejscu można już zaprogramować co ma się wydarzyć po wystąpieniu konkretnego zdarzenia, ale proponuję przekazać podejmowanie tej decyzji do wnętrza klasy, do odpowiednich funkcji. Oto one:

void MainApp::mouseMoved(int mouseX, int mouseY ){
  if (m_mousePressed) mouseDragged(mouseX, mouseY);
}

void MainApp::mouseDragged(int mouseX, int mouseY){
}

void MainApp::mousePressed(int mouseX, int mouseY, int button){
  m_mousePressed = true;
}

void MainApp::mouseReleased(int mouseX, int mouseY, int button){
  m_mousePressed = false;
}

void MainApp::mouseDoubleClick(int mouseX, int mouseY, int button){
}

Jak widać nie ma tutaj nic szczególnego - kilka pustych funkcji. Czekają one po prostu na oprogramowanie przez użytkownika, a ich treść zależy od tego co w danym momencie chcemy osiągnąć. Na końcu wpisu zostanie podany przykład jak za pomocą kliknięcia myszką w element usunąć go z ekranu, więc polecam doczytać do końca :-).

Obsługa zdarzeń klawiatury

Z klawiaturą mamy już dużo łatwiej, aniżeli z myszką. Można zupełnie obejść się bez dodatkowych funkcji i wszystko zaprogramować w ciele funkcji, która zawiera pętlę główną programu. Wystarczy w tym celu skorzystać z funkcji cvWaitKey(int), aby sprawdzić czy użytkownik wcisnął jakiś klawisz.

int key;
while(1) {
  key = cvWaitKey(DELAY);  // wait for keyboard input
  if (key == 'q') break;   // 'q' pressed, quit the program
  if (key != -1 ) keyPressed(key);

  update();
  draw();
  
  imshow(win_canvas, canvas);
}

Podejście to pomimo tego, że poprawne, może niepotrzebnie wprowadzić nam zamieszanie nadmiarem kodu w ważnym miejscu programu. Polecam zatem wprowadzić dodatkową funkcję, która będzie zajmowała się wyłącznie reagowaniem na zdarzenia klawiatury. Jej deklaracja jest następująca:


void MainApp::keyPressed(int key){

  switch (key){
    case ' ':
      pauseBallsMove = !pauseBallsMove;
      break;
  }
}

Patrząc na tą funkcję widać, że została zdefiniowana akcja, która wydarzy się po wciśnięciu przez użytkownika spacji. Nie dzieje się tutaj nic więcej, aniżeli prosta negacja zmiennej logicznej pauseBallsMove. Zmienna ta może się przydać np. do zatrzymania ruchu elementów w naszym przykładowym programie:

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

Przykładowa aplikacja

A teraz obiecany przykład :-) Nie zawiera on szczególnie wiele kodu, ale efekt jaki pozwala osiągnąć wydaje się całkiem interesujący. Na początek polecam dodać dodatkowe polecenia obsługujące zdarzenie naciśnięcia klawisza 'a' i 'd'. Naciśniecie pierwszej literki będzie powodować dodanie kolejnych kulek na ekranie, natomiast naciśnięcie drugiej zmniejszenie ich liczny.

case 'a':
  myBall.push_back( Ball(&canvas) );
  break;
case 'd':
  if (myBall.size()>=1) myBall.pop_back();
  break;

Kolejną zmianą jaką można wprowadzić jest dodanie kilku linii kodu, reagujących na klikniecie myszką. Uzupełniamy ciało funkcji mousePressed następującym kodem:


for(int i=0; i < myBall.size(); i++) {
  if(myBall[i].checkMouseOver(mouseX, mouseY)) {
    myBall[i].decrementLives();

    if(myBall[i].isEndLive()) myBall.erase(myBall.begin()+i);
  }
}

Teraz jeszcze wystarczy dodać kilka nowych funkcji do klasy Ball:


bool Ball::checkMouseOver(int _mouseX, int _mouseY) {
  bool mouseIsOver = false;

  if ( dist(_mouseX,_mouseY,x,y) < dim ) mouseIsOver = true;

  return mouseIsOver;
}

void Ball::decrementLives() {
  lives--;
}

bool Ball::isEndLive() {
  return (lives <= 0);
}

i chyba nie trudno się domyślić jaki w ten sposób efekt możemy uzyskać :-)

Dla osób zniecierpliwionych lub takich, które nie mają czasu na samodzielną kompilację przykładu, przygotowałem krótki filmik pokazujący efekty wprowadzenia obsługi klawiatury i myszki do omawianej aplikacji:


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ą.