Gra sterowana kamerą - pomaluj płot

19.05.2013 - Filip Mróz
TrudnośćTrudność

Szkielet gry

W tej części napiszemy działający szkielet gry, który potem uzupełnimy o część analizującą obraz z kamery.

Gotowy kod z tej części można znaleźć w paczce ze źródłami w katalogu: Wersja_1_Szkielet

Zacznijmy od opracowania schematu gry. Z uwagi na przyjęte założenia dotyczące gry, nie będzie on zbyt skomplikowany.

Sednem gry będzie wywoływanie wzdłuż ścieżki StanGry $ \leadsto $ StanMalowania dwóch metod: uaktualnij stan gry oraz wyświetl stan gry. Na przykład StanGry wyświetli pozostały czas rozgrywki, StanGracza nazwę gracza, a StanMalowania pokaże pomalowany kawałek płotu. Najtrudniejszą część, czyli przetwarzanie obrazów związane z wykrywaniem pędzla, zgrupujemy w osobnej klasie. Każdy stan malowania będzie miał własną instancję tej klasy, dzięki czemu będzie mógł ją odpowiednio sparametryzować. Przyda się to podczas wprowadzenia drugiego gracza.

Implementacja szkieletu

Mamy już ogólny zamysł, pora go więc przenieść na odpowiednie klasy C++. Będziemy to robić od góry, to znaczy zaczniemy od main.cpp, w którym zainicjujemy nową grę i dodamy przetwarzającą pętlę. Najpierw dodajmy odpowiednie deklaracje:

#include "Stany.h"
 
VideoCapture kamera;
StanGry stanGry;
Mat aktualnyObraz;
Mat wyswietlanyObraz;
main.cpp

aktualnyObraz będzie wejściowym obrazem z kamery, natomiast wyswietlanyObraz będzie reprezentował wyjściowy obraz z nakreślonym aktualnym stanem rozgrywki.

Po udanym połączeniu z kamerą, ustawimy nazwę gracza i za pomocą metody rozpocznijGre zainicjujemy rozgrywkę oraz ustawimy czas jej trwania.

printf("Kamera podlaczona!\n");
stanGry.gracz.nazwa = "Filip";
stanGry.rozpocznijGre(20);
main.cpp

Teraz zastąpimy wyświetlanie testowego obrazu główną pętlą programu:

while(waitKey(10)!=' ') {
    kamera >> aktualnyObraz;
    stanGry.uaktualnijStanGry(aktualnyObraz);
    aktualnyObraz.copyTo(wyswietlanyObraz);
    stanGry.wyswietlStanGry(wyswietlanyObraz);
    imshow("Maluj plot!",wyswietlanyObraz);
    if(stanGry.graTrwa() == false) break;
}
main.cpp

Pętlę będzie można przerwać naciśnięciem spacji. Na podstawie pobranego obrazu uaktualnimy stan gry, który następnie wyświetlimy na kopii pobranego obrazu. Całość pokażemy w oknie o nazwie "Maluj plot!". Na koniec sprawdzimy, czy gra się już nie skończyła.

StanGry

Zejdźmy poziom niżej do klasy StanGry w pliku Stany.h, gdzie będziemy zbiorczo trzymać wszystkie deklaracje stanów. Dodajmy pliki nagłówkowe, z których metod będziemy korzystać:

#pragma once
 
#include <string>
#include <cstdlib>
#include <ctime>
using namespace std;
 
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/highgui/highgui.hpp"
using namespace cv;
 
#include "Uzytki.h"
Stany.h

Plik Uzytki.h będzie zbiorem różnych prostych metod wykorzystywanych w różnych stanach. Dodajmy teraz klasę StanGry, za pomocą której chcemy sterować przebiegiem gry:

class StanGry {
protected:
    bool _graTrwa;
    double _czasRozgrywki;
    double _pozostalyCzasGry;
 
