5.11. Képkocka-különbségképzés

A képkocka-különbségképzés minden új képkockát egy eltárolt referencia-képkockához hasonlít, hogy megtalálja a jelenet megváltozott részeit. Ez a gerince azoknak a kamerás alkalmazásoknak, amelyek valamilyen esemény bekövetkezését figyelik – mozgásra indított felvétel, behatolásriasztás, „ments el egy videót, ha valami megmozdul” –, és teljes egészében a korábban tárgyalt képpontonkénti műveletekből épül fel: egy abszolút különbségből, egy küszöbölésből és egy területkeresésből, amelyeket minden képkockán lefuttatunk.

5.11.1. Az alapvető folyamat

Az első lépés egy referencia beszerzése. Az indulás közelében valamikor – ideális esetben akkor, amikor a jelenet abban az állapotban van, amit a „nincs változás” jelent – az alkalmazás rögzít egy képkockát, és megőrzi azt. Ez a képkocka lesz az az alapérték, amelyhez minden ezt követő felvételt hasonlítunk.

reference = csi0.snapshot().copy()

A .copy() számít. A csi0.snapshot() önmagában egy olyan Image objektumot ad vissza, amelynek puffere a képkocka-pufferben él, ahol a snapshot következő hívása felül fogja írni. A .copy() külön puffert foglal a referenciának, és lehetővé teszi, hogy ennek a képkockának a képpontjai túléljék a következő felvételt.

A második lépés minden képkockán lefut: rögzít egy friss képet, majd kiszámítja az abszolút különbséget közte és a referencia között. Pontosan ezt teszi a difference():

current = csi0.snapshot()
current.difference(reference)

E hívás után a current egy olyan képet tartalmaz, amelynek nem nulla képpontjai minden olyan pozíciót megjelölnek, ahol a jelenet a referencia rögzítése óta megváltozott, az egyes képpontok nagysága pedig arányos azzal, hogy az adott pozíción mennyit változott.

A harmadik lépés küszöböli a különbségképet. A nyers különbség mindig tartalmaz némi zajt: az érzékelő shot-zajából eredő apró fényerő-ingadozásokat, a megvilágítás elcsúszásából eredő gradiensváltozásokat, az enyhe kameramozgásból eredő képponton belüli remegést. Egy küszöbölési lépés – a binary() a zajszint fölé állított küszöbértékkel – csak azokat a változásokat tartja meg, amelyek elég nagyok ahhoz, hogy valódi mozgásnak számítsanak, a többit pedig eldobja, így egy bináris képet hoz létre, amelynek nem nulla képpontjai a ténylegesen megváltozott pozíciók.

A negyedik lépés kinyeri ennek a bináris maszknak az összefüggő régióit – a szomszédos nem nulla képpontok csoportjait, amelyek folytonos foltokat alkotnak. A find_blobs() ezt egyetlen hívással elvégzi, és visszaad egy listát a mozgásrégiókról, mindegyiket egy határoló dobozzal és egy képpontszámmal, amelyekre az alkalmazás többi része reagálhat.

Egy vízszintes folyamatábra. A legbaloldalibb két panel egy referencia- képkocka és egy aktuális képkocka egymás mellett, közöttük egy pluszjellel. Egy nyíl vezet a pártól egy harmadik, különbségnek nevezett panelhez, amelyben néhány folt világosan kiemelkedik a sötét háttér ellenében. Innen egy nyíl vezet egy negyedik panelhez, amely a különbség bináris, küszöbölt változatát mutatja, ahol ugyanazok a foltok most egyszínű fehérek. Egy utolsó nyíl vezet egy ötödik panelhez, amely a bináris maszkot mutatja, az egyes foltok köré rajzolt téglalap alakú határoló dobozokkal jelölve.

A képkocka-különbségképzési folyamat: egy referencia-képkockából és egy aktuális képkockából különbségkép lesz; a küszöbölés a különbséget a megváltozott pozíciók bináris maszkjává alakítja; egy összefüggő-régió lépés a maszkot a mozgásrégiók listájává alakítja.

5.11.2. Memóriabeli és lemezen tárolt referenciák

Az alapvető folyamat a referencia-képkockát a RAM-ban tartja. Ez a helyes megoldás akkor, amikor a referenciát a szkript ezen futása során rögzítjük, és csak addig kell fennmaradnia, ameddig a szkript fut.

Egy hosszan futó alkalmazás esetén – egy kamera, amelynek áramkimaradás után folytatnia kell a változásészlelést, egy időszakos szkript, amelynek bármilyen változást észlelnie kell egy korábbi pillanat óta – a referencia-képkockának túl kell élnie a futó szkriptet. A minta az, hogy a referenciát elmentjük a lemezre:

csi0.snapshot().save("/sdcard/reference.bmp")

és minden futás elején visszatöltjük:

reference = image.Image("/sdcard/reference.bmp")

A különbségképzési logika nem változik; csak az, hogy hol él a referencia a felvételek között. Néhány finomítás természetes módon kiegészíti ezt a lemezen tárolt változatot – a referencia automatikus újrarögzítése időzítővel, opcionális mozgóátlagok a lassú megvilágítás-elcsúszás követésére –, de a középponti helyettesítés ugyanaz.

5.11.3. Fényforrás-izolálás

Ugyanez a kivonási minta egy kissé eltérő helyzetben is felbukkan: egy fényforrás elkülönítésekor a jelenet többi részétől. A trükk az, hogy rögzítünk egy „lekapcsolt fény” referenciát – egy olyan képkockát, amelyet akkor készítünk, amikor az, amit éppen észlelni akarunk (egy IR-jeladó, egy képernyőképpont, egy állapotjelző), nem világít –, és ezt a referenciát kivonjuk minden ezt követő képkockából. Az eredmény mindenhol nulla fényerejű, ahol a jelenet mindkét felvételen azonos volt, és csak ott nem nulla fényerejű, ahol a fényforrás ténylegesen felvillant.

5.11.4. A difference vagy a sub választása

Egy gyakorlati megjegyzés arról, hogy melyik aritmetikai műveletet válasszuk. A difference() a változás abszolút értékét adja vissza – előjel nélkül –, ami érzékennyé teszi bármelyik irányú változásra (világosodás vagy sötétedés), annak árán, hogy nem árulja el az alkalmazásnak, melyik irányba ment a változás. A tiszta mozgásészleléshez ez a helyes megoldás: bármi, ami megmozdult, érdekes, függetlenül attól, hogy melyik irányba tolódott el a fényerő.

A fényforrás-észleléshez a megvilágított képpont mindig világosabb, mint a lekapcsolt fény referencia, ezért a sub() (a nullánál történő levágásával) a becsületesebb választás. Mindenütt, ahol az aktuális képkocka sötétebb a referenciánál (ami a nem megvilágított érték körüli érzékelőzaj lenne), nullára vág, ahelyett hogy hamis „a fény be volt kapcsolva” jelet jelentene.