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() виконує це за один виклик, повертаючи список областей руху, кожна з яких має обмежувальний прямокутник і кількість пікселів, на основі яких застосунок може діяти далі.

A horizontal pipeline diagram. The leftmost two panels are a reference frame and a current frame side by side, with a plus mark between them. An arrow leads from the pair to a third panel labelled difference, in which a few patches are bright against a dark background. An arrow leads from there to a fourth panel showing a binary thresholded version of the difference, with the same patches now solid white. A final arrow leads to a fifth panel showing the binary mask annotated with rectangular bounding boxes drawn around each patch.

Конвеєр різниці кадрів: еталонний кадр і поточний кадр утворюють зображення різниці; порогування перетворює різницю на двійкову маску змінених позицій; крок зв’язаних областей перетворює маску на список областей руху.

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() (з обрізанням до нуля) є більш коректним вибором. Там, де поточний кадр темніший за еталон (що буде шумом датчика навколо неосвітленого значення), відбувається обрізання до нуля замість формування хибного сигналу «підсвітка увімкнена».