5.11. Rozdílování snímků¶
Rozdílování snímků porovnává každý nový snímek s uloženým referenčním snímkem, aby nalezlo ty části scény, které se změnily. Je tažným koněm kamerových aplikací, které sledují, zda se něco děje – snímání spouštěné pohybem, výstrahy o vniknutí, „ulož video, když se něco hne“ – a je celé postaveno z operací nad jednotlivými pixely popsaných dříve: absolutní rozdíl, práh a hledání oblastí, prováděné na každém snímku.
5.11.1. Základní řetězec zpracování¶
Prvním krokem je získat referenci. V nějakém okamžiku krátce po spuštění – ideálně tehdy, když je scéna ve stavu, který znamená „žádná změna“ – aplikace zachytí snímek a uchová si jej. Tento snímek se stane základem, vůči kterému bude porovnáván každý další zachycený snímek.
reference = csi0.snapshot().copy()
Na .copy() záleží. csi0.snapshot() sám o sobě vrací Image, jehož buffer žije ve frame bufferu, kde jej další volání snapshot přepíše. .copy() alokuje pro referenci samostatný buffer a umožní pixelům tohoto snímku přežít i po dalším zachycení.
Druhý krok se provádí na každém snímku: zachytit nový obraz a poté spočítat absolutní rozdíl mezi ním a referencí. Přesně to dělá difference():
current = csi0.snapshot()
current.difference(reference)
Po tomto volání obsahuje current obraz, jehož nenulové pixely označují každou pozici, kde se scéna od pořízení reference změnila, přičemž velikost každého pixelu je úměrná tomu, jak moc se na dané pozici změnila.
Třetí krok na rozdílový obraz aplikuje práh. Surový rozdíl vždy obsahuje nějaký šum: drobné jasové výkyvy ze šumu senzoru, změny gradientu z kolísání osvětlení, subpixelové chvění z mírného pohybu kamery. Prahovací průchod – binary() s prahem nastaveným nad touto úrovní šumu – ponechá jen změny dostatečně velké na to, aby se počítaly jako skutečný pohyb, a zbytek zahodí, čímž vznikne binární obraz, jehož nenulové pixely jsou skutečně změněné pozice.
Čtvrtý krok z této binární masky extrahuje propojené oblasti – skupiny sousedních nenulových pixelů, které tvoří souvislé plochy. find_blobs() to udělá jedním voláním a vrátí seznam oblastí pohybu, každou s ohraničujícím rámečkem a počtem pixelů, na které může zbytek aplikace reagovat.
Řetězec rozdílování snímků: z referenčního snímku a aktuálního snímku vzniká rozdílový obraz; prahování změní rozdíl na binární masku změněných pozic; krok propojených oblastí změní masku na seznam oblastí pohybu.¶
5.11.2. Reference v paměti a na disku¶
Základní řetězec zpracování uchovává referenční snímek v RAM. To je správná odpověď tehdy, když je reference zachycena při tomto spuštění skriptu a musí přežít jen po dobu, kdy skript běží.
U dlouhoběžící aplikace – kamery, která by měla obnovit detekci změn po vypnutí a zapnutí napájení, občasně spouštěného skriptu, který potřebuje detekovat jakoukoli změnu od nějakého dřívějšího okamžiku – musí referenční snímek přežít déle než běžící skript. Vzorem je referenci uložit na disk:
csi0.snapshot().save("/sdcard/reference.bmp")
a znovu ji načíst na začátku každého spuštění:
reference = image.Image("/sdcard/reference.bmp")
Logika rozdílování se nemění; mění se jen to, kde reference mezi zachyceními žije. Tuto variantu s ukládáním na disk přirozeně rozšiřuje několik vylepšení – automatické opětovné pořízení reference podle časovače, volitelné klouzavé průměry pro sledování pomalého kolísání osvětlení – ale substituce v jádru zůstává stejná.
5.11.3. Izolace zdroje světla¶
Tentýž vzor odečítání se objevuje i v mírně odlišném prostředí: při izolaci zdroje světla vůči zbytku scény. Trik spočívá v zachycení reference „se zhasnutými světly“ – snímku pořízeného ve chvíli, kdy cokoli, co se detekuje (IR maják, pixel obrazovky, stavový indikátor), nesvítí – a v odečtení této reference od každého následujícího snímku. Výsledek má nulový jas všude tam, kde byla scéna v obou zachyceních stejná, a nenulový jas pouze tam, kde se zdroj světla skutečně rozsvítil.
5.11.4. Volba mezi difference a sub¶
Praktická poznámka k tomu, kterou aritmetickou operaci zvolit. difference() vrací absolutní hodnotu změny – bez znaménka – což ji činí citlivou na změnu v obou směrech (zesvětlení i ztmavení) za cenu toho, že aplikaci neřekne, kterým směrem změna proběhla. Pro čistou detekci pohybu je to správná odpověď: cokoli, co se pohnulo, je zajímavé, bez ohledu na to, jak se jas posunul.
Pro detekci zdroje světla je rozsvícený pixel vždy jasnější než reference se zhasnutými světly, takže sub() (se svým ořezem na nulu) je čestnější volbou. Všude, kde je aktuální snímek tmavší než reference (což by byl šum senzoru kolem nerozsvícené hodnoty), se ořízne na nulu, místo aby se nahlásil falešný signál „světlo svítilo“.