5.11. Différenciation de trames

La différenciation de trames compare chaque nouvelle trame à une trame de référence stockée afin de repérer les parties de la scène qui ont changé. C’est le pilier des applications de caméra qui surveillent l’apparition d’un événement – capture déclenchée par le mouvement, alertes d’intrusion, « enregistrer une vidéo lorsque quelque chose bouge » – et elle se construit entièrement à partir des opérations pixel par pixel présentées plus tôt : une différence absolue, un seuil et une recherche de régions, exécutés sur chaque trame.

5.11.1. Le pipeline de base

La première étape consiste à acquérir une référence. À un certain moment près du démarrage – idéalement lorsque la scène est dans l’état que signifie « aucun changement » – l’application capture une trame et la conserve. Cette trame devient la base de référence à laquelle toute capture ultérieure sera comparée.

reference = csi0.snapshot().copy()

Le .copy() a son importance. csi0.snapshot() seul renvoie un Image dont le tampon réside dans le tampon d’image, où le prochain appel à snapshot l’écrasera. .copy() alloue un tampon distinct pour la référence et permet aux pixels de cette trame de survivre à la capture suivante.

La deuxième étape s’exécute sur chaque trame : capturer une nouvelle image, puis calculer la différence absolue entre celle-ci et la référence. C’est exactement ce que fait difference() :

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

Après cet appel, current contient une image dont les pixels non nuls marquent chaque position où la scène a changé depuis la prise de la référence, l’amplitude de chaque pixel étant proportionnelle à l’ampleur du changement à cette position.

La troisième étape applique un seuil à l’image de différence. La différence brute contient toujours du bruit : petites variations de luminosité dues au bruit de grenaille du capteur, changements de gradient dus à la dérive de l’éclairage, gigue sous-pixellique due à de légers mouvements de la caméra. Une passe de seuillage – binary() avec un seuil placé au-dessus de ce plancher de bruit – ne conserve que les changements suffisamment importants pour compter comme un véritable mouvement et écarte le reste, produisant une image binaire dont les pixels non nuls sont les positions réellement modifiées.

La quatrième étape extrait les régions connexes de ce masque binaire – des groupes de pixels non nuls adjacents formant des taches contiguës. find_blobs() le fait en un seul appel, renvoyant une liste de régions de mouvement, chacune avec une boîte englobante et un nombre de pixels, sur laquelle le reste de l’application peut agir.

Un schéma de pipeline horizontal. Les deux panneaux les plus à gauche sont une trame de référence et une trame courante côte à côte, avec un signe plus entre elles. Une flèche mène de la paire à un troisième panneau intitulé différence, dans lequel quelques taches sont claires sur un fond sombre. Une flèche mène de là à un quatrième panneau montrant une version binaire seuillée de la différence, avec les mêmes taches maintenant d'un blanc plein. Une dernière flèche mène à un cinquième panneau montrant le masque binaire annoté de boîtes englobantes rectangulaires dessinées autour de chaque tache.

Le pipeline de différenciation de trames : une trame de référence et une trame courante deviennent une image de différence ; le seuillage transforme la différence en un masque binaire des positions modifiées ; une étape de régions connexes transforme le masque en une liste de régions de mouvement.

5.11.2. Références en mémoire et sur disque

Le pipeline de base conserve la trame de référence en RAM. C’est la bonne réponse lorsque la référence est capturée durant cette exécution du script et ne doit survivre que tant que le script continue de s’exécuter.

Pour une application de longue durée – une caméra qui doit reprendre la détection de changement après une coupure de courant, un script intermittent qui doit détecter tout changement depuis un moment antérieur – la trame de référence doit survivre au script en cours d’exécution. Le motif consiste à enregistrer la référence sur le disque :

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

et à la recharger au début de chaque exécution :

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

La logique de différenciation ne change pas ; seul l’endroit où la référence réside entre les captures change. Quelques raffinements prolongent naturellement cette variante sur disque – recapture automatique de la référence sur un minuteur, moyennes glissantes optionnelles pour suivre la dérive lente de l’éclairage – mais la substitution au cœur du procédé reste la même.

5.11.3. Isolation d’une source lumineuse

Le même motif de soustraction apparaît dans un cadre légèrement différent : isoler une source lumineuse du reste de la scène. L’astuce consiste à capturer une référence « lumière éteinte » – une trame prise lorsque ce qui est détecté (une balise IR, un pixel d’écran, un indicateur d’état) n’est pas allumé – et à soustraire cette référence de chaque trame ultérieure. Le résultat a une luminosité nulle partout où la scène était identique dans les deux captures, et une luminosité non nulle uniquement là où la source lumineuse s’est effectivement allumée.

5.11.4. Choisir difference ou sub

Une note pratique sur l’opération arithmétique à choisir. difference() renvoie la valeur absolue du changement – sans signe – ce qui la rend sensible au changement dans les deux directions (éclaircissement ou assombrissement) au prix de ne pas indiquer à l’application dans quelle direction le changement s’est produit. Pour de la simple détection de mouvement, c’est la bonne réponse : tout ce qui a bougé est intéressant, quel que soit le sens dans lequel la luminosité a varié.

Pour la détection d’une source lumineuse, le pixel allumé est toujours plus clair que la référence lumière éteinte, donc sub() (avec son écrêtage à zéro) est le choix le plus honnête. Partout où la trame courante est plus sombre que la référence (ce qui correspondrait au bruit du capteur autour de la valeur éteinte), le résultat est écrêté à zéro plutôt que de signaler un faux signal « la lumière était allumée ».