Добрый день. В этой статье я расскажу, далеко не в первый раз, как на Raspberry Pi 3 и более слабых платформах одновременно детектировать движение и сохранять/транслировать видео в формате H264. Я поделюсь с такими же новичками в мире Raspberry Pi, как и я, о том, что узнал сам за несколько дней, пока разбирался в способах решения задачи. Говорить буду о работе с камерой Raspberry Pi простым человеческим языком.
Я рассмотрю простую задачу "Требуется захватывать с CSI-камеры Raspberry Pi v.1.3 или 2.1 видео разрешением 720p @ 25 FPS, нарезать его в файлы продолжительностью 1 минуту в формате h264, чтобы затем выгружать на удаленный сервер те, в которых обнаружено изменение сцены".
Чтобы не обременять деталями, за которыми ускользнет понимание основных вещей, код буду предоставлять в минимальном виде.
Для продвинутых пользователей, наверняка более полезно будет прочитать статью, однако, если Вы (как и я) в вопросе не очень разбираетесь, лучше начать с этой, где описаны основы подхода.
Большинство статей о работе с камерой RPi, которые вы найдете в интернете, будут использовать OpenCV, если речь идет о том, чтобы что-то в сцене обнаруживать и менять, либо ffmpeg, если требуется делать что-то с видео.
Поскольку я двигался со стороны OpenCV, то я попал в следующую ловушку, которая характеризуется следующей отправной точкой мыслительного процесса: "камера вещает в RGB/MJPEG, как-то из этого надо сделать H264 и положить в MP4...".
Начните читать про камеру Raspberry Pi вот здесь, прежде чем вообще что-то делать, потому что все datasheets-ы от продавцов камер (включая Амперку), которые будут подсовывать вам поисковые системы — это не то пальто, ничего полезного в них нет.
Да, у камеры Raspberry Pi уже есть объектив. Трудно найти информацию о том, какое фокусное расстояние, но я предполагаю, что оно находится в районе 2.8-3.2 мм. Мой коллега распечатал корпус, который можно использовать как для стандартного объектива, так и прилепить любой CS-mount с переменным фокусом, а еще у этого корпуса есть насадка для штатива. Если кому-то надо файл для печати на 3d-принтере, обращайтесь — предоставлю без проблем, на Thingverse не выкладывали.
Не пытайтесь на Raspberry 3 и более слабых платформах кодировать/декодировать видео — они слишком слабы для этого. Не получится выполнить FullHD кодирование даже с OpenMAX.
Для меня следующая мысль была совсем не очевидна откровением: можно с камеры Raspberry Pi брать видео-поток уже кодированный в H264 и просто сохранять его в видео-файл.
В случае FFmpeg это выглядит так:
#!/bin/bash
SS=$1
FPS=$2
WIDTH=1600
HEIGHT=1200
while :
do
ffmpeg -f video4linux2 -input_format h264 -video_size ${WIDTH}x${HEIGHT} \
-framerate $FPS -i /dev/video0 -vcodec copy -map 0 \
-segment_time $SS -f segment \
-strftime 1 -reset_timestamps 1 \
-segment_format mp4 /video/file_%s.mp4
done
Этот код сохраняет куски продолжительностью $SS
и с FPS равным $FPS
в файлы, с именем unix-timestamp. При этом не выполняется никакого перекодирования, только упаковка в MP4. Загрузка CPU в этом случае будет единицы процентов. Далее, эти файлы можно загрузить на FTP.
На камере v2.1 не стоит использовать разрешение 1080p — получите очень странную картинку с обрезкой границ (очень узкий FOV). Используйте разрешение 1600x1200 — это дает прекрасный результат на том же FPS. Кроме этого, файлы, которые будут генерироваться ffmpeg или raspivid (см. далее) будут непропорционально большими. Это особенность реализации данной камеры.
Если хотите работать с бОльшими разрешениями, то, вероятно, потребуется увеличить RAM у GPU Raspberry Pi, иначе словите что-то подобное — я словил.
Если вы берете фрагмент в формате H264, само собой, вы не можете с ним работать без декодирования. В связи с этим многие рецепты как определить наличие движения в сцене сразу перестают быть доступными: не получится на Raspberry Pi 3 одновременно декодировать FullHD поток и определять в нем движение средствами OpenCV в реальном времени, даже просто поток 1280x720 @ 30 FPS декодировать не получится в реальном времени.
Как же тогда осуществлять детекцию движения и одновременно работать с H264? Способ есть — обработка векторов движения H264.
Камера Raspberry Pi умеет возвращать еще и так называемые векторы движения (motion vectors) для потока H264. Эта структура используется кодеком. Однако, она может использоваться и нами для определения присутствия движения в сцене.
Вот в этом примере из репозитория Raspberry Pi вы можете увидеть, что MV определен как:
typedef struct
{
signed char x_vector;
signed char y_vector;
short sad;
} INLINE_MOTION_VECTOR;
где {x,y}_vector — векторы движения в определенном месте кадра, а sad
— предположительно вот это. И sad и vectors могут использоваться для оценки движения в кадре. У меня не дошли руки до sad, я использовал motion vectors.
С помощью FFmpeg добраться до motion vectors не получится, поэтому будем использовать родную утилиту Raspivid:
#!/bin/bash
SS=$1
FPS=$2
W=1600
H=1200
while :
do
raspivid -pts -stm -w $W -h $H -fps $FPS -t 0 \
-sn $(date +%s) -sg $(($SS*1000)) \
-o /video/video_%d.h264 -x /video/mv_%d.txt
done
имя файла, генерируемое raspivid просто увеличивается на +1, то есть, вы не можете использовать timestamp, поэтому придется делать постпроцессинг на основании данных stat о созданном файле.
Эта утилита может использоваться и как источник для FFmpeg и для CVLC, если вы хотете сделать RTSP трансляцию. Вообще, она умеет делать много что хорошего — в частности сохранять motion vectors в файл.
Обратите внимание, если у вас видео "мигает", вероятно, неправильно настроена компенсация частоты мигания лампочек, которая связана с частотой тока электросети. Особо видно это на низкокачественных лампах дневного света. Установка FPS=25 (вместо 30) вкупе с встроенным механизмом подавления мигания, который имеет камера, для нашей электросети 50Hz дает лучший результат, без мигания.
Мигающее видео — это проблема, которая не только визуально ухудшает видео, но и найдет свое отражение в матрице MV, создав кучу векторов, которые ничего кроме шума не представляют. Очень важно решить проблему с миганием. Еще, видео с миганием будет занимать значительно больше места на диске для неподвижных сцен, чем немигающее.
Итак, вышеприведенный фрагмент кода, будучи запущенным на RPi будет создавать два набора файлов — один набор с H264 видео, второй набор с матрицами MV.
Видео, в файлах *.h264 — это сырой поток H264, а не MP4, вы можете проиграть их с помощью VLC, но эти файлы будут проигрываться некорректно, например, ускоренно. В документации по Raspberry Pi предлагается установить gpac:
sudo apt install -y gpac
и упаковывать h264 в mp4 с помощью следующей команды:
time MP4Box -add /video/video_1588645858.h264 /video/video_1588645858.mp4
AVC-H264 import - frame size 1280 x 720 at 25.000 FPS
AVC Import results: 1560 samples - Slices: 26 I 1534 P 0 B - 0 SEI - 26 IDR
Saving to /video/video_1588645858.mp4: 0.500 secs Interleaving
real 0m11.164s
user 0m0.548s
sys 0m0.469s
Минутный фрагмент упаковывается за 11 секунд на Raspberry Pi 3, то есть подходит для использования.
Для работы с MV можно отправной точкой взять следующий тривиальный код:
import os
import sys
import struct
import numpy as np
resolution = [1280, 720]
framelength = int(((resolution[0] + 16) / 16) * (resolution[1] / 16) * 4)
framedata = []
cnt = 0
while True:
data = sys.stdin.read(framelength)
if data == '':
break
cnt += 1
framedata = struct.unpack('>%db' % framelength, data)
vectors = np.reshape(framedata[:(resolution[1]/16 * (resolution[0]/16 + 1) * 4)], (resolution[1]/16, resolution[0]/16 + 1, 4)).astype(np.float32)
vectors = np.multiply(vectors[:, :, 0:1], vectors[:, :, 0:1])
print int(np.sum(vectors))
Он выполнен на примере кода на C, приведенного ранее.
time $(cat /video/mv_1588645871.txt| python proc.py > /dev/null)
real 0m26.288s
user 0m25.005s
sys 0m0.583s
Этот код работает быстро, поскольку вся логика внутри NumPy (то есть на C), но его можно еще ускорить, если обрабатывать, например, 50% кадров, а не все. Есть риск пропустить MV, но движение как правило занимает несколько фреймов, поэтому не страшно.
Гистограмма видео с изменениями в сцене выглядит так, как представлено на следующем изображении:
Помните, что изменения в сцене, это не обязательно движение — выключение или включение света в помещении даст эффект изменения в сцене. В случае инструмента для отбора фрагментов для загрузки — это не проблема. В других случаях может потребоваться существенное усложнение подхода к анализу векторов для отсечения ложных срабатываний.
Определить изменение в сцене можно с помощью следующего скрипта:
cat /video/mv_1588645871.txt | python proc.py | awk '{ if ($1 > 0) print "motion" }' | grep motion > /dev/null
echo $?
0
Возможно, для вашей окружающей среды порог срабатывания потребуется загрубить. Если изменение среды обнаружено, можете загружать файл на FTP, иначе можно просто удалить.
Таким образом, вот и вся наука для работы с кодированным видео и определением "движения в сцене" без декодирования, которую необходимо знать для того, чтобы выполнять все операции на Raspberry Pi без перегрузки ресурсов платформы.
Если вам требуется определять не просто изменение в кадре, но и области движения, вы можете анализировать области матрицы, в которых MV не нулевые. Еще можно обратиться к материалу на Medium, где авторы используют MV из Python.
Статья может показаться банальной для профессионалов, но мне пришлось потратить существенное время для того, чтобы разобраться с этими вещами. Надеюсь, что новичкам, начинающим работать с видео на платформе Raspberry Pi это будет полезным.