5.11. Różnicowanie ramek¶
Różnicowanie ramek porównuje każdą nową ramkę z zapisaną ramką referencyjną, aby znaleźć te fragmenty sceny, które uległy zmianie. Jest to podstawowe narzędzie aplikacji kamerowych, które wypatrują, czy coś się dzieje – przechwytywanie wyzwalane ruchem, alerty o intruzach, „zapisz wideo, gdy coś się poruszy” – a zbudowane jest w całości z operacji wykonywanych na poszczególnych pikselach, omówionych wcześniej: różnicy bezwzględnej, progowania i wyszukiwania obszarów, uruchamianych na każdej ramce.
5.11.1. Podstawowy potok przetwarzania¶
Pierwszym etapem jest pozyskanie referencji. W pewnym momencie zaraz po uruchomieniu – najlepiej wtedy, gdy scena jest w stanie oznaczającym „brak zmiany” – aplikacja przechwytuje ramkę i ją zachowuje. Ramka ta staje się punktem odniesienia, z którym porównywane będzie każde kolejne przechwycenie.
reference = csi0.snapshot().copy()
Wywołanie .copy() ma znaczenie. csi0.snapshot() samo w sobie zwraca obiekt Image, którego bufor znajduje się w buforze ramki, gdzie kolejne wywołanie snapshot go nadpisze. .copy() przydziela osobny bufor dla referencji i pozwala, by piksele tej ramki przetrwały następne przechwycenie.
Drugi etap uruchamiany jest na każdej ramce: przechwyć nowy obraz, a następnie oblicz różnicę bezwzględną między nim a referencją. Dokładnie to robi difference():
current = csi0.snapshot()
current.difference(reference)
Po tym wywołaniu current przechowuje obraz, którego niezerowe piksele oznaczają każde położenie, w którym scena zmieniła się od momentu zarejestrowania referencji, przy czym wielkość każdego piksela jest proporcjonalna do tego, jak bardzo zmieniło się to położenie.
Trzeci etap progowania obrazu różnicowego. Surowa różnica zawsze zawiera pewien szum: niewielkie wahania jasności pochodzące z szumu fotonowego sensora, zmiany gradientu wynikające z dryfu oświetlenia, drgania subpikselowe z lekkiego ruchu kamery. Przebieg progujący – binary() z progiem ustawionym powyżej tego poziomu szumu – zachowuje tylko te zmiany, które są na tyle duże, by uznać je za rzeczywisty ruch, a resztę odrzuca, tworząc obraz binarny, którego niezerowe piksele to faktycznie zmienione położenia.
Czwarty etap wyodrębnia połączone obszary tej maski binarnej – grupy sąsiadujących niezerowych pikseli tworzące zwarte plamy. find_blobs() robi to w jednym wywołaniu, zwracając listę obszarów ruchu, z których każdy ma ramkę ograniczającą i liczbę pikseli, na podstawie których reszta aplikacji może podjąć działanie.
Potok różnicowania ramek: ramka referencyjna i bieżąca ramka stają się obrazem różnicowym; progowanie zamienia różnicę w maskę binarną zmienionych położeń; etap połączonych obszarów zamienia maskę w listę obszarów ruchu.¶
5.11.2. Referencje w pamięci i na dysku¶
Podstawowy potok przechowuje ramkę referencyjną w pamięci RAM. Jest to właściwe rozwiązanie, gdy referencja jest przechwytywana w tym uruchomieniu skryptu i musi przetrwać tylko tak długo, jak długo skrypt działa.
W przypadku aplikacji działającej długotrwale – kamery, która powinna wznowić wykrywanie zmian po wyłączeniu i ponownym włączeniu zasilania, lub przerywanego skryptu, który musi wykrywać jakąkolwiek zmianę od pewnego wcześniejszego momentu – ramka referencyjna musi przetrwać dłużej niż działający skrypt. Wzorzec polega na zapisaniu referencji na dysku:
csi0.snapshot().save("/sdcard/reference.bmp")
i wczytaniu jej z powrotem na początku każdego uruchomienia:
reference = image.Image("/sdcard/reference.bmp")
Logika różnicowania nie zmienia się; zmienia się tylko to, gdzie referencja przebywa między przechwyceniami. Kilka udoskonaleń w naturalny sposób rozszerza ten wariant dyskowy – automatyczne ponowne przechwytywanie referencji według licznika czasu (timer), opcjonalne średnie kroczące śledzące powolny dryf oświetlenia – ale podstawienie w centrum pozostaje takie samo.
5.11.3. Izolacja źródła światła¶
Ten sam wzorzec odejmowania pojawia się w nieco innym kontekście: izolowania źródła światła na tle reszty sceny. Sztuczka polega na przechwyceniu referencji „przy zgaszonym świetle” – ramki zarejestrowanej wtedy, gdy to, co ma być wykrywane (beacon IR, piksel ekranu, wskaźnik stanu), nie świeci – i odjęciu tej referencji od każdej kolejnej ramki. W wyniku jasność jest zerowa wszędzie tam, gdzie scena była taka sama na obu przechwyceniach, a niezerowa tylko tam, gdzie źródło światła faktycznie się zaświeciło.
5.11.4. Wybór między difference a sub¶
Praktyczna uwaga dotycząca tego, którą operację arytmetyczną wybrać. difference() zwraca wartość bezwzględną zmiany – bez znaku – co czyni ją czułą na zmianę w dowolnym kierunku (rozjaśnienie lub przyciemnienie) kosztem niemówienia aplikacji, w którym kierunku poszła zmiana. Dla czystego wykrywania ruchu jest to właściwe rozwiązanie: wszystko, co się poruszyło, jest interesujące, niezależnie od tego, w którą stronę przesunęła się jasność.
W przypadku wykrywania źródła światła oświetlony piksel jest zawsze jaśniejszy niż referencja przy zgaszonym świetle, więc sub() (z przycinaniem do zera) jest uczciwszym wyborem. Wszędzie tam, gdzie bieżąca ramka jest ciemniejsza niż referencja (co byłoby szumem sensora wokół nieoświetlonej wartości), wynik przycinany jest do zera zamiast zgłaszać fałszywy sygnał „światło było włączone”.