    clock_t _rozpoczecieGry;
public:
    void rozpocznijGre(double czasRozgrywki = 20);
    void zakonczGre();
 
    bool graTrwa();
};
Stany.h

Informacje o rozpoczęciu gry i pozostałym czasie pozwolą nam dość dokładnie wyznaczać czas trwania jednej "klatki" gry.

Oczywiście stan gry musi zawierać jeszcze StanGracza reprezentujący aktualną sytuację gracza. Musimy też zadeklarować dwie kluczowe metody: uaktualnij grę na podstawie podanego obrazu i przedstaw aktualny stan na obrazie:

    $ \dots $
public:
    StanGracza gracz;
 
    void uaktualnijStanGry(Mat obraz);
    void wyswietlStanGry(Mat gdzie);
    $ \dots $
Stany.h

Przejdźmy do implementacji tych funkcji w pliku StanGry.cpp. Część dotycząca sterowania rozgrywką jest trywialna:

#include "Stany.h"
 
void StanGry::rozpocznijGre(double czasRozgrywki) {
    _rozpoczecieGry = clock();
    _czasRozgrywki = czasRozgrywki;
    _pozostalyCzasGry = czasRozgrywki;
    _graTrwa = true;
}
 
void StanGry::zakonczGre() {
    _graTrwa = false;
}
 
bool StanGry::graTrwa() {
    return _graTrwa;
}
StanGry.cpp

Po prostu ustawiamy nazwę gracza i przepisujemy resztę pól, zapisując przy okazji moment rozpoczęcia rozgrywki.

W przypadku uaktualnienia stanu gry musimy wyliczyć czas, jaki upłynął od ostatniego wywołania i przesłać tę informację do stanu gracza. Na szczęście mamy wszystkie potrzebne do tego elementy:

void StanGry::uaktualnijStanGry(Mat obraz) {
    if(_graTrwa) {
        double nowyPozostalyCzasGry = _czasRozgrywki - (double)(clock() - _rozpoczecieGry)/CLOCKS_PER_SEC;
        gracz.uaktualnijStanGracza(_pozostalyCzasGry - nowyPozostalyCzasGry,obraz);
        _pozostalyCzasGry = nowyPozostalyCzasGry;
 
        if(_pozostalyCzasGry < 0) zakonczGre();
    }
}
StanGry.cpp

Obliczamy pozostały czas rozgrywki i odejmujemy go od czasu zapisanego w ostatniej klatce. Z tą informacją udajemy się do StanGracza i każemy mu się uaktualnić.

Pozostało wyświetlanie stanu gry, gdzie poza przesłaniem wywołania dalej (tzn. do stanu gracza), chcemy także wyświetlić pozostałą ilość czasu.

void StanGry::wyswietlStanGry(Mat gdzie) {
    if(_graTrwa) {
        gracz.wyswietlStanGracza(gdzie);
        char pozostalyCzas[20];
        sprintf(pozostalyCzas,"%.2lf s.", _pozostalyCzasGry);
        Uzytki::wypiszTekst(gdzie,pozostalyCzas,Point(0,gdzie.rows-20));
    }
}
StanGry.cpp

Mamy już implementację StanGry, brakuje nam jeszcze Uzytki i StanGracza. Odważnie udamy się w stronę większego wyzwania, jakim jest StanGracza.

StanGracza

Dodajmy deklarację klasy StanGracza, w której możemy znaleźć kolejny "stan", tym razem StanMalowania. Można się zastanowić po co dodajemy jeszcze jeden poziom, bo przecież aktualnie jeden gracz to tylko jeden płot. Coś nam jednak mówi, że w przyszłości gra będzie mieć parę przeplatających się etapów.

class StanGracza {
public:
    string nazwa;
    StanMalowania stanMalowania;
 
    void uaktualnijStanGracza(double czas, Mat obraz);
    void wyswietlStanGracza(Mat gdzie);
};
Stany.h

