Virtuális tárgyak valósidejű térbeli illesztése külső kamerás képre Diplomaterv Alföldi Norbert V. éves informatika szakos hallgató
Konzulens: Nepusz Tamás
Budapesti Műszaki és Gazdaságtudományi Egyetem Villamosmérnöki és Informatikai Kar
Budapest, 2008
Nyilatkozat Alulírott Alföldi Norbert, a Budapesti Műszaki és Gazdaságtudományi Egyetem hallgatója kijelentem, hogy ezt a diplomatervet meg nem engedett segítség nélkül, saját magam készítettem, és a diplomatervben csak a megadott forrásokat használtam fel. Minden olyan részt, melyet szó szerint vagy azonos értelemben, de átfogalmazva más forrásból átvettem, egyértelműen, a forrás megadásával megjelöltem.
Értékelés.............................................................................................................57 5.1. Teljesítmény mérése.................................................................................57 5.1.1. Szintetikus adatok..........................................................................57 5.1.2. Webkamerás kép............................................................................65 5.1.3. Videófájl........................................................................................68 5.2. ARToolKit.................................................................................................69 5.3. Továbbfejlesztési lehetőségek....................................................................71 5.3.1. Képfeldolgozás...............................................................................71 5.3.2. Számítógépes grafika......................................................................73 5.4. Összefoglalás............................................................................................74 A. függelék.........................................................................................................76 A highgui modul újrafordítása...........................................................................76 B. függelék.........................................................................................................78 A DVD-melléklet tartalma.................................................................................78 Irodalomjegyzék..................................................................................................79
Kivonat A világ megfigyelésére használt elsődleges érzékszervünk a szemünk. Ha a valós világot utánozni szeretnénk, elsőként a szemünkkel kell elhitetnünk, hogy a látvány valóságos. Ez számtalan esetben sikerül is, gondoljunk csak a filmipar néhány klasszikus példájára, ahol sokszor nem tudjuk eldönteni, hogy egy jelenetben hol van a számítógépes grafika és a valóság határa. Enélkül sok film sikere kérdésessé is válna. A filmipar azonban csak a legismertebb példák egyike, ahol a valós és virtuális világ keveredik és kölcsönösen kiegészíti egymást, említhetnénk még számtalan más területet, ahol a témakör kutatása folyik. Ahhoz, hogy egy valós jelenetbe számítógépes grafikával készült modelleket tudjunk illeszteni, pontosan rekonstruálnunk kell a jelenetet rögzítő kamera mozgását, tulajdonságait. Az első ilyen irányú próbálkozásoknál a kamerát rögzített, előre ismert pályákon mozgatták, így tudták meghatározni a viszonylag pontos helyzetét minden időpillanatban. Ez a módszer azonban meglehetősen korlátozott lehetőségeket biztosított, a megoldást pedig olyan eljárások hozták, melyek pusztán a felvett képszekvenciából elő tudják állítani az azt rögzítő kamera paramétereit akár utófeldolgozási lépésben, akár valós időben. Ezáltal vált lehetővé, hogy gyakorlatilag bármilyen videóra virtuális tárgyakat tudjunk illeszteni. Az ezt megvalósító rendszereket, illetve magát a tárgyterületet „augmented reality”-nek nevezzük, amit leginkább kiterjesztett- vagy kibővített valóságként lehetne fordítani. Dolgozatomban áttekintem ezen rendszerek elméleti hátterét és gyakorlati alkalmazását egy lehetséges megvalósításon keresztül. Munkám során a kiterjesztett valóság rendszerek minden lényegesebb komponensével foglalkozom: alkalmas módszert adok a valós kamerák mozgásának és perspektivikus leképzésének rekonstruálására, majd ezen paraméterek ismeretében a kamera által látott képre számítógépes grafikával előállított modellek rajzolását megvalósító modult tervezek, amely a valós és virtuális tárgyak takarási viszonyait helyesen jeleníti meg. Munkám részeként a megtervezett rendszert implementáltam, és egy, a rendszer képességeit bemutató demo alkalmazást készítettem.
Abstract Our eyes are the primary organs we use for observing the world. In order to simulate the real world in a virtual environment, it is essential to convince our eyes that the illusion is real. This goal is fulfilled in many cases, just think about movies, where it is often difficult to tell which objects in a scene are real and which ones are rendered by computers. Without this, the success of several movies would be questionable. However, the movies are only one of the best-known examples where the real and virtual worlds are mixed and complement each other. The first step in inserting a computer-generated model into a real scene is to reconstruct the exact properties and movements of the camera used for recording the scene. The first attempts involved cameras moved on a fixed path that was completely known before the recording phase, therefore it was possible to obtain more or less precise estimates of camera positions in every second. However, this approach was rather limited, and the current stateof-the-art solutions are able to estimate camera parameters solely from the recorded image sequence either during post-processing or in real time, allowing one to render virtual objects on practically any kind of video recordings. These systems and this specific field of research is called "augmented reality". In my thesis, I give a review of the theoretical background of these systems and demonstrate the way they are used in practice by constructing a possible implementation. I deal with all major components of augmented reality systems: I discuss methods for estimating the intrinsic and extrinsic parameters of real cameras and I present a software module that renders computer-generated models onto images recorded by the calibrated camera. This module is also able to deal with the problem of rendering virtual objects that are partially covered by real ones. I also present an application that demonstrates the most important features of the implemented module.
1. fejezet
1
1. fejezet
Bevezetés 1.1. A feladat megfogalmazása Az igény, hogy a valós világban nem létező, vagy csak nehezen hozzáférhető tárgyakat, jeleneteket jelenítsünk meg, régóta foglalkoztatja a kutatókat. A mai számítógépes grafikai rendszereknek ezen feladatok nagy része nem okoz gondot, azonban a technika jelenlegi állása szerint még vannak olyan – a valóságban is létező – tárgyak, melyek kellő pontosságú modellezése és megjelenítése komoly problémákat vet fel. A mérnöki gyakorlatban sokszor találkozhatunk olyan megoldással, ahol két, egymás előnyeit és hátrányait jól kiegészítő módszerből mindkettő előnyeit megtartva, hátrányait pedig kiküszöbölve összeillesztünk egy ”kevert” megoldást. A természet megjeleníti számunkra a legbonyolultabb valóságos alakzatokat is, számítógépes grafika segítségével pedig a valóságos képet ki tudjuk egészíteni
olyan
virtuális
tárgyakkal,
melyek
akár
a
természet
törvényeinek
is
ellentmondanak. Virtuális tárgyak illesztése valós képre számtalan esetben hasznos lehet, kezdve az egyszerűbb segédjelzések, pl. nehezen követhető, gyors mozgású tárgyak trajektóriájának kirajzolásával egészen a komolyabb, nagy számítási kapacitást igénylő alkalmazásokig. Az illesztés történhet akár valós időben, akár utófeldolgozási lépésben. Valósidejű rendszerek esetén a gyors, autonóm és robusztus működés kerül előtérbe, míg az utófeldolgozás jellemzően emberi beavatkozást igényel és nem időkritikus, így a hangsúly inkább a pontosságra tevődik. Előbbire példa lehet a már említett segédjelzések kirajzolása, míg utóbbi módszerek legismertebb felhasználási területe a filmipar. Dolgozatomban
1.1. A feladat megfogalmazása
2
valósidejű rendszerekkel foglalkozom, melynek eredeti célja az volt, hogy a Méréstechnika és Információs Rendszerek Tanszék által fejlesztett mobil robotok, az ún. mitmótok térbeli mozgásának megfigyelésére és jellemzésére segédjelzéseket tudjunk elhelyezni a kamera képére. A megoldandó feladat végül túlnőtt ezen a specifikus alkalmazási területen és a kitűzött cél egy olyan valósidejű rendszer megvalósítása lett, mely a segédjelzéseknél bonyolultabb tárgyak, modellek megjelenítését is lehetővé teszi. Felhasználási területe így teljesen általános, nem szorítkozik egyetlen specifikus problémára. A fejlesztés során ebből kifolyólag külön figyelmet fordítottam arra, hogy a rendszer felépítése átlátható, rugalmas és bővíthető legyen, a meglévő modulok pedig könnyen cserélhetők.
1.2. A dolgozat szerkezete A dolgozat öt fejezetet tartalmaz. Az első fejezet megfogalmazta a megoldandó problémát, a második fejezetben pedig ismertetésre kerülnek a megoldás során felhasznált eszközök. A harmadik fejezet a valós és virtuális kamerákkal foglalkozik, bevezetésre kerül a virtuális kameramodell, ismertetésre kerülnek a valós kamerák fizikai tulajdonságai és korlátai, a fejezetet pedig a virtuális tárgyak illesztését lehetővé tevő ún. kalibrációs algoritmus elméleti ismertetése zárja. A negyedik fejezet a megvalósítás részleteit tartalmazza, az ötödik fejezetben pedig értékelem az elvégzett munkát, ismertetem a megvalósított rendszer teljesítményének mérőszámait, végül pedig megvizsgálom a továbbfejlesztés lehetőségeit és ezek megoldására javaslatot teszek.
2. fejezet
3
2. fejezet
Felhasznált szoftverkomponensek Ezen fejezet egy bevezető áttekintést ad a feladat megoldása során felhasznált szoftverkomponensekről. Képfeldolgozásra az OpenCV, háromdimenziós rajzolásra az OpenGL,
térbeli
hangzás
megvalósítására
pedig
az
OpenAL
programkönyvtárakat
használtam.
2.1. OpenCV 2.1.1. Bevezetés Az Open Source Computer Vision Library (továbbiakban OpenCV) egy C/C++ nyelven írt magasszintű képfeldolgozó függvénykönyvtár, mely eredetileg az Intel1 gondozásában készült [1]. Idővel egy igen aktív közösség szerveződött köré, így mára már az őket tömörítő Yahoo Groups csoport az elsődleges kiindulópont a tájékozódáshoz. Az OpenCV elsősorban valósidejű alkalmazásokra optimalizált, ezenfelül az inteles kötődésnek köszönhetően hozzáférést biztosít az Intel Integrated Performance Primitives2 könyvtárhoz, amivel további gyorsítást tud elérni a támogatott processzorokkal rendelkező (pl. a Pentium-széria) rendszerek esetén. Azon túl, hogy mind kutatási, mind pedig kereskedelmi célokra egyaránt 1 http://intel.com 2 Az Intel IPP alacsonyszintű, többmagos processzorokat is támogató, erősen optimalizált hívások gyűjteménye multimédiás adatelérésekhez, ami azonban az OpenCV-vel ellentétben nem ingyenes.
2.1. OpenCV
4
ingyenes és nyílt forráskódú, platformfüggetlensége (operációs rendszer-, ablakkezelő- és hardverfüggetlen) is hozzásegítette a széleskörű elterjedéshez. Az OpenCV négy modulból épül fel, melyek a logikailag összetartozó hívásokat foglalják magukba. Ezek a cv, cvaux, cxcore és a highgui, melyek rendre a főbb OpenCV hívásokat, kiegészítő eljárásokat, adatszerkezeteket és lineáris algebrát, valamint grafikus felhasználó felület hívásait tartalmazzák.
2.1.2. Hello World OpenCV Az alábbi példában a klasszikus „Hello World!” programot valósítjuk meg tisztán OpenCV hívásokat felhasználva. #include int main( int argc, char *argv[] ) { IplImage* image = 0; CvFont font; image = cvCreateImage( cvSize( 300, 50 ), IPL_DEPTH_8U, 3 ); for ( int i = 0; i < image->height; i++ ) for ( int j = 0; j < image->width; j++ ) for (int k = 0; k < image->nChannels; k++ ) image->imageData[ i * image->widthStep + j * image->nChannels + k ] = 200; cvInitFont( &font, CV_FONT_HERSHEY_SIMPLEX, 1.0, 1.0, 0.0, 2 ); cvPutText( image, "Hello World!", cvPoint( 50, 35 ), &font, cvScalar( 255, 255, 255 ) ); cvNamedWindow( "OpenCV", CV_WINDOW_AUTOSIZE ); cvShowImage( "OpenCV", image ); cvWaitKey( 0 ); cvReleaseImage( &image ); return 0; }
Ehhez a példaprogramhoz két modult is felhasználtunk a négy közül: a képkezelő és szövegkiíró hívásokhoz a cxcore-ra, az ablak- és eseménykezelő hívásokhoz pedig a highgui-ra van szükségünk. Az előfeldolgozó számára azonban csak a highgui beépítését kell előírnunk (#include ), mivel a cxcore modult az OpenCV belső implementációi is használják, tehát implicit módon már a highgui modulban beépítésre került. Ugyanakkor az sem okoz fordítási hibát, ha explicit módon is előírjuk a beépítését (#include ), mivel feltételes makró védi a duplikációtól.
2.1. OpenCV
5
Ebben a rövid forráskódban is jól látható, hogy minden OpenCV hívás a "cv" prefixummal rendelkezik, egyetlen kivétel az IplImage típus, ami a képek tárolására kialakított struktúra. Mutatóként
deklaráltuk,
így
a
memórában
történő
helyfoglalásról
nekünk
kell
gondoskodnunk. Dinamikus adatszerkezeteknél meg kell adnunk, hogy mekkora helyet igényelünk a memóriában, ami képek esetén több paramétertől is függ. A cvCreateImage ennek megfelelően három paramétert vár: egy, a kép szélességéből és magasságából képzett CvSize3 típust, a színmélységet, végül pedig a színcsatornák számát. A színmélységet az IPL_DEPTH_8U konstanssal adtuk meg, ami azt jelenti, hogy képünk színkomponensei 8 biten és előjel nélkül tárolódnak, tehát minden színkomponens a 0-tól 255-ig terjedő tartományban vehet fel értéket. Ebben a példában egyszínű szürke háttérszínt használunk, így elegendő egyetlen csatorna is a színinformációknak, a gyakorlati esetek többségében viszont az RGB komponenseknek megfelelően háromcsatornás képekekkel dolgozunk. A helyfoglalás után már szabadon hivatkozhatunk a képstruktúra adatmezőire a "->" operátorral. A három egymásba ágyazott ciklus sorfolytonosan bejárja a kép összes pontját és csatornáját, eközben a képpontok színét az egy byte-os tartományban beállítja 200-ra. Az OpenCV lehetőséget biztosít szövegek kiírására és egyszerűbb alakzatok rajzolására. Példánkban a “Hello World!” szöveget szeretnénk kiírni az előzőleg létrehozott szürke színű képre. Ehhez elsőként egy CvFont típusú változót kell deklarálnunk, ami a kiírás paramétereit tartalmazza, pl. a betűméretet és a vonalvastagságot, értékeit pedig a cvInitFont eljárással tudjuk beállítani. Ezt a változót felhasználva már kiírathatjuk a képre a szöveget a cvPutText eljárással, tetszőleges színnel, tetszőleges pozícióba. Ha ezen a ponton lefordítjuk és futtatjuk programunkat, semmit nem fogunk látni az eddigi munkálkodásunkból, ugyanis csak a memóriában dolgoztunk, de nem jelenítettük meg a képet, amit létrehoztunk. Ahhoz, hogy kirajzolhassunk valamit a képernyőre, szükségünk van egy ablakra. Az OpenCV highgui modulja tartalmazza a grafikus felhasználói felület eszköztárát, így innen fogunk meghívni néhány eljárást. Itt érdemes megjegyezni, hogy a highgui csak egyszerű megvalósításokat tartalmaz, melyek célja a gyors és könnyű 3 Az OpenCV elnevezési konvenciókat követve típusok megnevezésekor a prefixum nagy kezdőbetűvel, minden más esetben kis kezdőbetűvel szerepel.
2.1. OpenCV
6
használat prototípusok készítésére, ezért végfelhasználói programokhoz nem javasolt. A "Hello World!" programot azonban nem végfelhasználóknak szánjuk, így nyugodtan használhatjuk
a
highgui
hívásokat.
Elsőként
létrehozunk
egy
üres
ablakot
a
cvNamedWindow eljárással, mely első paramétere határozza meg, hogy a későbbiekben milyen névvel hivatkozhatunk erre az ablakra, második paraméterben pedig azt írjuk elő, hogy az ablak mérete automatikusan igazodjon a benne megjelenített kép méretéhez. Ezután a cvShowImage segítségével egyszerűen csak meg kell adnunk, hogy ebbe az ablakba melyik képet szeretnénk kirajzoltatni.
Fordítás és futtatás után az alábbi eredményt kapjuk:
2.1. ábra. A „Hello World” program futási eredménye OpenCV-ben. Programunk ezután a cvWaitKey híváshoz érkezve egy billentyű lenyomására vár. A várakozás maximális időtartamát zárójelben adhatjuk meg, ahol a nulla az időbeli korlát hiányát jelöli, tehát a hívás addig blokkolódik, amíg egy billentyű leütésre nem kerül. Végül pedig nem szabad megfeledkeznünk a kép számára lefoglalt memória felszabadításáról.
2.2. OpenGL 2.2.1. Bevezetés Az Open Graphics Library (továbbiakban OpenGL) egy grafikus függvényekből álló könyvtár, mely
egy
egységes,
átlátszó
felületet
(interface)
biztosít
grafikus
hardverek
programozásához. Eredetileg a Silicon Graphics cég fejlesztette ki az akkor még IrisGL-nek nevezett könyvtárat saját munkaállomásaira, melyek nagy sebességű grafikus célhardverek,
2.2. OpenGL
7
később azonban kódjaik hordozhatósága érdekében platformfüggetlenné tették, amit a nevébe bekerült "open" szó is jelez. 1992-es bemutatkozása óta széles körben elterjedt és számtalan alkalmazás született, amely a két- és háromdimenziós megjelenítéshez az OpenGL-t használja. Az előző fejezetben említésre került, hogy az OpenCV is rendelkezik bizonyos rajzolási funkciókkal. Első megközelítésben a virtuális tárgyak kirajzolásához ezen eszközöket használtam, azonban az OpenGL számtalan előnyt kínált az OpenCV korlátozott rajzolási képességeihez képest, ezért úgy döntöttem, a rajzolást teljes egészében áthelyezem OpenGL alapokra. Ugyanakkor az OpenGL-t csak rajzolásra tudjuk használni, így pl. a kamera képének lekérdezése és lényegében minden képkezelő eljárás továbbra is az OpenCV hívásain keresztül valósul meg. A rajzolásnál az OpenGL előnyei közül az egyik legfontosabb a hardveres gyorsítás, mivel közvetlenül a grafikus hardvert programozzuk4, így jelentős sebességnövekedést érhetünk el. Ez azért is különösen fontos, mert valósidejű alkalmazást tervezünk, tehát a rendszerünk egyik alapvető fontosságú kérdése a gyors működés. Ha a virtuális
tárgyak
kirajzolásának
sebessége
elmaradna
a
kamera
által
nyújtott
másodpercenkénti képkeretek számától (frame per secundum – FPS), tárgyaink folyamatos lemaradásban követnék csak a valós világ képét, aminek következtében lehetetlenné válna a pontos illesztés. Emellett az OpenGL megoldja számunkra a takarást, a nem látható alakzatok vágását és még számos olyan – a háromdimenziós megjelenítéssel járó – feladatot, melyek mindegyikét sajátkezűleg kellett volna megvalósítani szoftveresen úton.
2.2.2. GLUT-család Az OpenGL önmagában nem tartalmaz ablak- és eseménykezelőt, ezért szükségünk lesz egy ezen funkciókat megvalósító eszközre. Számos ingyenes és nyílt forráskódú alternatíva közül választhatunk; kis- és középméretű programokhoz az OpenGL Utility Toolkit (továbbiakban GLUT) javasolt. Kis- és középméret alatt természetesen nem a program által megvalósított funkciók komplexitását kell érteni, hanem az ablakozó rendszerrel szemben támasztott igények összetettségét. A GLUT ugyanis csak alapvető szolgáltatásokat nyújt, ha kifinomultabb felhasználói felületre van szükségünk (pl. gördítősávra), a [4] és az [5] 4 Ha a grafikus kártya nem támogatja a hardveres gyorsítást, a rajzolás szoftveresen történik.
2.2. OpenGL
8
rendszereket javaslom. Esetünkben azonban nincs szükség ilyen szolgáltatásokra, mivel csak kirajzolásra fogjuk használni az ablakot, így a GLUT funkcionalitása tökéletesen elegendő céljainkra. Mindazonáltal a GLUT – alább ismertetett – sajátosságainak egyike nem illeszkedett jól a megvalósítandó feladathoz, így a vele egyenértékű, illetve néhány ponton eltérő OpenGLUT használata mellett döntöttem. A GLUT-ot eredetileg Mark J. Kilgard készítette egy, az OpenGL programozásával foglalkozó könyvéhez, azonban a legutolsó verzió is (3.7) 1998-ban jelent meg, azóta fejlesztése abbamaradt. Részben ezen, részben pedig licencelési okokból kifolyólag elkészült a Free OpenGL
Utility
Toolkit
(továbbiakban
freeglut),
amely
a
GLUT
nyílt
forráskódú
továbbfejlesztése. Az evolúciós lánc ezzel nem ér véget: OpenGLUT néven ismét egy új taggal bővült a GLUT-család. Ennek alapjául már a freeglut szolgált, számos funkció további fejlesztésre került és szintén nyílt forráskódú. Ezen rendszerek úgy kerültek kialakításra, hogy nagymértékben kompatibilisek legyen az eredeti GLUT-tal, így az alább ismertetett tulajdonságok többsége mindhárom rendszerre igaz, az eltéréseket külön jelzem.
2.2.3. Hello World OpenGL OpenGLUT Az OpenGL és (Open)GLUT megismeréséhez is a „Hello World!” programot fogjuk áttekinteni, mely bemutatja az ezen eszközöket használó programok általános felépítését. #include #include #include <stdlib.h> void render( ) { char buffer[ 20 ] = "Hello World!"; glClearColor( 0.8, 0.8, 0.8, 0.0 ); glClear( GL_COLOR_BUFFER_BIT ); glViewport( 0, 0, 300, 50 ); glMatrixMode( GL_MODELVIEW ); glPushMatrix( ); glLoadIdentity( ); glMatrixMode( GL_PROJECTION ); glPushMatrix( ); glLoadIdentity( ); gluOrtho2D( 0.0, 1.0, 0.0, 1.0 ); glRasterPos2f( 0.33, 0.3 ); glColor3f( 1.0, 1.0, 1.0 );
2.2. OpenGL
9
for ( int i = 0; i < 20; i++ ) { if ( buffer[ i ] == '\0' ) break; else glutBitmapCharacter( GLUT_BITMAP_HELVETICA_18, buffer[ i ] ); } glPopMatrix( ); glMatrixMode( GL_MODELVIEW ); glPopMatrix( ); glutSwapBuffers( ); } void processNormalKeys( unsigned char key, int x, int y ) { if ( key == 27 ) exit( 0 ); } int main( int argc, char *argv[ ] ) { glutInit( &argc, argv ); glutInitDisplayMode( GLUT_DOUBLE | GLUT_RGB ); glutInitWindowPosition( 800, 400 ); glutInitWindowSize( 300, 50 ); glutCreateWindow( "OpenGL OpenGLUT" ); glutDisplayFunc( render ); glutKeyboardFunc( processNormalKeys ); }
glutMainLoop( );
Az OpenGL hívások „gl”, míg a GLUT hívások „glut” prefixummal rendelkeznek. Mindenekelőtt inicializálnunk kell a GLUT könyvtárat. Ezt a glutInit hívással tehetjük meg, paramétereinek a main függvény paramétereit adjuk át. A glutInitDisplayMode eljárással beállítjuk, hogy a villogó rajzolás elkerülésére a dupla pufferelést5 alkalmazó és RGB színmódú ablakot szeretnénk használni. A következő két sorban megadjuk ablak pozícióját és méreteit, majd létrehozzuk azt a glutCreateWindow hívással. Az eseménykezelés szintén fontos része a GLUT-nak, mivel a felhasználók ezen keresztül tudják befolyásolni a program futását. Ezzel el is érkeztünk ahhoz a sajátossághoz, ami nem illeszkedett jól a megvalósítandó rendszerhez. A GLUT ugyanis az inicializálás után belép egy belső végtelen ciklusba, melyből csak az események hatására lép ki, de akkor is csak azok lekezeléséig. Ez a filozófia jól illeszkedik az animációk világához, ahol a glutIdleFunc hívással beállíthatjuk, hogy ha nem érkezik feldolgozandó esemény, akkor az animáció képkereteit számolja és rajzolja, esetünkben viszont egy saját kialakítású „végtelen” ciklusra van szükség, mely folyamatosan lekérdezi a webkamera képét és feldolgozza azt. Másfelől 5 Dupla pufferelés esetén a GLUT két rasztertárnak foglal helyet. Amíg az egyikbe rajzolunk, a másik kerül megjelenítésre és fordítva.
2.2. OpenGL
10
nincs is szükségünk a webkamera által nyújtott FPS-nél gyakrabban kirajzolni a virtuális tárgyakat, így fölöslegesen foglalnánk a grafikus hardvert. Saját kialakítású főciklusra viszont a GLUT keretein belül nincs lehetőségünk. A freeglut és az OpenGLUT fejlesztői felismerték ezt a problémát és lehetőséget biztosítottak a belső főciklus elkerülésére azáltal, hogy implementáltak egy glutMainLoopEvent nevű hívást, mely olyan eseményt generál, mintha a GLUT főciklusa iterált volna egyet6. Ezt a hívást saját főciklusunkba ágyazva a GLUT belső ciklusával egyenértékű funkcionalitást érünk el. Az eseménykezelést ún. visszahívó (callback) függvényeken keresztül valósíthatjuk meg. A visszahívó függvények lényege, hogy az egyes eseményekhez lekezelő eljárásokat regisztrálhatunk,
melyek
minden
esetben
meghívódnak,
mikor
az
adott
esemény
bekövetkezik. A glutDisplayFunc paraméterében azt adjuk meg, hogy melyik eljárást szeretnénk meghívni a rajzoláshoz. A billentyűzet eseményeit kétfelé osztjuk aszerint, hogy normál (ASCII-kóddal rendelkező) karaktert vagy speciális billentyűt (pl. F1) ütött-e le a felhasználó.
Normál
esetekre
a
glutKeyboardFunc,
speciális
esetekre
pedig
glutSpecialFunc használandó. (Az „Esc” billentyű nem számít speciálisnak, ASCII-kódja a 27.) Az utolsó sorral pedig belépünk a GLUT főciklusába.
Az alábbiakban áttekintjük, hogy az egyes eseményekhez regisztrált lekezelő eljárások mit valósítanak meg. Rajzoláshoz a render nevű eljárást adtuk meg, így ha a képernyő tartalmát újra kell rajzolni, ez az eljárás fog meghívódni. A glClearColor a paraméterként kapott színt állítja be törlőszínként, a glClear pedig törli a megadott puffert, jelen esetben a színpuffert. A glViewport paramétereivel azt tudjuk megadni, hogy az ablak mely területére szeretnénk rajzolni. Példánkban – mint ahogy az esetek többségében – az ablak egész területére igényt tartunk. A következő sorban a glMatrixMode beállítja az aktuális transzformációs mátrixot. Az OpenGL három transzformációt ismer, ezek a modell-nézeti, a projekciós és a textúra transzformációk. Ezen transzformációk között lehet váltani a glMatrixMode segítségével. 6 A példaként bemutatott „Hello World!” programnál nincs ilyen probléma, így használhatjuk a GLUT főciklusát.
2.2. OpenGL
11
Mindhárom mátrixhoz egy verem (stack) tartozik, ahova a glPushMatrix és glPopMatrix hívásokkal tudunk betenni, ill. kivenni egy elemet. Mindig a verem legtetején lévő transzformációs mátrix az érvényes, így minden művelet ezen hajtódik végre (pl. forgatás esetén a forgatást leíró mátrix az aktuális transzformációhoz tartozó verem legfelső elemével szorzódik). Ezen transzformációkat homogén mátrixok reprezentálják, melyek egységeleme az egységmátrix. A glLoadIdentity az aktuális mátrix helyére az egységmátrixot állítja be. Példánkban a modell-nézeti és a projekciós transzformációk közt váltunk, így ezeket nézzük meg részletesebben. A modellezési transzformáció célja, hogy a tárgyak saját modellezési
koordinátarendszerében
koordinátarendszerbe,
a
nézeti
adott
pontjait
transzformáció
áttranszformálja pedig
a
a
virtuális
világkamera
koordinátarendszerébe visz át, mivel a virtuális kamera szemszögéből szeretnénk látni a színteret. Ezt a két transzformációt az OpenGL együtt végzi el (GL_MODELVIEW). A projekciós
transzformáció
a
háromdimenziós
koordinátákat
képezi
le
a
képernyő
kétdimenziós síkjára (GL_PROJECTION). Az OpenGL kétféle vetítést ismer, a párhuzamos (és egyben ortografikus, tehát a vetítőegyenesek merőlegesek a vetítősíkra) és a perspektivikus vetítést. Párhuzamos vetítés esetén a párhuzamosság a vetítőegyenesek egymáshoz képesti viszonyára vonatkozik, így a háromdimenziós tárgyak pontosan ugyanakkora méretben jelennek meg a képernyőn távolról nézve, mint közelről.
Perspektivikus vetítés esetén a
vetítőegyenesek egy pontból indulnak ki, aminek következtében a távolabbi tárgyakat kisebb méretűnek látjuk, mint a közelebbieket. Programunkban a párhuzamos vetítésre láthatunk példát. Háromdimenziós esetben a glOrtho hívással tudjuk specifikálni azt a téglatest alakú látóteret (vágási tartomány), amely a képernyőre vetül. Ha azonban kétdimenzióban szeretnénk rajzolni (a képernyőre történő szöveg kiírása ilyen feladat), a gluOrtho2D hívást használhatjuk, mely vágási tartományként egy téglalapot jelöl ki. Ne tévessze meg az Olvasót a „glu” prefixum, ez a hívás nem az OpenGL és nem is GLUT, hanem az OpenGL Utility Library (továbbiakban GLU) része, amely az OpenGL-re épülő könyvtár számos hasznos függvénnyel. A glRasterPos a pixelműveletekhez jelöli ki egy pixel pozícióját, példánkban a kiírás helyét. Itt érdemes kitérnünk a függvénynevek alakjára. Az OpenGL elnevezési sémája szerint a függvénynevek végén két karakter jelzi a paraméterek számát és típusát. A „2”-es azt jelzi, hogy a függvénynek két paramétert kell megadnunk, az „f” pedig a float jelölése, tehát
2.2. OpenGL
12
mindkét paraméternek float típusúnak kell lennie. A glColor a rajzolási színt specifikálja, szintén megfigyelhető a „3f” jelölés az RGB komponenseknek megfelelően. Az ezt követő ciklus végigmegy a kiírandó szöveget tároló tömbön, a glutBitmapCharacter pedig OpenGL hívásokkal kirajzolja az aktuális karaktert. Miután végeztünk a rajzolással, az elmentett transzformációs mátrixokat visszaállítjuk, végül pedig a dupla pufferelési technika használata miatt meg kell cserélnünk a két puffert a glutSwapBuffers hívással. A render mellett a másik regisztrált eljárásunk a processNormalKeys, melyben csupán egyetlen billentyű lenyomásához van esemény rendelve, mégpedig az „Esc” billentyű lenyomására kilépünk a programból. Fordítás és futtatás után az alábbi, OpenCV-s ablakhoz nagyon hasonló eredményt kapjuk:
2.2. ábra. A „Hello World!” program futási eredménye OpenGL-ben.
2.3. OpenAL 2.3.1. Bevezetés Az
Open
Audio
Library
(továbbiakban
OpenAL)
egy,
a
háromdimenziós
hangzás
megvalósítását elősegítő platformfüggetlen könyvtár. Mondhatjuk, hogy ami a számítógépes grafikában az OpenGL, az az audio területén az OpenAL. A fejlesztés során is az OpenGL filozófiát követték, így számos hasonlóságot fogunk látni a két könyvtár között. Eredetileg a Loki Software cég fejlesztette, melynek fő profilja a Windows játékok Linuxra portolása volt, jelenleg a Creative Technology7 áll mögötte és a szabad szoftverek közösségével karöltve folyik a fejlesztés. Használata az eddig tárgyalt könyvtárakhoz hasonlóan ingyenes. Az OpenGL-nél említett előnyök itt is érvényesek: nem kell foglalkoznunk a hangkártyák 7 http://creative.com
2.3. OpenAL
13
sajátosságaival, egy egységes felületen keresztül programozhatjuk a hardvert. Az OpenAL-nél három alapvető fogalommal kell tisztában lennünk, ezek a hallgató (listener), a hangforrás (source) és az adatpuffer (buffer). Az OpenAL háromdimenziós világában csak egyetlen hallgató létezik, minden hangforrás az ő perspektívájából kerül megszólaltatásra. A hangforrásokhoz adatpuffereket köthetünk, melyekbe a számítógépünkön elhelyezett hangfájlokat tölthetjük be.
Elsőként az OpenAL Utility Toolkit (továbbiakban ALUT) kerül beépítésre, amit a GLUT-nak lehet megfeleltetni: hasznos függvények gyűjteménye. Fontos megjegyezni, hogy az ALUT implicit beépíti az „al.h”-t is, így ha egy programban nem használunk ALUT-beli hívásokat, akkor ezt explicit is be kell építenünk, különben fordítási hibát kapunk. Az elnevezési konvenciók teljes mértékben az OpenGL-t követik, így az előző fejezetben leírtak érvényesek itt is. Az OpenAL-nek is saját típusai vannak, példánkban egy adatpuffer és egy hangforrás azonosítót, majd a forrás és a hallgató térbeli helyzetét leíró tömböket deklaráltuk. A hallgatóhoz két tömb is tartozik, míg a hangforráshoz csak egy. Ennek oka, hogy a hangforrások a modellben pontszerűek, a hang megszólaltatását a forrás orientációja nem befolyásolja. A hallgatónál viszont fontos az irány, mivel ahhoz, hogy a hangokat térben halljuk, két érzékelőre van szükségünk. Ezek egymáshoz képesti viszonyát pontosan meg kell adnunk ahhoz, hogy a hangforrások felől érkező hangokat a megfelelő irányból, pontosabban az iránynak megfelelő hangszóróból hallhassuk. A hallgató orientációját egy hatelemű tömbbel írjuk le, melynek első három eleme határozza meg, hogy milyen irányba nézünk (lookat), a második három elem pedig a felfelé irányt jelöli ki (up). Az OpenGL virtuális kameráját is pontosan ugyanezen vektorok írják le. Az irányok mellett a másik tényező, ami a hangok előállítását befolyásolja, a távolság. Minél messzebb van a hallgatótól egy hangforrás, annál kisebb hangerőn szól. Felmerül a kérdés, hogy a távolságot milyen mértékegységben mérjük. A válasz meglepően egyszerű: teljesen mindegy, az OpenAL mértékegység-független. Mindegy, hogy métert, centimétert vagy bármi más hosszmértéket használunk, a hangerő meghatározásához csak a távolságok arányait veszi figyelembe. Ez a függetlenség biztosítja, hogy az OpenAL jól integrálható más grafikus alkalmazásokhoz, nem kell a mértékegységekben kompromisszumokat kötnünk, csupán egy erősítő tényezővel (AL_GAIN) beállítjuk, hogy mekkora legyen a távolságokhoz jól illeszkedő hangerő. Természetesen itt egyszerűbb alkalmazásokról van szó, ha a Doppler-effektusra vagy más, hangsebességet igénylő funkcióra van szükségünk, definiálhatjuk a hangsebesség értékét
(AL_SPEED_OF_SOUND),
és
onnantól
minden
mértékegységben fog értelmeződni, mint amilyen ezt megadtuk.
más
érték
is
abban
a
2.3. OpenAL
15
Az ALUT inicializálásához minden más előtt az alutInit eljárást kell meghívnunk, ami a main függvény paramétereit kapja. Az alutCreateBufferHelloWorld egy adatpuffert ad vissza, mely a „Hello World!” hangmintát tartalmazza. Ez a hangminta az érthető népszerűsége miatt kapott egy dedikált hívást, minden más hangot külső fájlból kell betöltenünk. Az általunk használt eljárások paraméterezése a következő sémára épül: elsőként megadjuk az azonosítóját annak az objektumnak, melynek valamelyik értékén változtatni szeretnénk, második helyen egy konstanssal kijelöljük a beállítandó tulajdonságot (pl. AL_POSITION), végül pedig az új értéket adjuk át. A hallgatóra vonatkozó hívásoknál az első paraméter elmarad, mivel hallgatóból csak egy lehet, nincs szükség azonosítóra. Az alGenSource hívással tudunk hangforrást létrehozni: első paraméter a létrehozandó források száma, második pedig ezek azonosítója. A létrehozás után hozzárendeljük a „Hello World!” hangmintát tároló adatpuffert és elindítjuk a lejátszást. A hangminta mindössze 1 másodperc hosszú, ebből a „Hello” kb. 0.4 másodperc, a „World!” pedig 0.6 másodperc. A térbeli hangzás demonstrálására a két szó között a hangforrást átpozicionáljuk a hallgató bal oldaláról a jobb oldalára, így futtatáskor is ezt fogjuk tapasztalni. Utolsó hívásként a alutExit ismét inicializálatlan állapotba helyezi az ALUT-ot.
3. fejezet
16
3. fejezet
Kamerák Ezen fejezet a valós és virtuális kamerák tulajdonságait ismerteti. Valós kamerák esetén elsősorban a hardveres korlátok kerülnek kiemelésre, hogy tudjuk, milyen környezetben kell a valósidejű működést elérni és milyen hibákat kell szoftveres úton korrigálni. A virtuális kameráknál
definiáljuk
a
perspektivikus
kameramodellt,
mellyel
a
háromdimenziós
koordinátákat leképezzük a kétdimenziós képernyősíkra. A fejezetet a valós kamerák paramétereinek becslésére használt algoritmus elméleti leírása zárja.
3.1. Valós kamerák A videókamerák egyik legkorábbi alkalmazása a televíziós adások (BBC) közvetítése volt az 1930-as években. Akkoriban még elektromechanikus megoldást alkalmaztak, később ezt felváltotta a katódsugárcsöves technológia, majd az 1980-as évektől napjainkig a legtöbb videókamera CCD vagy CMOS szenzort használ. A webkamerák általános értelmezésben a videókamerák azon csoportja, melyek képe elérhető weben keresztül. A legelső webkamerát 1991-ben helyezték üzembe a Cambridge-i Egyetem helyi hálózatán és az egyik szobában (Trojan Room) elhelyezett kávékancsóra volt irányítva. Célja az volt, hogy az épület különböző pontjain dolgozók elkerüljék a fölösleges sétákat, mikoris a kávézós helyiségbe érkezve üresen találják a kancsót. Azóta széles körben elterjedt és általános felhasználásának területe pl. a videóhívás az azonnali üzenetküldő (instant messaging) szoftverekben. Mivel a hálózati továbbítás miatt a webkamerák általában
3.1. Valós kamerák
17
gyengébb minőségű képet nyújtanak, a digitális kamerák alacsony felbontású csoportjára is a webkamera megnevezés terjedt el. Jelen dolgozatban a webkamera megnevezést az utóbbi értelmezésben használom. Egy átlagos webkamera maximális felbontása 640 x 480 pixel és másodpercenként 25 képkeretet tud produkálni. A két érték közül általában csak az egyiket tudjuk maximalizálni a másik rovására, tehát magas FPS-t csak alacsony felbontáson és nagy felbontást alacsony FPS mellett lehet elérni. A grafikus kártyák esetén nem kell ilyen szigorú kompromisszumot kötnünk, mivel egy néhány alakzatból álló színteret másodpercenként több százszor is ki tud rajzolni, így hardveres oldalon a működés szűk keresztmetszete a webkamera. Felépítését tekintve a képszenzor mellett a másik legfontosabb eleme a lencse. A lencsék általában nem tudnak tökéletes képet alkotni, a – nem gyártási pontatlanságból adódó, hanem természetszerűen fellépő – lencsehibák következtében bizonyos aberrációk lépnek fel (pl. tükröződés). Az OpenCV kameramodellje a torzítás (distortion) típusú hiba javítására ad lehetőséget, ezért ezt vizsgáljuk meg részletesebben. A torzítást leginkább akkor könnyű felismerni, ha a kép a szélekkel párhuzamos egyeneseket tartalmaz, mivel a torzítás során az egyenesek meggörbülnek. Ennek oka, hogy a lencsék nagyítása az optikai tengelytől való távolsággal változik. Kisebb fókusztávolság, tehát nagyobb látószög esetén a hiba nagyobb mértékben jelentkezik. A torzítás ezen típusát sugárirányú (radial) torzításnak nevezzük és az ideális képtől való eltérést legnagyobb mértékben ez a típus okozza. Attól függően, hogy a lencse külső részein kisebb vagy nagyobb a nagyítás, hordó- (barrel) vagy párnaszerű (pincushion) torzításról beszélünk. A jelenség mindkét esetben szimmetrikus.
3.1. Valós kamerák
18
3.1. ábra. Torzítások: eredeti kép, hordószerű torzítás, párnaszerű torzítás. A sugárirányú torzítást Taylor-polinomos közelítéssel modellezhetjük [6]. Az ideális vetítés x és y koordinátáiból felírhatjuk a kör egyenletét:
r 2t =x 2t y 2t
(3.1)
ahol rt a középponttól való távolság, vagyis a kör sugara. Az ideális vetítéstől való eltérést a kör sugarától függő torzítási függvénnyel modellezhetjük:
[]
[]
xt =L r t x y yt
(3.2)
ahol xt és yt a torzított vetítés koordinátái, a torzítás függvényét pedig Taylor-polinommal közelíthetjük: 2
3
L r t =1k 1 r t k 2 r t k 3 r t ...
(3.3)
ahol k1, k2 … pedig a torzítási együtthatók. Mivel a torzítást közelítő polinom harmadfokú tagjától kezdődően az együtthatók elhanyagolható értékűek k1 és k2-höz képest, az OpenCV csak a polinom első két tagját veszi figyelembe. A kamera kalibráció eljárás során ezen együtthatókat is meghatározzuk, majd ezek ismeretében a kamera képén a torzítást korrigáljuk. Mivel az OpenCV modellje tartalmazza, érdemes még megemlíteni az érintőirányú (tangential) torzítást is, ami a sugárirányú torzítás után a hiba második legmeghatározóbb tényezője. Ennek értéke azonban általában a sugárirányú hiba értékének mindössze 15%-a körül mozog, így a legtöbb esetben ezt a komponenst már nem veszik figyelembe.
3.2. Virtuális kamerák
19
3.2. Virtuális kamerák A perspektív kameramodell ismertetéséhez szükségünk lesz a középpontos vetítés geometriai apparátusára. Tekintsük a következő ábrát:
3.2. ábra. Háromdimenziós pont vetítése a képernyő síkjára. [7, 6. fejezet] A háromdimenziós pontokat nagybetű (X, Y, Z), a kétdimenziós vetületüket pedig kisbetű jelöli (x, y). A vetítés középpontját fókuszpontnak (C) nevezzük, a vetítési síktól való távolságát pedig fókusztávolságnak (focal length, f). A fókuszpontot a vetítési síkkal összekötő merőleges egyenes és a sík metszéspontja a döféspont (principal point, P). Az ismert koordinátákkal rendelkező háromdimenziós pontok síkra vetülő kétdimenziós koordinátáit szeretnénk megkapni. Ez egyszerű feladat, mivel a hasonló háromszögek oldalarányaiból könnyen kiszámíthatjuk az ismeretlen értékeket. Felírhatók tehát a következő összefüggések:
x= f X /Z
(3.4)
y= f Y / Z
(3.5)
Homogén koordinátákat bevezetve a vetítés mátrixos alakban is megadható:
f X f fY = 0 Z 0
0 f 0
0 0 0 0 1 0
X Y Z 1
(3.6)
3.2. Virtuális kamerák
20
Az eddigiekben azzal a feltételezéssel éltünk, hogy a döféspont a képernyő középpontjában van. Ez a gyakorlatban nem mindig áll fenn, így a fenti alakot ki kell bővítenünk a döféspont koordinátáival (px, py):
f X px Z f = f Y py Z 0 Z 0
0 f 0
px 0 py 0 1 0
X Y Z 1
(3.7)
Az így kapott mátrix elemeit nevezzük a kamera belső paramétereinek, magát a mátrixot pedig kamera mátrixnak. A fent megadott leképzések során a pontokat a kamera eszközkoordinátarendszerében adtuk meg, mely jobbsodrású, origója a fókuszpontban helyezkedik el és a Z tengely a vetítés síkja felé mutat. Az esetek többségében azonban pontjaink a világkoordinátarendszerben
adottak,
így
ezeket
át
kell
transzformálunk
a
kamera
koordinátarendszerébe. Ehhez szükségünk lesz a transzformációt leíró rotációs és transzlációs mátrixokra. Ez a két mátrix adja meg a kamera mindenkori helyzetét, elemeiket a kamera külső paramétereinek nevezzük. Ezen új elemeket is bevezetve a modellbe az alábbi összefüggés írható fel:
[]
X x Y y =[ K ] [ R∣t ] Z 1 1
[]
(3.8)
ahol K a kamera mátrix, R a rotációs mátrix, t a transzlációs vektor, „|” pedig a konkatenálás jele. Az itt ismertetett kameramodellen kívül még számos más modell is létezik, ezek áttekintésére a témakör alapművének számító [7, 6. fejezet] ajánlott.
3.3. Kamera illesztés
21
3.3. Kamera illesztés 3.3.1. Általános áttekintés A kamera illesztés a számítógépes látás egyik alapvető problémája. Számos algoritmus született már a feladat megoldására, melyekről ezen fejezet egy általános áttekintést ad, amit az OpenCV-ben implementált eljárás részletes ismertetése követ. A kalibráció során egy ismeretlen kamerával készített képsorozatból határozzuk meg az eszköz külső és belső paramétereit. Ezen paraméterek ismeretében lehetővé válik számítógépes grafikával létrehozott tárgyak rajzolása a valós világról készült képsorozatra. A folyamat során két kamerával dolgozunk: egy valóssal, amely a megfigyelt világról készít felvételt, és egy virtuálissal, amely szemszögéből a számítógépes modelljeinkre tekintünk. Az illesztés a két kamera paramétereire utal, mivel a virtuális kamera minden paramétere a fizikai kamera paraméterének értékét kell kövesse. Ezáltal ha a valós kamera elmozdul vagy elfordul, a virtuális kamera is követni fogja a mozgását, így a jelenetbe beillesztett tárgy együtt fog mozogni a környezetével. Első lépésben a kamera perspektivikus vetítését leíró belső paramétereit határozzuk meg, melyek állandónak tekinthetők8 és függetlenek az éppen látott képkockától. Ha a belső paramétereket ismerjük, a kamerát kalibráltnak nevezzük. A kalibrációs módszereket alapvetően két csoportba sorolhatjuk. Fotogramatikus módszerek. A fotogramatikus módszerek egy kalibrációs objektumot használnak, melynek a háromdimenziós tulajdonságai kellő pontossággal ismertek. Általában kettő vagy három, egymásra merőleges síkot használnak, melyekre jól követhető pontokból álló, szabályos mintát nyomtatnak. Az ilyen kalibrációs objektumok összeállítása és használata nehézkes, bár a módszerek elég pontos eredményeket adnak.
8 A belső paraméterek pl. gumioptika (zoom) használata esetén változhatnak.
3.3. Kamera illesztés
22
3.3. ábra. Tipikus kalibrációs objektum fotogramatikus módszerekhez. Autokalibrációs módszerek. Az autokalibrációs módszerek nem használnak semmiféle előzetes ismeretet (kalibrációs objektumot) a megfigyelt környezetről. Ezek a módszerek kevésbé pontos eredményt adnak, viszont meglehetősen robusztusak.
3.3.2. Zhang-féle kalibrációs algoritmus Az OpenCV-ben a Zhang-féle kalibrációs algoritmus került implementálásra, így az alábbiakban ezt a módszert ismertetem [8]. Előtte azonban a 3.2-es fejezetben ismertetett ideális kameramodellt ki kell bővítenünk néhány új elemmel, mivel a kalibrációs eljárás ennél általánosabb modellre is igaz. Az ideális kameramodellnél azzal a feltételezéssel éltünk, hogy egy pixel mindkét irányú kiterjedése azonos. Ez azonban CCD kamerák esetén nem mindig van így, ezért ezt a különbözőséget be kell vezetnünk a vetítési egyenletekbe:
x= f m x X /Z
(3.9)
y= f m y Y / Z
(3.10)
ahol mx és my a pixel egy képkoordinátára eső vízszintes és függőleges irányú kiterjedése. A kamera mátrix továbbá tartalmazhat még egy ”s” jelölésű ferdeséget jellemző paramétert is (skew), ez azonban normál körülmények közt nulla értékű. Nem normál körülmény lehet pl. egy „kép a képben” kompozíció, ahol a vízszintes és függőleges tengelyek nem derékszöget zárnak be egymással.
3.3. Kamera illesztés
23
Ezen új elemeket bevezetve a kamera mátrix a következő alakú lesz:
[ ]
s K= 0 0 0 ahol = f m x és =m f
y
px py 1
(3.11)
.
A kameramodell kibővítése után már rátérhetünk Zhang módszerének ismertetésére, mely leginkább a fent említett két csoport közé tehető, mivel egyetlen síkbeli kalibrációs mintát használ, ami lényegesen egyszerűbb, mint két vagy három merőleges síkból álló minta összeállítása, ugyanakkor mégsem autokalibrációs eljárás. Mivel síkbeli mintáról van szó és a világ-koordinátarendszert tetszőlegesen megválaszthatjuk, legyen a Z tengely merőleges a minta síkjára és minden pontra Z = 0. Ekkor felírhatjuk a következőt:
[]
X u X v =[ K ] [ r 1 r 2 r 3 t ] Y = [ K ] [ r 1 r 2 t ] Y 0 1 1 1
[]
[]
(3.12)
ahol r1, r2 és r3 rendre a rotációs mátrix oszlopvektorai, λ pedig egy tetszőleges skálázó tényező. Mivel minden háromdimenziós pontra Z = 0, így mindkét oldalon pontjaink kétdimenziósnak tekinthetők, ezért a leképezés egy homografikus leképezéssel írható le. Homografikus leképezésről akkor beszélünk, ha a perspektív leképezés síkból síkba képez.
3.3. ábra. Homografikus vetítés.
3.3. Kamera illesztés
24 m= H M
(3.13)
H =[ K ] [ r 1 r 2 t ]
(3.14)
Ha kellő számú, legalább négy pontpár koordinátáit ismerjük, H kiszámítható [7, 90. oldal]. A fenti leképezés azonban csak ideális esetben teljesül, a valóságban a megfigyelt adataink zajjal terheltek, amit nulla várható értékű, σ szórású Gauss-zajjal modellezhetünk. Ilyen esetekben, mikor paramétereket kell becsülnünk zajos mérési adatok alapján, a maximum likelihood eljárás alkalmazható [7, 102. oldal]. Azzal a feltételezéssel élve, hogy a pontokat terhelő zaj egymástól független, felírhatjuk az alábbi valószínűségi sűrűségfüggvényt:
f x =
1 e−d x , x / 2 2 2
2
(3.15)
ahol x a pont mért helyzete. Mivel több ismert pontpárunk is van, melyek függetlenek egymástól, a likelihood függvény a következő alakú:
f { x i }∣H =∏ i
1 e−d Hx , x / 2 2 2
i
i
2
(3.16)
Jelentését úgy értelmezhetjük, mint annak a valószínűsége, hogy ha H írja le az ideális leképezést, akkor a mért adatok halmaza { x i } . A logaritmus függvény monotonitása miatt a maximalizálás szempontjából ekvivalens a likelihood függvény helyett annak logaritmusát maximalizálni. Az ún. log-likelihood függvényre való áttérés oka nem elméleti, inkább gyakorlati nehézségek elkerülése végett célszerű használni. A likelihood függvény ugyanis még jó paraméterezés esetén is rendkívül kicsi, nulla közeli értéket vesz fel, melyeket független pontpárok esetén összeszorozva még kisebb értékeket kapunk. A gyakorlatban az ilyen kis értékekkel nem tudunk kellő pontossággal számolni, természetes alapú logaritmusra való áttérés után viszont már nem szorzásokat, hanem összeadásokat végzünk, így az értékek nem nullához tartva csökkennek, hanem nőnek.
3.3. Kamera illesztés
25
log f { x i }∣H =C−
1 2 d H x i , x i 2∑ 2 i
(3.17)
ahol C egy tetszőleges konstans. Ez az érték akkor maximális, mikor a
∑ d H xi , x i 2 i
minimális. Ezt a nemlineáris
minimalizálást a Levenberg-Marquardt algoritmussal tudjuk megoldani [11]. Innentől tehát H mátrix elemei ismertek. A továbbiakban H oszlopvektoraira vezessük be a következő jelölést:
H =[ h 1 h 2 h3 ]
(3.18)
A (3.11) miatt írhatjuk, hogy
[ h1
h 2 h 3]= [ K ] [ r 1 r 2 t ]
(3.19)
ahol λ egy tetszőleges skalár. Ezekre a skálázó szorzókra azért van szükség, mivel egy homografikus leképzés mátrixát tetszőleges skalárral megszorozva a leképezés nem változik. Ebből kifolyólag bár a H mátrix kilenc elemű, csak nyolc szabadságfokkal rendelkezik, ugyanis ha egy tetszőleges elemét lerögzítjük, a többi elemet minden esetben meg tudjuk úgy választani, hogy ugyanazt az eredeti leképezést valósítsa meg. Mivel H elemeit ismerjük, a továbbiakban az a feladatunk, hogy a H mátrixot szétbontsuk K és
[ r1
r 2 t ] szorzatára. Kihasználva, hogy r1 és r2 ortonormáltak (a rotációs mátrix
oszlopvektorai), felírhatjuk, hogy: T
−T
T
(3.20)
T
−T
T
(3.21)
T
−T
−1
(3.22)
h 1 K =r 1
h 2 K =r 2 T
r 1 r 2=h 1 K
K h 2=0
r T1 r 1=r T2 r 2=h T1 K −T K −1 h1=hT2 K −T K −1 h2 ahol a kitevőben a ”-T” az inverz transzponáltját jelöli.
(3.23)
3.3. Kamera illesztés
26
Vezessük be a következő jelölést:
[
L11 L12 L=K ⋅K = L 21 L22 L 31 L32 −T
−1
L13 L23 L33
]
(3.24)
Behelyettesítve K elemeit és a szorzást elvégezve kapjuk, hogy
[
1 2 s L= − 2 p y s− p x 2
−
s
p y s− p x
2
−s p y s− p x
2
2
s
1 2 s p y s− p x 2
−
2
2
2
2
−
2 p y s− p x
py
2
2
2
2
−
py
2
2
py
2
1
]
(3.25)
Vegyük észre, hogy a mátrix a főátlóra szimmetrikus, így a mátrix leírható az alábbi hatelemű vektorral:
l=[ L11 L 12 L22
L13 L23
L33 ]
(3.26)
H mátrix oszlopvektoraiban jelöljük az egyes elemeket a következőképp:
h i=[ hi1 hi2 h i3 ]
T
(3.27)
Ekkor felírhatjuk, hogy
h Ti L h j=vTij l
(3.28)
ahol
v ij =[ h i1 h j1 hi1 h j2 h i2 h j1 hi2 h j2 hi3 h j1hi1 h j3 h i3 h j2 hi2 h j3 h i3 h j3 ]
T
(3.29)
A bevezetett új elemekkel újraírhatjuk (3.19)-et és (3.20)-at:
[
v T12
v 11−v 22
T
]
l =0
(3.30)
3.3. Kamera illesztés
27
Ha n darab kép áll rendelkezésre a megfigyelt kalibrációs mintáról, a (3.27) egyenletet mindegyikre felírva összevont alakban:
V l=0
(3.31)
ahol V egy 2n x 6-os mátrix. Az l összesen hat ismeretlent tartalmaz, így n >= 3 esetén lesz az egyenletrendszernek megoldása. Ha n = 2, tehát csak két képünk van, tehetünk megkötéseket az ismeretlenekre, pl. ahogy már korábban említésre került, a ferdeség (s) paramétert vehetjük nullának. Ezzel az ismeretlenek számát l-ben ötre csökkentjük, a
[ 0 1 0 0 0 0 ] l =0
egyenlet hozzáadásával pedig az egyenletek számát ötre
emeljük. Ha csak egyetlen kép áll rendelkezésre (n = 1), akkor a kamera belső paraméterei közül csak kettőt tudunk meghatározni. Ilyen esetekben az s = 0 megkötésen túl pl. adottnak tekinthetjük a döféspont koordinátáit a kép középpontjával. (Az OpenCV implementációban ugyanakkor van egy alsó korlát a képek számára: legalább négy képet meg kell adnunk a kalibrációhoz.) Végül nincs más dolgunk, mint megoldani (3.28)-at. Az A x=0 alakú, homogén túlhatározott
egyenletrendszer
megoldása
az AT A legkisebb
sajátértékéhez
tartozó
sajátvektor. Ha l vektort kiszámoltuk, L mátrix minden elemét ismerjük és K értékei is meghatározhatók. Mivel L mátrix skálázható, a L= K −T K −1 egyenletből számolhatjuk ki K elemeit:
= / L 11
(3.32)
= L11 / L11 L12− L212
(3.33)
p x =s p y /−L13 2 /
(3.34)
p y = L12 L 13− L11 L 23/ L 11 L22− L212
(3.35)
s=−L12 2 /
(3.36)
= L33 −[ L213 p y L12 L13−L 11 L23 ]/ L11
(3.37)
3.3. Kamera illesztés
28
Ha pedig ismerjük K értékeit, a külső paraméterek is számolhatók (3.19) alapján: −1
ahol =1/∣∣K
−1
r1= K h1
(3.38)
r 2= K −1 h2
(3.39)
r 3=r 1 ×r 2
(3.40)
t= K −1 h3
(3.41)
−1
h1∣∣=1/∣∣K h 2∣∣ a normalizálás miatt.
Mivel ezen értékek zajjal terheltek, a meghatározott eredményt szintén egy maximum likelihood eljárással lehet tovább finomítani.
4. fejezet
29
4. fejezet
Implementáció Ezen fejezet a megvalósítás részleteit ismerteti. Természetesen a dolgozatban nincs mód az implementáció teljes áttekintésére, így csak a feladat szempontjából releváns részek kerülnek bemutatásra. A teljes forráskód megtalálható a DVD-mellékleten (B. függelék).
4.1. Bevezetés A
rendszer
prototípusát
C++
nyelven
implementáltam,
Microsoft
Visual
C++
fejlesztőkörnyezetben, Windows platformon. Az implementáció során az objektumorientált szemléletet követtem, és az egyes osztályokat különálló modulokban valósítottam meg úgy, hogy azok a lehető legkisebb, jól definiált felületen kapcsolódjanak egymáshoz. Ezáltal bármelyik modul könnyen cserélhető a többi változtatása nélkül, nagymértékben elősegítve a prototípus könnyebb továbbfejlesztését. Szintén nagy hangsúlyt fektettem a logikai modellezésre, így minden objektum pontosan meghatározott szerepkörrel rendelkezik.
4.2. Felépítés
30
4.2. Felépítés A program működésének áttekintéséhez tekintsük a 4.1-es ábrát:
4.1. ábra. A program folyamatábrája. Induláskor végrehajtódnak az inicializáló lépések: létrejönnek az OpenGLUT ablakai, lefutnak a példányosított osztályok konstruktorai, beolvasásra kerül a beállításokat tartalmazó konfigurációs fájl, kiíródik a konzolra a kezelési útmutató és végül elindul a kamera. Ezután belépünk a saját megvalósítású végtelen ciklusba, melynek első lépéseként lekérdezzük a kamera által látott képet. A cikluson belül a program egy véges állapotgépet valósít meg, melynek állapotait és állapotátmeneteit a 4.2-as ábra mutatja:
4.2. Felépítés
31
4.2. ábra. A program állapotdiagramja. Az állapotkezelés megvalósításának helye a tervezés során nem volt egyértelmű. Egyfelől helyes az a megközelítés, miszerint az állapotok számontartása és az átmenetek kezelése logikailag a főprogram feladata, esetünkben azonban nem ilyen egyszerű a helyzet, ugyanis az egyes állapotok pontosan megfelelnek a kamera kalibráltsági állapotának. Egy kamera objektummal kapcsolatban pedig jogos az elvárás, hogy mindig tudja a kalibráltságának állapotát, így a kamera osztályhoz is be kell vezetnünk ezt a állapotváltozót. Egyazon változó értékét ugyanakkor nem célszerű két különböző helyen is vezetni, mivel inkonzisztenciához vezethet, ezért úgy döntöttem, hogy a kamera osztályában implementálom az állapotváltozó kezelését. Az egyes állapotok között a felhasználó billentyűleütéssel válthat, melyeket a 4.3as ábra jelöl, illetve kalibrálás esetén az állapotátmenetet az is kiváltja, ha a konfigurációs fájlban megadott számú kép összegyűlt a belső paraméterek számításához. A folyamatábrán továbbhaladva eljutunk az állapotváltozótól függő elágazáshoz. Ha a kamera még nincs kalibrálva, nem tudjuk a virtuális tárgyakat mi alapján kirajzolni, így a végtelen ciklus üresen iterál egyet a következő képhez. A másik lehetőség, hogy bár még nincs kalibrálva, éppen most végezzük a kalibrációt. Ebben az esetben fel kell dolgozni az aktuális képet, ami a sakktábla-minta megkeresésével kezdődik. Ha a minta nem található, az adott képet nem tudjuk felhasználni a kalibrációhoz és érdemi művelet nélkül iterálunk. Ha azonban megtaláltuk, elmentjük a sarokpontok képkoordinátáit, melyek a hozzájuk tartozó minta háromdimenziós pontjaival összepárosítva adják a kalibrációs eljárás bemenetét. A konfigurációs fájlban adhatjuk meg, hogy hány képet készítsen a program; amint összegyűlt az előírt mennyiség, lefut a kalibráció. Mivel ezután már ismerjük a belső paramétereket, átlépünk a kalibrált állapotba és a külső paraméterek meghatározása utána ki tudjuk rajzolni a virtuális tárgyakat. A következő iterációban már az állapottól függő elágazás után az ”igen” ág felé megyünk tovább, majd megint megkeressük a képen a sakktábla-mintát. Ha megtaláltuk, újraszámoljuk az esetlegesen megváltozott külső paramétereket, majd újból
4.2. Felépítés
32
kirajzoljuk a számítógépes modelleket. Ha a sakktáblát nem találjuk, de már korábban ismertük a belső sarokpontjainak helyét, követjük a mozgását. Mivel a belső paraméterek értékét fájlból is beolvashatjuk, lehetséges, hogy úgy kerülünk kalibrált állapotba, hogy még egyszer sem találtuk meg a mintát. Ilyenkor nem tudunk mit követni, ezért szükséges ennek vizsgálata. Összesen hét osztály került implementálásra, melyek kapcsolatát a 4.3-ás ábrán látható UML diagram mutatja:
4.3. ábra. A program UML diagramja. Az egyes osztályok lényegesebb funkcióit az alábbi fejezetek ismertetik.
4.3. Inicializálás
33
4.3. Inicializálás Az inicializálás során a változók felveszik kezdeti értéküket, melyeket egy konfigurációs fájlban adhatunk meg (config.txt), ezzel lehetővé téve, hogy a program egyes funkciói újrafordítás nélkül is kipróbálhatóak legyenek. Minden sor egy változónévből és egy értékből áll, melyeket egy szóköz választ el egymástól. Az első sor, az input határozza meg, hogy milyen forrásból származnak a képek: 0 jelenti a webkamerát, 1 a videófájlt (videofile.avi), 2 pedig a szintetikus képeket. A következő néhány sorban az ablakok helyzetét lehet szabályozni, mivel kisebb méretű monitorokon az alapbeállítások mellett kilóghatnak az egymás mellé rendezett ablakok a képernyő területéről. Az image_count azt határozza meg, hogy a kalibrációhoz hány mintaképet készítsen a program. Az egyes képek között eltelt időt a delay állítja be. Az utolsó sorok a takaráshoz kapcsolódnak: egy valós, téglatest alakú tárgy helyzetét és méreteit adják meg; alapbeállítások mellett a videófájlokon látható tárgy adatait tartalmazza. Fontos megjegyezni, hogy az egyszerűség kedvéért a valós tárgyak modelljeinek minden oldala párhuzamos kell, hogy legyen az egyes tengelyekkel.
4.3.1. Bemenetek A rendszert elsősorban valósidejű működésre terveztem, így az elsődleges bemeneti forrás egy kamera, tipikusan egy USB-s webkamera élőképe. A kamera osztály ezenkívül fel van készítve videófájlok feldolgozására és mesterségesen képek előállítására is, melyek megkönnyítik a tesztelést, egyúttal pedig a webkamerák hardveres korlátait meghaladó környezetben is lehetőség nyílik a teljesítmény mérésére. A kamera osztály elfedi az egyes bemeneti források különbözőségeit, így a többi modulnak nincs tudomása arról, hogy éppen milyen forrásból származó képet dolgoz fel. A 3.1-es fejezetben, a webkamerák hardveres korlátainál említésre került, hogy a felbontás nagymértékben befolyásolja a elérhető maximális FPS-t. A kamerákat az OpenCV highgui moduljának hívásain keresztül érjük el, amik alapesetben a 320 x 240 pixeles felbontásban indítják el a webkamerákat. A rossz hír az, hogy sajnos ennél nagyobb felbontásra nem is tudunk váltani, mivel az ehhez szükséges eljárások egyelőre nem kerültek implementálásra
4.3. Inicializálás
34
az OpenCV-ben. A kapcsolódó eljárások ugyan hívhatók, a felületük (interface) már elkészült, de nincs semmiféle hatásuk. A jó hír pedig az, hogy mivel az OpenCV nyílt forráskódú és szabad szoftver, tetszőlegesen megváltoztathatjuk az eljárásokat vagy akár sajátokat is implementálhatunk. A Yahoo Groups OpenCV csoportjának tagja, Tim Hutt elkészítette a kamerák felbontását beállító eljárást, így az A függelékben ismertetett módon újrafordítottam a highgui modult, hogy a webkamerák felbontása beállítható legyen.
4.3.2. Megjelenítés A megjelenítéshez két ablakot használtam. A bal oldali ablakban a képfeldolgozással kapcsolatos információk jelennek meg, jobb oldalon pedig az illesztett kép. Az ismertetett könyvtárakat használva két ablakkezelő közül is választhatunk: az egyik az OpenCV highgui moduljának része, a másik pedig az OpenGLUT. Az OpenGLUT-ra mindenképp szükségünk van, mivel az OpenGL a kettő közül csak ilyen típusú ablakba tud rajzolni. A kérdés tehát az, hogy a képkezelő ablakhoz melyik ablakkezelőt használjuk. A highgui mellett az szól, hogy képkezelésre van optimalizálva, és mivel ebbe az ablakba az OpenCV eszközeivel előállított és feldolgozott képeket fogunk kirajzolni, ideális és kézenfekvő eszköz az eredmények megjelenítésére. Ellenérvként az eseménykezelését lehet említeni, ami blokkoló híváson keresztül valósul meg, így a valósidejű alkalmazások sebességét csökkentheti. Szintén az eseménykezeléshez kapcsolódik, hogy mivel a másik ablakhoz mindenképpen az OpenGLUTot kell használnunk, minden egyes billentyűhöz két különböző módon is el kell készítenünk ugyanazon lekezelő részeket, hogy bármelyik ablak is van a fókuszban, a felhasználói felület egységes legyen. Ezen okokból kifolyólag mindkét ablak megvalósításához az OpenGLUT mellett döntöttem. Mivel mindkét ablak a beolvasott forrás képét jeleníti meg, azonos felbontásban kell megnyitnunk őket, a fent ismertetett okokból kifolyólag pedig a regisztrált eseménykezelő eljárások is megegyeznek. Különbség csak a pozícióban és a regisztrált rajzoló eljárásban van. A két ablak között a glutSetWindow hívással tudunk váltani, az ablak-specifikus utasítások (pl. egy transzformációs mátrix beállítása) mindig az aktuális ablakra érvényesek.
4.4. Kalibrálás Ebben a fejezetben a folyamatábra baloldali ágát tekintjük át. A 3.3-as fejezetben leírtak szerint a kalibrációhoz szükségünk van egy olyan tárgyra, melynek háromdimenziós tulajdonságait
ismerjük.
A
Zhang-féle
módszer
síkbeli
tárgyakat
használ,
tehát
tulajdonképpen egy alkalmas kétdimenziós mintát keresnünk, melynek pontjai jól felismerhetők. A sakktábla-minta ideális erre a célra, mivel belső sarokpontjai jól követhetők, és az egyes pontok egymáshoz képesti helyzetét is könnyen meg tudjuk határozni. Ez a minta programozástechnikailag is jó választás, mivel pontjainak koordinátáit egyszerűen és automatikusan tudjuk generálni a pontok szabályos elrendezése miatt. A minta megkeresése a ChessBoard osztály findIt nevű metódusában került implementálásra. Maga a ChessBoard osztály egy sakktáblát reprezentál, melyet a világot jelentő World osztály példányosít. A tárolásra két adattagot tartalmaz: a pontok háromdimenziós helyzetét a Points3D, ezek kétdimenziós vetületeit pedig a Points2D tömb tárolja. Az OpenCV a sakktábla-minták
felismerésére
tartalmaz
egy
beépített
eljárást
(cvFindChessboardCorners), melynek a Points2D tömböt és a kamera által látott képet átadva megkapjuk a belső sarokpontok koordinátáit, ha a képen látható a sakktábla. Ez az eljárás azonban túlságosan nagy szórással ismeri fel a sarokpontokat, tehát önmagában nem használható, viszont egy nagyon jó kezdeti becslést ad, amit tovább pontosíthatunk. A pontosítás azon a megfigyelésen alapul, miszerint egy sarokpont közvetlen szomszédaiba mutató vektor merőleges az adott szomszédos pontra számolt gradienssel
4.4. Kalibrálás
36
vektorra9.
4.4. ábra. A sarokpontok környezetének gradiens vektorai. Ha a sakktábla belső sarokpontjára adott becslés valamekkora környezetében minden pontra és összes szomszédjára kiszámítjuk és összegezzük a két vektor szorzatát, ott kapjuk meg az pontos koordinátát, ahol a legkisebb ez az érték. A szorzásnál skaláris szorzást végzünk, ami merőleges vektorok esetén nulla eredményt ad, ezért keressük a minimális értéket. Ezt az eljárást valósítja meg az OpenCV cvFindCornerSubPix hívása, melynek paraméterül adjuk át a vizsgált képet, a sarokpontok helyének kezdeti becslését, a keresési környezet méretét és – mivel a megvalósítás iterációs alapú – egy leállási feltételt. Kimenetként a kezdeti becsléseket tartalmazó tömbben kapjuk vissza a pontosított értékeket. Mivel az eljárás az egyes pixelek RGB komponenseit külön vizsgálja, a sakktábla viszont fekete-fehér mintájú, érdemes szürkeárnyalatossá konvertálnunk a képet az eljárás futtatása előtt. found = cvFindChessboardCorners( frame, cvSize( width, height ), Points2D_, &count, 0 ); if (found) {
Ne tévessze meg az Olvasót, hogy a Points2D helyett a Points2D_ tömb szerepel az eljárásokban, ennek a pontkövetésnél lesz jelentősége, ami a 4.5-ös fejezetben kerül ismertetésre.
9 Egy képpontra számolt gradiens vektor mindig abba a szomszédos képpontba mutat, amely irányban a legnagyobb az intenzitáskülönbség.
4.4. Kalibrálás
37
Ha megtaláltuk a sakktábla összes sarokpontját, a findIt metódus igaz értékkel tér vissza. Ekkor a kétdimenziós koordinátákat elmentjük, lekérdezzük az aktuális időt, hogy a következő képig az új pozíció beállítására a konfigurációs fájlban megadott időtartam elteljen, és a képet invertáljuk jelezvén a mintavétel sikerességét. cvSeqPush( image_points_seq, myWorld.getChessBoard()->getPoints2D() ); prev_timestamp = clock(); cvNot( myRealCamera.getFrame(), myRealCamera.getFrame() );
Ha elértük a kellő számú képet – amit szintén a konfigurációs fájlban adtunk meg –, a RealCamera osztály calibrateIt metódusa hívódik meg. A kalibrációhoz a 3.3-as fejezetben leírtak szerint háromdimenziós koordinátákból és azok kétdimenziós vetületükből álló pontpárokra van szükségünk. A kétdimenziós vetületeket már ismerjük, a hozzájuk tartozó térbeli pontok koordinátáit tároló Points3D tömböt pedig a ChessBoard osztály konstruktorában töltjük fel, melyekre Z = 0, az X és Y értékeket pedig automatikusan generáljuk a szabályos elrendezésnek megfelelően. for ( int j = 0; j < height; j++ ) { for ( int k = 0; k < width; k++ ) { Points3D[ j * width + k ].x = k * square_size; Points3D[ j * width + k ].y = j * square_size; Points3D[ j * width + k ].z = 0; }
A
belső
}
paramétereket
tartalmazó
kamera
mátrixot
a
RealCamera
osztály
intrinsic_parameters adattagja tárolja, a külső paramétereket pedig a rotation és a translation vektorok. A 3.1-es fejezetben ismertetésre került a valós kamerák torzítása, melynek modellezésére Taylor-polinomot használunk. A torzítási együtthatókat szintén a kalibráció során határozzuk meg, melyek értékét a – sugár- és érintőirányú torzítás első kétkét együtthatójának megfelelően – négyelemű distortion_coefficients vektor tartalmazza. Az OpenCV minden, a kalibrációhoz felhasznált képhez kiszámolja a hozzá tartozó külső paramétereket, így létre kell hoznunk egy, a képek számával megegyező sorból álló háromelemű rotációs és transzlációs mátrixot. Mivel az utolsó képre már rajzolhatunk, ezen mátrix utolsó sorát beállítjuk aktuális külső paramétereknek. A kalibráció során több ésszerű feltételezéssel is élünk. Ezek nagymértékben javítják a számolás pontosságát azáltal, hogy kevesebb paraméter értékét kell becsülni, mivel az eljárás során meghatározandó
4.4. Kalibrálás
38
szabad paraméterek közül néhány értékét pontosan megadjuk: a döféspont koordinátáit a kép középpontjával adottnak tekintjük (CV_CALIB_FIX_PRINCIPAL_POINT), a pixelek kétirányú
kiterjedését
azonosan
egységnyinek
tételezzük
fel
(CV_CALIB_FIX_ASPECT_RATIO) és csak a sugárirányú torzítást kezeljük, az érintőirányú torzítást
4.5. Virtuális tárgyak kirajzolása A virtuális tárgyak kirajzolásához a 2.2-es fejezetben ismertetett OpenGL könyvtárat használtam. Ahhoz, hogy a kirajzolt modell illeszkedjen a valós környezetbe, minden kirajzolás előtt meg kell adnunk azt a transzformációt, ami ennek megfelelően képezi le a modell pontjait. A kamera által megvalósított leképzést a belső paraméterek határozzák meg, ezt állandónak tekinthetjük. A külső paraméterek azonban minden képkeretben változhatnak, ezért folyamatosan újra kell számolnunk az értékeit. Ezt a funkciót a RealCamera osztály findExtrinsicParameters metódusa valósítja meg, mely a hasonló nevű, külső paramétereket kiszámító OpenCV eljárást hívja. Ne tévessze meg az Olvasót az intrinsic_parameters
és
a
distortion_coefficients
az
argumentumok
listájában, ebben az esetben bemeneti változóként szerepelnek. cvFindExtrinsicCameraParams2( Points3D, Points2D, intrinsic_parameters, distortion_coefficients, rotation, translation);
Miután mind a belső, mind a külső paramétereket ismerjük, beállíthatjuk az OpenGL-t, hogy a valós környezetnek megfelelő leképzést hajtsa végre.
4.5. Virtuális tárgyak kirajzolása
39
4.5.1. Nézeti csővezeték A modern számítógépes grafikai rendszerek – beleértve az OpenGL-t is – az inkrementális képszintézis elvét alkalmazzák [10, 7. fejezet], miszerint az egyes feladatokat (pl. egy pixel színének meghatározását) nem egymástól függetlenül oldják meg, hanem minden lépésben felhasználják az előző lépésben kiszámolt értékeket. Ezáltal jelentős sebességnövekedés érhető el, mivel azon értékek, melyekre két egymást követő lépésben is szükség van, csak egyszer kerülnek kiszámításra. Egy másik fontos elve az inkrementális képszintézisnek, hogy minden feladatot (pl. takarás) az arra legalkalmasabb koordinátarendszerben végez el. Emiatt a virtuális tárgyaink egy koordinátarendszerekből álló soron, az ún. nézeti csővezetéken mennek keresztül. Az egyes koordinátarendszerek között homogén lineáris transzformációkkal tudunk váltani. A kamera illesztés során az OpenCV segítségével meghatározott értékekkel kell felparamétereznünk az OpenGL virtuális kameráját. Ez a nézeti csővezeték két transzformációs mátrixa, a modell-nézeti és a perspektív mátrixok megadását jelenti.
4.5.1.1. Modell-nézeti transzformáció A modell-nézeti transzformáció során a világ-koordinátarendszerből (4.5. ábra) áttérünk a kamera-koordinátarendszerbe, melynek leképzési mátrixát a fizikai kamera rotációs és transzlációs mátrixai adják az OpenCV-ben végzett kalibráció eredményeként.
4.5. Virtuális tárgyak kirajzolása
40
4.5. ábra. A világ-koordinátarendszer. A rotációs mátrixot tömör, vektoros alakban kapjuk meg (rotation), melynek iránya a forgatás tengelyét jelöli ki, hossza pedig a forgatás szögét adja meg. Az OpenGL azonban 4 x 4-es mátrixokkal dolgozik, melyből a bal felső 3 x 3-as almátrix valósítja meg a forgatást. Így mielőtt továbbmennék, a tömör alakot át kell alakítanunk a Rodrigues-képlet alkalmazásával (rotation_) [9, 1.2 fejezet]:
[ R t , ]=C I 1−C [ t⋅t ]S [ t× ]
(4.1)
ahol t a forgatás tengelye, Φ a forgatás szöge, S és C a szinusz és koszinusz függvények, I az egységmátrix,
[ t× ] és [ t⋅t ] pedig a t vektor önmagával vett vektor-, ill. diadikus
szorzásának mátrixai:
[
0 −t z t y [t × ]= t z 0 −t x −t y t x 0
]
(4.2)
4.5. Virtuális tárgyak kirajzolása
41
[
tx tx tx ty tx tz [ t⋅t ]= t y t x t y t y t y t z t z tx tz t y t z tz
]
(4.3)
Az így kapott mátrixot a transzlációs mátrix elemeivel kiegészítve megkapjuk azt a transzformációs mátrixot, mellyel a világ-koordinátarendszerben adott pontokat a kamerakoordinátarendszerbe vihetjük át. Ezt azonban még nem tudjuk közvetlenül átadni az OpenGL-nek, mivel az OpenCV balsodrású koordinátarendszert használ, az OpenGL ezzel szemben jobbsodrásút. A kettő közötti egyetlen különbség a Z tengely iránya. A váltás ezért viszonylag egyszerű: a transzformációs mátrixot meg kell szoroznunk balról az alábbi mátrixszal:
[
1 T −Z = 0 0 0
0 0 0 1 0 0 0 −1 0 0 0 1
]
(4.4)
Mivel a mátrix diagonális, elkerülhetjük a mátrixszorzással járó bonyodalmakat, ha ügyesen kihasználjuk az OpenGL skálázási funkcióját (glScale). A skálázás során ugyanis pontosan egy diagonális mátrixszal való szorzás hajtódik végre, melyben a három tengelynek megfelelő skálázó szorzók szerepelnek a főátlóban. Ha tehát a fenti mátrix főátlóbeli elemeit adjuk paraméterül a skálázó eljárásnak, pontosan a sodrásváltást fogja megvalósítani. Ezzel már majdnem készen is vagyunk, az egyetlen átalakítás, amit még eszközölnünk kell, egy transzponálás az alább ismertetett okok miatt. Egy pont koordinátáit kétféle reprezentációban is megadhatjuk attól függően, hogy sor- vagy oszlopvektoros formában szeretnénk felírni őket. A kétféle alak teljesen egyenértékű, a vektorok egymás transzponáltjai, azonban a transzformációs mátrixokat is a választott alakhoz kell igazítanunk. Ha oszlopvektorosan írjuk fel pontjainkat, a transzformációs mátrixokkal balról kell szoroznunk,
míg
sorvektoros
esetben
jobbról,
mégpedig
a
leképezés
mátrixának
transzponáltjával. Mind az OpenGL, mind pedig az OpenCV oszlopvektoros alakban kezeli a pontokat, így nem emiatt szükséges a transzponálás. Az átalakításra a C/C++ mátrixtárolási mechanizmusa miatt van szükség, ugyanis a C/C++ sorfolytonosan tárolja a mátrixokat, nem pedig oszlopfolytonosan. Az OpenCV-től kapott oszlopvektoros alakhoz igazodó mátrixot a C+ + sorfolytonosan fogja tárolni és átadni az OpenGL-nek, ami
egy transzponálásnak felel
4.5. Virtuális tárgyak kirajzolása
42
meg, tehát ha az eredeti mátrixot akarjuk átadni az OpenGL-nek, nekünk is transzponálnunk kell. Ezen átalakítások után már használhatjuk a saját transzformációs mátrixunkat. double modelview[ 16 ]; CvMat* rotation_ = cvCreateMat( 3, 3, CV_64F ); cvRodrigues2( rotation, rotation_ ); for ( int i = 0; i < 3; i++ ) { for ( int j = 0; j < 3; j++ ) { modelview[ j * 4 + i ] = cvmGet( rotation_, i, j );
4.5.1.2. Perspektív transzformáció A perspektív transzformációnál már egyszerűbb dolgunk van. Az OpenGL projekciós mátrixa esetünkben egy normalizálást és egy perspektív vetítést megvalósító leképzés mátrixának szorzata, mely a következő alakú [10, 7.3 fejezet]:
[T proj ]=
[
1/tg
fov aspect 2
0
0
1 /tg
0 0
0 0
fov 2
0
0
0
0
− f pb p /b p− f p −1 −2 f p b p /b p− f p 1
]
(4.5)
ahol fov a függőleges látószög, aspect az ablak szélességének és magasságának aránya, fp az elülső, bp pedig a hátsó vágósík távolsága.
4.5. Virtuális tárgyak kirajzolása
43
Az OpenCV kamera mátrixából a látószög helyett a fókusztávolságot kapjuk meg, az átszámítás egy egyszerű szögfüggvény (3.2-es ábra):
1/tg
fov f =2 2 height
(4.6)
ahol height az ablak magassága. Az elülső és hátsó vágósík távolságát önkényesen megválaszthatjuk10, ezeknek 0.1 és 1000 értékeket választottam. Az OpenGL projekciós mátrixának így már minden elemét ismerjük és behelyettesíthetjük. double projection[ 16 ]; double nearPlane = 0.1; double farPlane = 1000; projection[ projection[ projection[ projection[
4.5.2. Árnyékok A számítógépes grafikával előállított tárgyak egymáshoz illesztésénél alapvető szerepe van az árnyékoknak, mivel általuk kapunk információt két tárgy egymáshoz képesti távolságáról. Egy pattogó labdánál például nehezen tudnánk megállapítani, hogy pontosan mikor éri el a talajt, ha az árnyéka nem adna folyamatos visszajelzést a magasságáról. A virtuális tárgyak illesztése a kamera által látott képre lehet akármilyen pontos, árnyékok nélkül nem fogja a tökéletes illeszkedés hatását kelteni. Ezen okokból kifolyólag, és mivel az OpenGL nem 10 Bizonyos megkötések mellett, pl. az elülső vágósík értéke nem lehet nulla.
4.5. Virtuális tárgyak kirajzolása
44
tartalmaz direkt támogatást az árnyékok rajzolásához, egy saját árnyékvetítő eljárásra lesz szükségünk. Az árnyékok a fény útjába kerülő tárgyak által takart térrészekben alakulnak ki, alakjukat tehát a fény terjedési iránya és az árnyékot vető tárgy alakja határozza meg. A prototípus egyetlen virtuális tárgyat jelenít meg a sakktábla síkjára illesztve, így az árnyékszámítás egyik legegyszerűbb esetével van dolgunk, mikor az árnyékot sík felületre kell vetítenünk. Mivel vetítésről van szó, a 3.2-es fejezetben foglaltak szerint egy mátrixszal írhatjuk le [10, 7.18. fejezet]. A sík egyenlete normálvektoros alakban:
ahol n=[ N X
NY
n p− p0 =0
(4.7)
N x P x N y P y N z P z D=0
(4.8)
N Z ] a sík normálvektorja és D=−n p0 .
[
− L x N x −L y N x −L z a −N x −L x N y − L y N y −L z N y −N y T shadow = −L x N z −L y N z − Lz N z −N z −L x d −L y D −L z D − D ahol l=[ L X
LY
]
(4.9)
L Z ] a fényforrás helyvektora és =n l− p 0 , tehát a fényforrás és sík
közötti távolság (dot product). Ez így kapott mátrix azt a transzformációt írja le, amellyel megszorozva egy tárgy háromdimenziós pontjait, annak síkra vetült képét kapjuk meg. Ha ezt a képet sötét színnel jelenítjük meg, akkor pontosan a test által vetett árnyékot rajzoljuk ki. double d = -normalX * pointX - normalY * pointY - normalZ * pointZ; double dot = normalX * lightX + normalY * lightY + normalZ * lightZ + d * lightW; shadowMatrix[0] shadowMatrix[4] shadowMatrix[8] shadowMatrix[12] shadowMatrix[1] shadowMatrix[5] shadowMatrix[9] shadowMatrix[13]
A fent ismertetett síkra vetülő árnyékszámítást kicsit finomítanunk kell még, hogy a kívánt eredményt kapjuk. Mivel tárgyaink térbeli kiterjedéssel rendelkeznek, egyazon síkbeli árnyékpont több sokszöghöz is tartozhat. Képzeljünk el egy gömböt, pontosabban annak egy háromszögekkel történő közelítését. Ha egy ilyen gömbnek vetítenénk az árnyékát, egy vetítőegyenes pontosan kétszer metszené a gömb felületét egy általános ponton keresztül, a szélein viszont csak egyszer. Ezáltal, mivel a vetítőegyenesek síkkal való metszéspontjában a sík eredeti színét sötétebbre színezzük, a gömb széleihez tartozó pontokon csak egyszer sötétítünk, míg minden más pontot kétszer, így a szélek világosabbak lesznek. Gömb esetén ez
még
kevésbé
szembetűnő,
de
bonyolultabb
alakzatoknál
jelentős
különbségek
alakulhatnak ki. A másik probléma, amire megoldást kell keresnünk, a lelógó árnyékok problémája. A sakktábla-minta síkját egy téglalappal modellezhetjük, melynek kiterjedése véges. Bár a téglalap méreteit gyakorlatilag tetszőleges nagyságúra is megválaszthatjuk, a virtuális modelleket általában egy kisebb kiterjedésű asztallapra helyezett sakktáblára illesztjük. Ilyen esetekben az asztallap széleihez közeledve az árnyékok a téglalapon túlra vetülnek. Ezen problémák megoldására a stencil puffert tudjuk használni. A stencil puffer egy, a képernyő méretével megegyező nagyságú tár, mely az egyes pixelekről tárol információt. Arra használhatjuk, hogy az egyes pixelekről eldöntsük, hogy színének változtatását engedélyezzük-e. Ha bekapcsoljuk (glEnable( GL_STENCIL_TEST )), minden rajzolás előtt végrehajtódik a stencil-teszt, ami egy általunk megadott referencia érték és a pufferben tárolt érték összehasonlítása. Az összehasonlítási relációt a glStencilFunc hívással állíthatjuk be, mely három paramétert vár: első helyen az összehasonlítási reláció szerepel (pl. GL_EQUAL), második helyen a referencia érték, harmadik paraméterben pedig egy bitmaszkot definiálhatunk11. A teszt eredménye lehet igaz, ilyenkor a rajzolás folytatódik a takarási vizsgálattal (z-puffer), és ha a kirajzolandó sokszög az éppen vizsgált pixel helyén 11 Az árnyékszámításhoz használt funkciókhoz nincs szükség maszkolásra, ezért ez a paraméter minden esetben a ~0 lesz, tehát minden bitet átengedünk.
4.5. Virtuális tárgyak kirajzolása
46
nincs takarásban, akkor megváltoztatjuk a színét. Ha a stencil-tesz hamis eredményt, a rajzolási folyamat nem megy tovább és a pixel színe változatlan marad. A harmadik lehetőség az, hogy a stencil-teszt továbbengedi a rajzolást, viszont a sokszög az adott pixelben takarásban van, így ezen a ponton áll meg a rajzolás. A glStencilFunc hívással mindhárom esetre külön megadhatjuk, hogy mi történjen a stencil pufferben tárolt értékkel (pl. GL_ZERO). Ez az új érték kerül majd összehasonlításra a következő rajzolásnál a referencia értékkel. A stencil puffert az árnyékok esetében arra használhatjuk, hogy letiltsuk azon pixelek sötétítését, melyek a téglalapon túlra nyúlnak. A többszörös sötétítést is el tudjuk kerülni, ha az egyszer már besötétített pontokat kizárjuk a további változtatásokból. glClearStencil(0); glClear(GL_STENCIL_BUFFER_BIT); glEnable(GL_STENCIL_TEST); glStencilFunc(GL_ALWAYS, 1, ~0); glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE); glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); glBegin(GL_QUADS); glNormal3f(0.0, -1.0f, 0.0f); glVertex3f(-10.0, -10.0, 0); glVertex3f(10.0, -10.0, 0); glVertex3f(10.0, 10.0, 0); glVertex3f(-10., 10.0, 0); glEnd(); glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); glDisable(GL_LIGHTING); glDisable(GL_DEPTH_TEST); glEnable(GL_BLEND); glBlendFunc(GL_ZERO, GL_DST_COLOR); glStencilFunc(GL_EQUAL, 1, ~0); glStencilOp(GL_KEEP, GL_KEEP, GL_ZERO); glPushMatrix(); glMultMatrixf(shadowMatrix); myCar->drawIt(); glPopMatrix(); glEnable(GL_DEPTH_TEST); glEnable(GL_LIGHTING); glDisable(GL_STENCIL_TEST); glDisable(GL_BLEND);
Az eredmény a 4.6-os ábrán látható.
4.5. Virtuális tárgyak kirajzolása
47
4.6. ábra. Síkra vetülő árnyék.12
4.5.3. Takarás A virtuális tárgyak kirajzolásakor a valós kamera kamera által látott képre rajzoljuk modelljeinket. OpenGL-ben ezt úgy tudjuk elérni, hogy a kamera képét egy folyamatosan frissített textúraként kezeljük, majd a 2.2.2-es fejezetben ismertetett módon kétdimenziós ortografikus vetítéssel jelenítjük meg. Ahhoz, hogy a képre illesztett, térben elhelyezett modelleket is láthassuk, a takarást ki kell kapcsolnunk glDisable( GL_DEPTH_TEST ). Ha a takarás ki van kapcsolva, a virtuális kamera elé közvetlenül kifeszített kép mélységi értékei (Z = 0) nem kerülnek be a z-pufferbe, így bármit is rajzolunk a virtuális kamera előtti térrészbe, az felül fogja írni a kamera képét a modellek térbeli helyzetétől függetlenül. Ez a felülírás minden olyan esetben problémát okoz, mikor a kamera képén látott valós tárgy takarná valamelyik virtuális tárgyunkat, mivel ilyen esetekben is a virtuális tárgy fog látszódni. A problémát a 4.7-es ábrán figyelhetjük meg.
12 A demo alkalmazáshoz használt modellt az Internetről töltöttem le az alábbi címről: http://www.turbosquid.com/FullPreview/Index.cfm/ID/268878
4.5. Virtuális tárgyak kirajzolása
48
4.7. ábra. Takarási viszonyok kezelése nélküli rajzolás. Ahhoz, hogy a valós és virtuális tárgyak közötti takarási viszonyokat kezelni tudjuk, szükségünk van a valós tárgyak háromdimenziós modelljére és a térbeli helyzetét leíró adatokra, melyek adottnak tekinthetők. Ha az ismert valós tárgyak közelítő modelljét a virtuális tárgyakkal együtt kirajzoljuk, az OpenGL kiszámítja nekünk, hogy melyik tárgy van takarásban. Mivel azonban nem szeretnénk, hogy a valós tárgyak modelljei is látszódjanak, a kirajzolás közben letiltjuk a rasztertár írását , így bár a z-puffer tartalmazni fogja a mélységi információkat, csak az illesztett tárgyak fognak megjelenni. A valós és virtuális tárgyak közötti takarási viszonyok kezelésére egy téglatestet reprezentáló osztályt (Brick) implementáltam, melynek példányai téglatest alakú valós tárgyakat modelleznek. Természetesen bármilyen bonyolultabb modellt is használhatunk, a takarási feladat bemutatásához azért választottam a téglatestet, mert ilyen alakú tárgyat könnyen találhatunk környezetünkben, egyszerűsége miatt pedig nem igényel modellezői ismereteket. Minden tárgy helyzete a világ-koordinátarendszerben adott, melynek origója a sakktáblaminta bal alsó, fekete színű mezőjének belső sarokpontja, egysége pedig egy mező szélességével egyenlő. Tehát ha egy valós tárgyat helyezünk a jelenetbe, aminek a takarási
4.5. Virtuális tárgyak kirajzolása
49
viszonyait szeretnénk helyesen kezelni, meg kell adnunk, hogy az origóhoz képest hány egységre helyezkedik el és ugyanezen egységben mérve mekkora a térbeli kiterjedése. Helyesen megadott pozíció mellett minél pontosabban közelíti modellünk a valós tárgy alakját, annál pontosabb lesz a takarás (4.8-as ábra). glDisable( GL_DEPTH_TEST ); glMatrixMode( GL_MODELVIEW ); glPushMatrix( ); glLoadIdentity( ); glMatrixMode( GL_PROJECTION ); glPushMatrix( ); glLoadIdentity( ); gluOrtho2D( 0.0, 1.0, 0.0, 1.0 ); glEnable( GL_TEXTURE_2D ); glBindTexture( GL_TEXTURE_2D, mTexture ); glPixelStorei( GL_UNPACK_ALIGNMENT, 1 ); glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST ); glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST ); glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP ); glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP ); glTexEnvi( GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE ); glTexImage2D( GL_TEXTURE_2D, 0, GL_RGB,frame->width, frame->height, 0, GL_BGR_EXT, GL_UNSIGNED_BYTE, frame->imageData ); glBegin( GL_QUADS ); glTexCoord2f( glTexCoord2f( glTexCoord2f( glTexCoord2f( glEnd( );
4.8. ábra. Rajzolás a takarási viszonyok kezelésével.
4.5.4. Audio Az árnyékok használatánál már volt róla szó, hogy a pontos illesztésen túl más módon is támogathatjuk a virtuális tárgyak valós környezetbe helyezését. A szemünk után a világ érzékelésére használt második legfontosabb érzékszervünk a fülünk. Ha nem látunk egy tárgyat, de halljuk az általa keltett hangot, meg tudjuk mondani, hogy merre található. Még ha látjuk is az adott tárgyat, sokat segíthet a térbeli hangzás az érzékelésben. Ezen okokból kifolyólag
a
virtuális
tárgyak
illesztésének
támogatására
a
térbeli
hangzást
is
megvalósítottam. Térbeli hangzást a 2.3-as fejezetben ismertetett OpenAL könyvtár segítségével tudunk létrehozni. A virtuális tárgyak lesznek a hangforrások, a hallgató pedig a kamera. Mivel a valós világ hangjait a valós kamera rögzíti13, a virtuális modellek hangérzékelése logikailag a 13 A webkamerák általában tartalmaznak mikrofont, de külső hangrögzítő esetén is az eszköz többnyire a kamerához közel helyezkedik el, így ebben az esetben is tekinthetjük úgy, hogy a hangokat a kamera pozíciójából halljuk.
4.5. Virtuális tárgyak kirajzolása
51
virtuális kamerához tartozik. Ennek megfelelően a hallgató pozíciójának és orientációjának beállítása a virtuális kamera felparaméterezését megvalósító setCameraView metódusban valósul meg. A 4.1-es fejezetben foglaltak szerint a virtuális kamera helyzetét a modell-nézeti transzformációs mátrix írja le. Ahhoz, hogy a hallgató pozícióját és orientációját megkaphassuk, ezt a mátrixot kell lekérdeznünk az OpenGL-től. A beállított paraméterek értékének lekérdezésére általánosan a glGet alakú hívásokat használhatjuk, ahol a hívás nevének vége a lekérdezett paraméter típusát határozza meg. Mivel a modell-nézeti transzformáció
megadásához
dupla
pontosságú
lebegőpontos
tömböt
használtunk,
lekérdezésére a glGetDoublev hívás használható. A mátrix alakja a következő:
[
ux vx wx uy vy wy T modeview = uz vz wz −camera x −camera y −camera z
0 0 0 1
]
(4.10)
ahol u, v és w vektorok jelölik a kamera-koordinátarendszer bázisvektorait rendre a vízszintes, függőleges (up) és nézeti iránynak (lookat) megfelelően. A 2.3.1-es fejezetben már találkoztunk ezekkel a vektorokkal, miszerint az OpenAL hallgatójának leírása pontosan az OpenGL-t követi, így nincs más dolgunk, mint a modell-nézeti mátrix két oszlopvektorát és a transzlációs sorvektorát beállítani a hallgatót leíró értékeknek. Szintén a 2.3.1-es fejezetben elmélkedtünk az OpenAL mértékegység-függetlenségén. Szó volt róla, hogy az egyes távolságokhoz kívánt hangerőt skáláznunk kell, ezért itt minden távolságot a tizedére vettem, hogy az általam megfelelőnek ítélt hangerőt elérjem. Természetesen itt bármilyen más skálázó tényező szerepelhet; ha a pozíciót felszorozzuk, halkabb hangokat kapunk, osztáskor pedig a hangerő növekedését érhetjük el. Nem szabad megfeledkeznünk arról sem, hogy a hangforrások elmozdulásának beállításakor is ugyanezen skálázó értéket kell használnunk a helyes működéshez. glGetDoublev( GL_MODELVIEW_MATRIX , modelview ); listenerPos[ 0 ] = - ( ALfloat ) modelview[ 12 ] / 10; listenerPos[ 1 ] = - ( ALfloat ) modelview[ 13 ] / 10; listenerPos[ 2 ] = - ( ALfloat ) modelview[ 14 ] / 10; listenerOri[ listenerOri[ listenerOri[ listenerOri[ listenerOri[ listenerOri[
A hangforrások a virtuális tárgyak, melyek mozoghatnak. A demo alkalmazásban az egyetlen hangforrás a kisautó, mely egy 54 másodperc hosszú wave formátumú hangmintát szólaltat meg ciklikusan lejátszva. ALuint buffer; ALenum format; ALsizei size; ALvoid* data; ALsizei freq; ALboolean loop; alGenBuffers( 1, &buffer ); alutLoadWAVFile( "mc_.wav", &format, &data, &size, &freq, &loop ); alBufferData( buffer, format, data, size, freq ); alutUnloadWAV( format, data, size, freq ); alGenSources( 1, &source ); alSourcei( source, AL_BUFFER, buffer ); alSourcef(source, AL_GAIN, scale); alSourcefv( source, AL_POSITION, sourcePos ); alSourcei( source, AL_LOOPING, AL_TRUE );
Az egyszerűség kedvéért állóhelyzetben nem ad hangot, mozgás közben pedig a sebességétől független hangminta kerül lejátszásra. if ( (vel == 0) && ( abs( vel0 ) > 0 ) ) { alSourcePlay( source ); } else if ( ( abs ( vel ) > 0 ) && ( vel0 == 0 ) ) alSourceStop( source );
Amelyik animációs eljárásban a helyüket megváltoztatják, értelemszerűen oda kell beillesztenünk a hangforrások pozíciójának megadását is. sourcePos[ 0 ] = x / 10; sourcePos[ 1 ] = y / 10; alSourcefv( source, AL_POSITION, sourcePos );
4.6. Követés Az OpenCV beépített sakktábla-felismerője meglehetősen szigorúan van hangolva, aminek következtében sokszor – főleg a webkamera gyenge minőségű képén – veszítjük el a sarokpontokat. Ha nem találjuk meg a sarokpontokat, nem tudjuk kiszámítani a külső paraméterek
értékét,
így
a
virtuális
tárgyak
kirajzolása
nem
lehetséges.
Ennek
4.6. Követés
53
következtében, ha a körülmények kismértékben is romlanak, a modellek rövidebb-hosszabb időre eltűnnek a képről. A körülmények romlását pedig már az is előidézi, ha a mintára kicsit is élesebb szögből tekintünk, általában viszont oldalirányból szeretünk gyönyörködni az illesztett képben. Ennek megoldására, vagyis a kétdimenziós pontok koordinátáinak folyamatos ismeretére a következő stratégiát dolgoztam ki: amikor csak lehetséges, a koordinátákat a szigorú felismerőtől kapjuk meg. Ha felismerő hamis értékkel tér vissza, tehát nem találta meg az összes pontok, akkor az előző képen megtalált pontokat a színük alapján megkeressük az új képen. Jellemzően az előző kép szűk környezetében meg tudjuk találni az összes pontot, mivel a sakktábla-minta belső sarokpontjai a fekete-fehér színes váltása miatt jól követhető. Ha a követés valamely pontnál téveszt, onnantól a rosszul felismert képpontot fogja követni, tehát a hiba akkumulálódik. Ennek kompenzálására minden olyan esetben, ahol a szigorú felismerő igaz értékkel téri vissza, a pontok helyzetét beállítjuk az általa megtalált képpontokra. A felismerés és követés váltását a megtalált pontokra kirajzolt körök színe jelzi: a kék szín a felismerést, a zöld szín pedig a követést jelenti. Jellegzetes pontok követésére a Lucas-Kanade algoritmust használhatjuk [12], mely szintén része az OpenCV-nek (cvCalcOpticalFlowPyrLK). Az eljárás nevében az optical flow arra az eljáráscsaládra utal, melyben valamilyen objektum mozgását két kép eltérése alapján határozzuk meg. A ”pyr” az OpenCV implementációs megoldására utal, melyben rekurzív módon épít fel egy ”piramisszerű” adatszerkezetet a eljárás hatékonyságának növelésére. Az adatszerkezet számára a helyfoglalásról nekünk kell gondoskodnunk, mivel az implementáció sajátossága miatt az egy képre kiszámított értékeket felhasználhatjuk a következő képhez is. Mivel azonban a mintafelismerés és követésből szerzett információk teljesen átlátszóak a feldolgozó modul számára, nem lehetünk biztosak abban, hogy az előző képen a követés vagy a felismerés futott le. Ebből kifolyólag nem használhatjuk ki ezt az előnyt; minden képet, illetve képpárt önmagában, a többitől függetlenül kezelünk. Az eljárás hívás előtt a vizsgált képpárt szürkeárnyalatosra konvertáljuk, majd a paraméterekben átadjuk a képek méretét, a keresési környezet méretét és az eljárás iterációs megvalósítása miatt egy leállási feltételt.
4.6. Követés
54
A mintafelismerőnél szó volt arról, hogy a Points2D tömb helyett a Points2D_ tömbben kapjuk meg az új képen megtalált pontok koordinátáit. Ennek a követésnél van jelentősége, mivel a mintafelismerőnél is fel kell készülnünk arra az esetre, ha a következő képen már követésre kell váltanunk, amihez mindig szükségünk van az előző képen megtalált pontok koordinátáira, melyeket a Points2D_ tömbben gyűjtünk, és magára az előző képre, melyet pedig a prev_gray tárol. A sakktábla osztály Points2D tömbje minden időpillanatban lekérdezhető, így mindig az aktuális képhez tartozó koordinátákat kell tartalmaznia. A követés előtt tehát előtt tehát ez a tömb tárolja a keresendő pontok aktuális koordinátáit, az újakat pedig a Points2D_ tömbben kapjuk meg. A követés utána a két tömb értékeit felcseréljük, és a feldolgozott új képet beállítjuk a következő iteráció előző képének. cvCvtColor( frame, gray, CV_BGR2GRAY ); cvCalcOpticalFlowPyrLK( prev_gray, gray, prev_pyramid, pyramid, Points2D, Points2D_, width * height, cvSize( 14, 14 ), 3, status, 0, cvTermCriteria( CV_TERMCRIT_ITER | CV_TERMCRIT_EPS, 20, 0.03 ), 0 ); cvCopy( gray, prev_gray ); for ( int j = 0; j < height; j++ ) { for ( int k = 0; k < width; k++ ) {
}
Points2D[ j * width + k ].x = Points2D_[ j * width + k ].x; Points2D[ j * width + k ].y = Points2D_[ j * width + k ].y;
}
4.7. Kezelési útmutató A program induláskor a konzolra írja a kezelési útmutatót: std::cout << " \n" " Usage:\n" " -p <pause/play> " -b " -c " -f " -u " -l " -s <shadow> " -a