Pull to refresh

Детекция изменений в сцене и сохранение видеофрагментов в формате h264 на Raspberry Pi без декодирования

Reading time6 min
Views7K


Добрый день. В этой статье я расскажу, далеко не в первый раз, как на 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 это будет полезным.

Tags:
Hubs:
Total votes 18: ↑18 and ↓0+18
Comments18

Articles