poniedziałek, 16 maja 2011

OpenCV - obsługa klawiatury i myszki

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:


0 komentarze:

Prześlij komentarz