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. Эталоны в памяти и на диске¶
Базовый конвейер хранит эталонный кадр в ОЗУ. Это правильное решение, когда эталон захватывается в этом запуске скрипта и должен сохраняться только до тех пор, пока скрипт продолжает работать.
Для долго работающего приложения – камеры, которая должна возобновить обнаружение изменений после перезагрузки питания, периодически запускаемого скрипта, которому нужно обнаружить любое изменение с некоторого более раннего момента, – эталонный кадр должен пережить работающий скрипт. Принцип в том, чтобы сохранить эталон на диск:
csi0.snapshot().save("/sdcard/reference.bmp")
и загрузить его обратно в начале каждого запуска:
reference = image.Image("/sdcard/reference.bmp")
Логика вычисления разности не меняется; меняется только то, где между захватами находится эталон. Несколько усовершенствований естественно расширяют этот дисковый вариант – автоматический повторный захват эталона по таймеру, опциональные скользящие средние для отслеживания медленного дрейфа освещения, – но подстановка в центре остаётся той же.
5.11.3. Изоляция источника света¶
Тот же принцип вычитания проявляется в немного ином контексте: при изоляции источника света на фоне остальной сцены. Приём заключается в том, чтобы захватить эталон «при выключенном свете» – кадр, снятый, когда то, что обнаруживается (ИК-маяк, пиксель экрана, индикатор состояния), не освещено, – и вычитать этот эталон из каждого последующего кадра. В результате яркость равна нулю везде, где сцена была одинаковой в обоих захватах, и ненулевая яркость только там, где источник света действительно загорелся.
5.11.4. Выбор между difference и sub¶
Практическое замечание о том, какую арифметическую операцию выбрать. difference() возвращает абсолютное значение изменения – без знака, – что делает её чувствительной к изменению в любом направлении (увеличению или уменьшению яркости) ценой того, что приложению не сообщается, в каком направлении произошло изменение. Для чистого обнаружения движения это правильный ответ: всё, что переместилось, представляет интерес, независимо от того, в какую сторону сдвинулась яркость.
Для обнаружения источника света освещённый пиксель всегда ярче, чем эталон при выключенном свете, поэтому sub() (с её ограничением на нуле) – более честный выбор. Везде, где текущий кадр темнее эталона (что было бы шумом датчика вокруг неосвещённого значения), результат обрезается до нуля вместо ложного сигнала «свет был включён».