Pora na implementację, która jest krótka, ponieważ spychamy całą pracę na StanMalowania. Jedyne, co teraz robimy, to wyświetlenie na ekranie nazwy gracza i informacji o aktualnie pomalowanej części.

#include "Stany.h"
 
void StanGracza::uaktualnijStanGracza(double czas, Mat obraz) {
    stanMalowania.uaktualnijStan(czas,obraz);
}
 
void StanGracza::wyswietlStanGracza(Mat gdzie) {
    stanMalowania.wyswietlStan(gdzie);
    Uzytki::wypiszTekst(gdzie,nazwa,Point(0,25));
    Uzytki::wypiszTekst(gdzie,"Pomalowano: " +   Uzytki::ftos(stanMalowania.wyznaczPomalowanaCzesc()*100,0) + "%",Point(0,50));
}
StanGracza.cpp

Widać, że w Uzytki będziemy musieli umieścić prostą funkcję wypisującą liczbę zmiennoprzecinkową, ale, jako że jest to rzecz prosta, zajmiemy się tym dopiero po implementacji StanMalowania.

StanMalowania

W tej klasie zawrzemy całą informację o malowaniu oraz metody nim sterujące. Zgodnie ze zwyczajem, najpierw dodajmy deklaracje:

#include "PrzetwarzanieObrazow.h"
 
class StanMalowania {
protected:
    static const int ROZMIAR_PEDZLA = 20;
    Mat _aktualnyStan;
public:
    Vec3b kolor;
    PrzetwarzanieObrazow przetwarzanieObrazow;
 
    StanMalowania();
    double wyznaczPomalowanaCzesc();
    void uaktualnijStan(double czas, Mat obraz);
    void wyswietlStan(Mat gdzie);
};
Stany.h

Zgodnie z ogólnym projektem, w PrzetwarzanieObrazow umieścimy wszystkie metody związane z analizą obrazu z kamery. kolor będzie kolorem "farby", którym będziemy oznaczać pomalowany obszar. W _aktualnyStan będziemy trzymać pomalowany dotąd obszar.

Implementację umieścimy w pliku StanMalowania.cpp. W konstruktorze stanu malowania ustawimy kolor farby:

StanMalowania::StanMalowania() {
    kolor = Vec3b(0,0,255);
}
StanMalowania.cpp

Wbrew pozorom nie będzie to kolor niebieski, a czerwony, ponieważ używana jest reprezentacja BGR (blue, green, red), czyli odwrotna do powszechnego RGB.

W wyznaczeniu pomalowanej części płotu wykorzystamy funkcję countNonZero, która zlicza liczbę niezerowych pikseli w obrazie (tak też będziemy zaznaczać pomalowane obszary). Na razie będziemy traktować cały obraz jako płot, więc dzielimy obszar pomalowany przez całkowitą liczbę pikseli w obrazie.

double StanMalowania::wyznaczPomalowanaCzesc() {
    return (double)cv::countNonZero(_aktualnyStan)/_aktualnyStan.total();
}
StanMalowania.cpp

Zajmijmy się teraz kluczowymi dla stanu malowania funkcjami, czyli uaktualnieniem stanu malowania i jego wyświetleniem.

void StanMalowania::uaktualnijStan(double czas, Mat obraz) {
    if(_aktualnyStan.data==NULL) {
        _aktualnyStan = Mat(obraz.size(),CV_8U,Scalar(0));
    }
    Point polozenie = przetwarzanieObrazow.polozeniePedzla(obraz);
    if(polozenie.x>0) circle(_aktualnyStan,polozenie,ROZMIAR_PEDZLA,255,-1);
}
StanMalowania.cpp

