5.11. Різниця кадрів¶
Різниця кадрів порівнює кожен новий кадр з раніше збереженим еталонним кадром, щоб знайти частини сцени, які змінилися. Це основний механізм камерних застосунків, що відстежують певні події — захоплення за рухом, сигнали вторгнення, «зберегти відео, коли щось рухається» — і він повністю побудований на попіксельних операціях, розглянутих раніше: абсолютна різниця, поріг і пошук областей, виконані для кожного кадру.
5.11.1. Базовий конвеєр¶
Перший етап полягає в отриманні еталону. На початку роботи — в ідеалі, коли сцена перебуває в стані, що відповідає «відсутності змін» — застосунок захоплює кадр і зберігає його. Цей кадр стає базовою лінією, з якою порівнюватиметься кожне наступне захоплення.
reference = csi0.snapshot().copy()
Виклик .copy() є обов’язковим. csi0.snapshot() сам по собі повертає Image, буфер якого знаходиться у кадровому буфері, де наступний виклик snapshot його перезапише. .copy() виділяє окремий буфер для еталону і дозволяє пікселям цього кадру зберігатися після наступного захоплення.
Другий етап виконується для кожного кадру: захоплюється нове зображення, після чого обчислюється абсолютна різниця між ним та еталоном. Саме це й робить difference():
current = csi0.snapshot()
current.difference(reference)
Після цього виклику current містить зображення, ненульові пікселі якого позначають кожну позицію, де сцена змінилася з моменту захоплення еталону, а величина кожного пікселя пропорційна ступеню зміни в цій позиції.
Третій етап порогує зображення різниці. Необроблена різниця завжди містить певний шум: невеликі зміни яскравості від дробового шуму датчика, зміни градієнта від дрейфу освітлення, субпіксельне тремтіння від незначного руху камери. Прохід порогування — binary() з порогом, встановленим вище цього рівня шуму — залишає лише ті зміни, які достатньо великі, щоб вважатися реальним рухом, відкидаючи решту, і формує двійкове зображення, ненульові пікселі якого є реально зміненими позиціями.
Четвертий етап витягує зв’язані області цієї двійкової маски — групи суміжних ненульових пікселів, що утворюють суцільні плями. find_blobs() виконує це за один виклик, повертаючи список областей руху, кожна з яких має обмежувальний прямокутник і кількість пікселів, на основі яких застосунок може діяти далі.
Конвеєр різниці кадрів: еталонний кадр і поточний кадр утворюють зображення різниці; порогування перетворює різницю на двійкову маску змінених позицій; крок зв’язаних областей перетворює маску на список областей руху.¶
5.11.2. Еталони в пам’яті та на диску¶
Базовий конвеєр зберігає еталонний кадр у RAM. Це правильний підхід, коли еталон захоплюється під час поточного запуску скрипта і має зберігатися лише протягом його виконання.
Для застосунку з тривалим часом роботи — камери, яка має відновити виявлення змін після перезапуску живлення, або переривчастого скрипта, що має виявляти будь-які зміни з якогось більш раннього моменту — еталонний кадр має пережити запущений скрипт. Для цього еталон зберігається на диск:
csi0.snapshot().save("/sdcard/reference.bmp")
і завантажується назад на початку кожного запуску:
reference = image.Image("/sdcard/reference.bmp")
Логіка обчислення різниці не змінюється; змінюється лише місце зберігання еталону між захопленнями. Кілька вдосконалень природно розширюють цей варіант із дисковим сховищем — автоматичне повторне захоплення еталону за таймером, опціональне ковзне середнє для відстеження повільного дрейфу освітлення — але підстановка в центрі залишається незмінною.
5.11.3. Ізоляція джерела світла¶
Той самий патерн віднімання з’являється в дещо іншому контексті: ізоляція джерела світла на тлі решти сцени. Прийом полягає в захопленні еталону «без підсвітки» — кадру, знятого, коли об’єкт виявлення (ІЧ-маяк, піксель екрана, індикатор стану) не освітлений — і відніманні цього еталону від кожного наступного кадру. Результат має нульову яскравість скрізь, де сцена була однаковою в обох захопленнях, і ненульову яскравість лише там, де джерело світла справді засвітилося.
5.11.4. Вибір між difference та sub¶
Практична примітка щодо вибору арифметичної операції. difference() повертає абсолютне значення зміни — без знаку — що робить її чутливою до зміни в будь-якому напрямку (збільшення або зменшення яскравості) ціною відсутності інформації про напрямок зміни для застосунку. Для суто виявлення руху це правильний підхід: все, що рухалося, є цікавим, незалежно від того, в який бік змінилася яскравість.
Для виявлення джерела світла освітлений піксель завжди яскравіший за еталон «без підсвітки», тому sub() (з обрізанням до нуля) є більш коректним вибором. Там, де поточний кадр темніший за еталон (що буде шумом датчика навколо неосвітленого значення), відбувається обрізання до нуля замість формування хибного сигналу «підсвітка увімкнена».