5.11. Bildrutedifferentiering¶
Bildrutedifferentiering jämför varje ny bildruta mot en lagrad referensbildruta för att hitta de delar av scenen som har förändrats. Det är arbetshästen i kameratillämpningar som bevakar att något händer – rörelseutlöst infångning, intrångslarm, ”spara en video när något rör sig” – och den är helt och hållet uppbyggd av de pixelvisa operationerna som beskrevs tidigare: en absolut differens, ett tröskelvärde och en regionsökning, körd på varje bildruta.
5.11.1. Den grundläggande pipelinen¶
Det första steget är att hämta en referens. Vid någon tidpunkt nära uppstart – helst när scenen är i det tillstånd som ”ingen förändring” betyder – fångar tillämpningen en bildruta och behåller den. Bildrutan blir den baslinje som varje efterföljande infångning kommer att jämföras mot.
reference = csi0.snapshot().copy()
Det är .copy() som är avgörande. csi0.snapshot() returnerar i sig en Image vars buffert ligger i bildbufferten, där nästa anrop till snapshot kommer att skriva över den. .copy() allokerar en separat buffert för referensen och låter pixlarna i denna bildruta överleva förbi nästa infångning.
Det andra steget körs på varje bildruta: fånga en ny bild och beräkna sedan den absoluta differensen mellan den och referensen. Det är precis vad difference() gör:
current = csi0.snapshot()
current.difference(reference)
Efter detta anrop innehåller current en bild vars nollskilda pixlar markerar varje position där scenen förändrats sedan referensen togs, med varje pixels magnitud proportionell mot hur mycket den förändrats på den positionen.
Det tredje steget tröskelvärdesbehandlar differensbilden. Den råa differensen innehåller alltid en del brus: små ljushetsvariationer från sensorns fotonbrus, gradientförändringar från ljusdrift, subpixeljitter från lätt kamerarörelse. En tröskelvärdespassage – binary() med ett tröskelvärde satt över det brusgolvet – behåller endast de förändringar som är tillräckligt stora för att räknas som verklig rörelse och förkastar resten, vilket producerar en binär bild vars nollskilda pixlar är de faktiskt förändrade positionerna.
Det fjärde steget extraherar sammanhängande regioner av den binära masken – grupper av angränsande nollskilda pixlar som bildar sammanhängande fläckar. find_blobs() gör det i ett enda anrop och returnerar en lista över rörelseregioner, var och en med en begränsningsruta och ett pixelantal, som resten av tillämpningen kan agera på.
Bildrutedifferentierings-pipelinen: en referensbildruta plus en aktuell bildruta blir en differensbild; tröskelvärdesbehandling förvandlar differensen till en binär mask av förändrade positioner; ett steg för sammanhängande regioner förvandlar masken till en lista över rörelseregioner.¶
5.11.2. Referenser i minnet och på disk¶
Den grundläggande pipelinen håller referensbildrutan i RAM. Det är rätt svar när referensen fångas under denna körning av skriptet och bara behöver överleva så länge skriptet fortsätter att köra.
För en långkörande tillämpning – en kamera som ska återuppta förändringsdetektering efter en strömcykel, ett återkommande skript som behöver detektera vilken som helst förändring sedan något tidigare ögonblick – måste referensbildrutan överleva längre än det körande skriptet. Mönstret är att spara referensen till disk:
csi0.snapshot().save("/sdcard/reference.bmp")
och att läsa in den igen vid början av varje körning:
reference = image.Image("/sdcard/reference.bmp")
Differentieringslogiken förändras inte; bara var referensen bor mellan infångningarna. Några förfiningar utökar naturligt denna disk-variant – automatisk återinfångning av referensen med en timer, valfria glidande medelvärden för att spåra långsam ljusdrift – men substitutionen i mitten är densamma.
5.11.3. Isolering av ljuskälla¶
Samma subtraktionsmönster dyker upp i en något annorlunda situation: att isolera en ljuskälla mot resten av scenen. Tricket är att fånga en referens med ”ljuset av” – en bildruta tagen när det som ska detekteras (en IR-fyr, en skärmpixel, en statusindikator) inte är upplyst – och att subtrahera den referensen från varje efterföljande bildruta. Resultatet har noll ljushet överallt där scenen var densamma i båda infångningarna, och nollskild ljushet endast där ljuskällan faktiskt lyste upp.
5.11.4. Att välja difference eller sub¶
En praktisk anmärkning om vilken aritmetisk operation man ska välja. difference() returnerar absolutvärdet av förändringen – teckenfritt – vilket gör den känslig för förändring i endera riktningen (ljusare eller mörkare) på bekostnad av att inte berätta för tillämpningen vilken riktning förändringen gick. För ren rörelsedetektering är det rätt svar: allt som rörde sig är intressant, oavsett åt vilket håll ljusheten skiftade.
För ljuskälledetektering är den upplysta pixeln alltid ljusare än referensen med ljuset av, så sub() (med sin klippning vid noll) är det mer ärliga valet. Överallt där den aktuella bildrutan är mörkare än referensen (vilket skulle vara sensorbrus runt det icke-upplysta värdet) klipps till noll snarare än att rapportera en falsk signal om att ”ljuset var på”.