5.11. Differenza tra frame¶
La differenza tra frame confronta ogni nuovo frame con un frame di riferimento memorizzato per individuare le parti della scena che sono cambiate. È il cavallo di battaglia delle applicazioni di camera che sorvegliano il verificarsi di un evento – acquisizione attivata dal movimento, allarmi di intrusione, «salva un video quando qualcosa si muove» – ed è costruita interamente a partire dalle operazioni pixel per pixel trattate in precedenza: una differenza assoluta, una soglia e una ricerca per regioni, eseguite su ogni frame.
5.11.1. La pipeline di base¶
La prima fase consiste nell”acquisire un riferimento. A un certo punto vicino all’avvio – idealmente quando la scena si trova nello stato che corrisponde a «nessun cambiamento» – l’applicazione cattura un frame e lo conserva. Il frame diventa la base di riferimento con cui verrà confrontata ogni acquisizione successiva.
reference = csi0.snapshot().copy()
Il .copy() è importante. csi0.snapshot() da solo restituisce un’immagine Image il cui buffer risiede nel frame buffer, dove la prossima chiamata a snapshot lo sovrascriverà. .copy() alloca un buffer separato per il riferimento e consente ai pixel di questo frame di sopravvivere alla successiva acquisizione.
La seconda fase viene eseguita su ogni frame: cattura una nuova immagine, poi calcola la differenza assoluta tra essa e il riferimento. È esattamente ciò che fa difference():
current = csi0.snapshot()
current.difference(reference)
Dopo questa chiamata, current contiene un’immagine i cui pixel non nulli segnalano ogni posizione in cui la scena è cambiata da quando è stato acquisito il riferimento, con la magnitudo di ciascun pixel proporzionale a quanto è cambiato in quella posizione.
La terza fase applica una soglia all’immagine di differenza. La differenza grezza contiene sempre del rumore: piccole variazioni di luminosità dovute al rumore di fotoni del sensore, variazioni di gradiente dovute alla deriva dell’illuminazione, micro-tremolii sub-pixel dovuti a lievi movimenti della camera. Un passaggio di soglia – binary() con una soglia impostata al di sopra di quel livello di rumore – mantiene solo i cambiamenti abbastanza grandi da contare come movimento reale e scarta il resto, producendo un’immagine binaria i cui pixel non nulli sono le posizioni effettivamente cambiate.
La quarta fase estrae le regioni connesse di quella maschera binaria – gruppi di pixel non nulli adiacenti che formano chiazze contigue. find_blobs() lo fa in un’unica chiamata, restituendo un elenco di regioni di movimento, ciascuna con un bounding box e un conteggio di pixel, su cui il resto dell’applicazione può agire.
La pipeline di differenza tra frame: un frame di riferimento più un frame corrente diventano un’immagine di differenza; l’applicazione della soglia trasforma la differenza in una maschera binaria delle posizioni cambiate; un passaggio di regioni connesse trasforma la maschera in un elenco di regioni di movimento.¶
5.11.2. Riferimenti in memoria e su disco¶
La pipeline di base mantiene il frame di riferimento in RAM. È la risposta giusta quando il riferimento viene catturato in questa esecuzione dello script e deve sopravvivere solo per il tempo in cui lo script continua a girare.
Per un’applicazione a lunga esecuzione – una camera che dovrebbe riprendere il rilevamento dei cambiamenti dopo un ciclo di alimentazione, uno script intermittente che deve rilevare qualsiasi cambiamento da un momento precedente – il frame di riferimento deve sopravvivere allo script in esecuzione. Lo schema consiste nel salvare il riferimento su disco:
csi0.snapshot().save("/sdcard/reference.bmp")
e nel ricaricarlo all’inizio di ogni esecuzione:
reference = image.Image("/sdcard/reference.bmp")
La logica di differenziazione non cambia; cambia solo dove risiede il riferimento tra un’acquisizione e l’altra. Alcuni perfezionamenti estendono naturalmente questa variante su disco – ri-acquisizione automatica del riferimento su un timer, medie mobili opzionali per seguire la lenta deriva dell’illuminazione – ma la sostituzione al centro è la stessa.
5.11.3. Isolamento della sorgente luminosa¶
Lo stesso schema di sottrazione compare in un contesto leggermente diverso: l’isolamento di una sorgente luminosa rispetto al resto della scena. Il trucco consiste nel catturare un riferimento «a luci spente» – un frame acquisito quando ciò che si sta rilevando (un faro IR, un pixel di uno schermo, un indicatore di stato) non è illuminato – e nel sottrarre quel riferimento da ogni frame successivo. Il risultato ha luminosità nulla ovunque la scena sia stata uguale in entrambe le acquisizioni e luminosità non nulla solo dove la sorgente luminosa si è effettivamente accesa.
5.11.4. Scelta tra difference e sub¶
Una nota pratica su quale operazione aritmetica scegliere. difference() restituisce il valore assoluto del cambiamento – senza segno – il che la rende sensibile al cambiamento in entrambe le direzioni (schiarimento o scurimento) al costo di non indicare all’applicazione in quale direzione sia avvenuto il cambiamento. Per il puro rilevamento del movimento questa è la risposta giusta: tutto ciò che si è mosso è interessante, indipendentemente da come si sia spostata la luminosità.
Per il rilevamento della sorgente luminosa, il pixel acceso è sempre più luminoso del riferimento a luci spente, quindi sub() (con il suo troncamento a zero) è la scelta più onesta. Ovunque il frame corrente sia più scuro del riferimento (il che sarebbe rumore del sensore attorno al valore non illuminato) viene troncato a zero anziché segnalare un falso segnale di «la luce era accesa».