Przy pierwszym uruchomieniu zainicjalizujemy _aktualnyStan pustym obrazem o rozmiarach obrazu z kamery. Następnie wyznaczymy położenie pędzla za pomocą metody klasy PrzetwarzanieObrazow, której implementację zostawimy sobie na koniec. Metoda ta, w przypadku nie znalezienia pędzla, zwróci punkt (0,0), więc musimy sprawdzić tę możliwość. Jeśli pędzel został znaleziony, rysujemy koło na _aktualnyStan w miejscu jego znalezienia. Ostatni argument circle to grubość rysowanego okręgu: -1 oznacza, że żądamy koła.

Wyświetlenie stanu to po prostu zamalowanie kolorem miejsc gdzie _aktualnyStan jest różny od zera.

void StanMalowania::wyswietlStan(Mat gdzie) {
    if(_aktualnyStan.data!=NULL) {
        gdzie.setTo(kolor,_aktualnyStan);
    }
}
StanMalowania.cpp

Do implementacji pozostały Uzytki i Przetwarzanie Obrazow. Zacznijmy od lżejszego zadania, czyli napisania Uzytki.

Użytki

Dodajmy deklaracje w pliku Uzytki.h:

#pragma once
 
#include <string>
#include <cstdlib>
using namespace std;
 
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/highgui/highgui.hpp"
using namespace cv;
 
namespace Uzytki {
    void wypiszTekst(Mat gdzie, String tekst, Point polozenie);
    string ftos(double liczba, int dokladnosc);
}
Uzytki.h

Do implementacji wypisywania tekstu wykorzystamy funkcję OpenCV putText używając standardowych ustawień. Ustawmy kolor tekstu na zielony, skalę na 1 i grubość na 3.

#include "Uzytki.h"
 
void Uzytki::wypiszTekst(Mat gdzie, String tekst, Point polozenie) {
    putText(gdzie, tekst, polozenie, CV_FONT_HERSHEY_SIMPLEX, 1, cvScalar(0, 255, 0, 0),3);
}
Uzytki.cpp

Do zamiany liczby zmiennoprzecinkowej na tekst z określoną dokładnością użyjemy operacji printf. Najpierw wypiszemy tekst formatujący, a potem wykorzystamy go do wypisania liczby.

string Uzytki::ftos(double liczba, int dokladnosc) {
    char znacznik[10];
    sprintf(znacznik,"%%.%dlf",dokladnosc);
    char wynik[20];
    sprintf(wynik,znacznik,liczba);
    return wynik;
}
Uzytki.cpp

By doprowadzić ten szkielet do kompilacji, musimy jeszcze napisać PrzetwarzanieObrazow.

Szkielet przetwarzania obrazów

W kolejnej części zajmiemy się prawdziwą implementacją tej klasy. Teraz chcemy tylko doprowadzić do kompilacji i uruchomienia programu. Zacznijmy od deklaracji w PrzetwarzanieObrazow.h

#pragma once
 
#include <string>
#include <cstdlib>
using namespace std;
 
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/highgui/highgui.hpp"
using namespace cv;
 
class PrzetwarzanieObrazow {
public:
    Vec3b kolor;
    Point polozeniePedzla(Mat obraz);
};
PrzetwarzanieObrazow.h

Implementacja w pliku PrzetwarzanieObrazow.cpp będzie udawana, czyli jej zadaniem będzie tylko umożliwienie poprawnej kompilacji.

#include "PrzetwarzanieObrazow.h"
 
Point PrzetwarzanieObrazow::polozeniePedzla(Mat obraz) {
    return Point(100,100);
}
PrzetwarzanieObrazow.cpp

Uruchomienie

Należy pamiętać o dodaniu opencv_imgproc230.lib do linkera, inaczej w przyszłości pojawią się błędy podczas linkowania.

Projekt powinien się już kompilować, w razie problemów można zerknąć do kodu źródłowego. Po uruchomieniu powinniśmy zobaczyć podobny widok:

5
Twoja ocena: Brak Ocena: 5 (2 ocen)

Copyright © 2008-2010 Wrocławski Portal Informatyczny

design: rafalpolito.com