5.11. Diferențierea cadrelor¶
Diferențierea cadrelor compară fiecare cadru nou cu un cadru de referință stocat pentru a găsi părțile scenei care s-au modificat. Este pilonul aplicațiilor de cameră care urmăresc apariția unui eveniment – capturarea declanșată de mișcare, alertele de intruziune, „salvează un video când ceva se mișcă” – și este construită în întregime din operațiile la nivel de pixel prezentate anterior: o diferență absolută, un prag și o căutare pe regiune, rulate pe fiecare cadru.
5.11.1. Fluxul de bază¶
Prima etapă este achiziționarea unei referințe. La un moment dat aproape de pornire – ideal când scena se află în starea pe care o înseamnă „fără modificare” – aplicația capturează un cadru și îl păstrează. Cadrul devine baza de referință cu care va fi comparată fiecare captură ulterioară.
reference = csi0.snapshot().copy()
.copy() este important. csi0.snapshot() în sine returnează un obiect Image al cărui tampon se află în tamponul de cadre (frame buffer), unde următorul apel la snapshot îl va suprascrie. .copy() alocă un tampon separat pentru referință și permite pixelilor acestui cadru să supraviețuiască dincolo de următoarea captură.
A doua etapă rulează pe fiecare cadru: capturează o imagine proaspătă, apoi calculează diferența absolută dintre aceasta și referință. Exact asta face difference():
current = csi0.snapshot()
current.difference(reference)
După acest apel, current conține o imagine ai cărei pixeli nenuli marchează fiecare poziție unde scena s-a modificat de la preluarea referinței, cu magnitudinea fiecărui pixel proporțională cu cât de mult s-a schimbat în acea poziție.
A treia etapă aplică un prag asupra imaginii de diferență. Diferența brută conține întotdeauna un anumit zgomot: mici variații de luminozitate cauzate de zgomotul fotonic al senzorului, modificări de gradient din deriva iluminării, tremurături subpixel din mișcarea ușoară a camerei. O trecere de prag – binary() cu un prag setat deasupra acelui nivel de zgomot – păstrează doar modificările suficient de mari pentru a conta ca mișcare reală și le elimină pe celelalte, producând o imagine binară ai cărei pixeli nenuli sunt pozițiile care s-au modificat efectiv.
A patra etapă extrage regiunile conectate din acea mască binară – grupuri de pixeli nenuli adiacenți care formează zone contigue. find_blobs() face asta într-un singur apel, returnând o listă de regiuni de mișcare, fiecare cu o casetă de încadrare și un număr de pixeli, asupra cărora restul aplicației poate acționa.
Fluxul de diferențiere a cadrelor: un cadru de referință plus un cadru curent devin o imagine de diferență; aplicarea pragului transformă diferența într-o mască binară a pozițiilor modificate; un pas de regiuni conectate transformă masca într-o listă de regiuni de mișcare.¶
5.11.2. Referințe în memorie și pe disc¶
Fluxul de bază păstrează cadrul de referință în RAM. Acesta este răspunsul corect atunci când referința este capturată în această rulare a scriptului și trebuie să supraviețuiască doar atât timp cât scriptul continuă să ruleze.
Pentru o aplicație de lungă durată – o cameră care ar trebui să reia detectarea modificărilor după o repornire, un script intermitent care trebuie să detecteze orice modificare de la un moment anterior – cadrul de referință trebuie să supraviețuiască scriptului în execuție. Tiparul constă în salvarea referinței pe disc:
csi0.snapshot().save("/sdcard/reference.bmp")
și încărcarea ei înapoi la începutul fiecărei rulări:
reference = image.Image("/sdcard/reference.bmp")
Logica de diferențiere nu se schimbă; se schimbă doar locul unde se află referința între capturi. Câteva rafinări extind în mod natural această variantă pe disc – recapturarea automată a referinței după un temporizator, medii rulante opționale pentru a urmări deriva lentă a iluminării – dar substituirea din centru este aceeași.
5.11.3. Izolarea sursei de lumină¶
Același tipar de scădere apare într-un context ușor diferit: izolarea unei surse de lumină față de restul scenei. Trucul este să capturezi o referință „cu luminile stinse” – un cadru preluat când ceea ce se detectează (un far IR, un pixel de ecran, un indicator de stare) nu este iluminat – și să scazi acea referință din fiecare cadru ulterior. Rezultatul are luminozitate zero peste tot unde scena a fost identică în ambele capturi și luminozitate nenulă doar acolo unde sursa de lumină s-a aprins efectiv.
5.11.4. Alegerea între difference și sub¶
O notă practică despre ce operație aritmetică să alegi. difference() returnează valoarea absolută a modificării – fără semn – ceea ce o face sensibilă la modificare în ambele direcții (luminare sau întunecare) cu prețul de a nu indica aplicației în ce direcție a mers modificarea. Pentru detectarea pură a mișcării acesta este răspunsul corect: orice s-a mișcat este interesant, indiferent în ce sens s-a deplasat luminozitatea.
Pentru detectarea sursei de lumină, pixelul aprins este întotdeauna mai luminos decât referința cu luminile stinse, așa că sub() (cu limitarea sa la zero) este alegerea mai onestă. Oriunde cadrul curent este mai întunecat decât referința (ceea ce ar fi zgomot al senzorului în jurul valorii neiluminate) se limitează la zero în loc să raporteze un semnal fals de tipul „lumina era aprinsă”.