5.11. Frame differencing

Frame differencing vergelijkt elk nieuw frame met een opgeslagen referentieframe om de delen van de scene te vinden die zijn veranderd. Het is het werkpaard van cameratoepassingen die op iets letten dat gebeurt – door beweging geactiveerde opname, inbraakwaarschuwingen, “sla een video op wanneer er iets beweegt” – en het is volledig opgebouwd uit de pixelgewijze bewerkingen die eerder zijn behandeld: een absoluut verschil, een drempelwaarde en een regiozoekopdracht, uitgevoerd op elk frame.

5.11.1. De basispijplijn

De eerste fase is het verwerven van een referentie. Op een bepaald moment vlak na het opstarten – idealiter wanneer de scene zich in de toestand bevindt die “geen verandering” betekent – legt de toepassing een frame vast en bewaart het. Het frame wordt de basislijn waarmee elke volgende opname zal worden vergeleken.

reference = csi0.snapshot().copy()

De .copy() is belangrijk. csi0.snapshot() op zichzelf retourneert een Image waarvan de buffer in de framebuffer leeft, waar de volgende aanroep van snapshot deze zal overschrijven. .copy() wijst een afzonderlijke buffer toe voor de referentie en laat de pixels van dit frame voortbestaan voorbij de volgende opname.

De tweede fase wordt op elk frame uitgevoerd: leg een verse afbeelding vast en bereken vervolgens het absolute verschil tussen die afbeelding en de referentie. Dat is precies wat difference() doet:

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

Na deze aanroep bevat current een afbeelding waarvan de niet-nulpixels elke positie markeren waar de scene is veranderd sinds de referentie werd genomen, waarbij de magnitude van elke pixel evenredig is aan hoeveel deze op die positie is veranderd.

De derde fase past een drempelwaarde toe op de verschilafbeelding. Het ruwe verschil bevat altijd wat ruis: kleine helderheidsvariaties door schotruis van de sensor, gradiëntveranderingen door verlichtingsdrift, subpixeltrilling door lichte camerabeweging. Een drempelstap – binary() met een drempelwaarde ingesteld boven die ruisvloer – behoudt alleen de veranderingen die groot genoeg zijn om als echte beweging te tellen en verwerpt de rest, wat een binaire afbeelding oplevert waarvan de niet-nulpixels de daadwerkelijk veranderde posities zijn.

De vierde fase extraheert verbonden regio’s van dat binaire masker – groepen aangrenzende niet-nulpixels die aaneengesloten vlakken vormen. find_blobs() doet dat in één aanroep en retourneert een lijst van bewegingsregio’s, elk met een begrenzingsvak en een pixelaantal, waarop de rest van de toepassing kan reageren.

Een horizontaal pijplijndiagram. De twee meest linkse panelen zijn een referentieframe en een huidig frame naast elkaar, met een plusteken ertussen. Een pijl leidt van het paar naar een derde paneel met het label verschil, waarin een paar vlakken helder afsteken tegen een donkere achtergrond. Een pijl leidt vandaar naar een vierde paneel dat een binaire, op drempelwaarde gebrachte versie van het verschil toont, waarbij dezelfde vlakken nu effen wit zijn. Een laatste pijl leidt naar een vijfde paneel dat het binaire masker toont, geannoteerd met rechthoekige begrenzingsvakken rond elk vlak.

De frame-differencing-pijplijn: een referentieframe plus een huidig frame worden een verschilafbeelding; drempelwaarde verandert het verschil in een binair masker van veranderde posities; een stap met verbonden regio’s verandert het masker in een lijst van bewegingsregio’s.

5.11.2. Referenties in geheugen en op schijf

De basispijplijn houdt het referentieframe in RAM. Dat is het juiste antwoord wanneer de referentie wordt vastgelegd tijdens deze uitvoering van het script en alleen hoeft te overleven zolang het script blijft draaien.

Voor een langdurig draaiende toepassing – een camera die de veranderingsdetectie moet hervatten na een stroomcyclus, een intermitterend script dat enige verandering sinds een eerder moment moet detecteren – moet het referentieframe het draaiende script overleven. Het patroon is om de referentie naar schijf te bewaren:

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

en deze aan het begin van elke uitvoering weer te laden:

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

De differencing-logica verandert niet; alleen waar de referentie tussen opnames leeft, verandert. Een paar verfijningen breiden deze schijfvariant op natuurlijke wijze uit – automatische heropname van de referentie op een timer, optionele voortschrijdende gemiddelden om langzame verlichtingsdrift te volgen – maar de substitutie in het midden is dezelfde.

5.11.3. Lichtbronisolatie

Hetzelfde aftrekpatroon duikt op in een iets andere context: het isoleren van een lichtbron tegen de rest van de scene. De truc is om een “licht-uit”-referentie vast te leggen – een frame genomen wanneer datgene wat wordt gedetecteerd (een IR-baken, een schermpixel, een statusindicator) niet verlicht is – en die referentie van elk volgend frame af te trekken. Het resultaat heeft nul helderheid overal waar de scene in beide opnames hetzelfde was, en niet-nul helderheid alleen waar de lichtbron daadwerkelijk oplichtte.

5.11.4. Kiezen tussen difference en sub

Een praktische opmerking over welke rekenkundige bewerking te kiezen. difference() retourneert de absolute waarde van de verandering – zonder teken – waardoor het gevoelig is voor verandering in beide richtingen (verhelderen of verdonkeren), ten koste van het niet vertellen aan de toepassing welke richting de verandering opging. Voor pure bewegingsdetectie is dat het juiste antwoord: alles wat bewoog is interessant, ongeacht in welke richting de helderheid verschoof.

Voor lichtbrondetectie is de verlichte pixel altijd helderder dan de licht-uit-referentie, dus sub() (met zijn afkapping op nul) is de eerlijkere keuze. Overal waar het huidige frame donkerder is dan de referentie (wat sensorruis rond de onverlichte waarde zou zijn) wordt afgekapt op nul in plaats van een vals “het licht was aan”-signaal te melden.