Raspberry Pi: Кодируем H.264 видео в реальном времени

в 9:16, , рубрики: computer vision, ffmpeg, itseez, Raspberry Pi, Блог компании «Itseez», Компьютерное зрение, Нижний Новгород, обработка видео, Работа с видео, метки: , , , , , ,

В одном из проектов компании Itseez, связанных с компьютерным зрением, мы используем Raspberry Pi для обработки видео потока с веб-камеры, и недавно столкнулись с проблемой записи видео на флеш-карту. Трудность состояла в том, что ресурсы ЦП съедались другими более важными задачами, однако сохранять видео все же было нужно. Причем предпочтений, каким кодеком сжимать и какой формат использовать, не было, лишь бы это никак не сказывалось на fps (количестве кадров в секунду). Перепробовав большое число программных кодеков от RAW до H.264 (использовалась обертка OpenCV над FFmpeg), пришли к выводу, что ничего из этого не выйдет, т.к. при высокой нагрузке fps проседал с 20 до 5 кадров в секунду, при том что картинка – черно-белая с разрешением 320x240. Немного погуглив, выяснили, что в процессоре Raspberry Pi есть аппаратный кодер с поддержкой стандарта H.264 (насколько мне известно, лицензия приобретена только для него). Плюсом ко всему было то, что взаимодействие с кодером реализовано по стандарту OpenMAX, поэтому было решено взяться за написание кода с использованием OpenMAX, и посмотреть, что из этого получится. Получилось, кстати, очень даже недурно!

Ниже пример видео до применения аппаратного ускорения:

.

OpenMAX (Open Media Acceleration) — это кросс-платформенный API, который предоставляет набор средств для аппаратного ускорения обработки видео и аудио и работы с различными мультимедийными системами, разработанный для использования независимо от ОС или аппаратной платформы. Сразу оговорюсь, что на Raspberry Pi реализован не «чистый» OpenMAX IL (Integration Layer) API, а некоторая адаптированная версия для чипа Broadcom. Поэтому попытка переиспользовать код на другой плате может провалиться. К тому же решено было использовать обертку над OpenMAX, предоставленную разработчиками Raspberry Pi — ilcient. В дистрибутиве Raspbian wheezy уже по умолчанию есть готовые библиотеки и примеры использования OpenMAX, которые находятся в каталоге /opt/vc/. В подкаталоге /opt/vc/src/hello_pi/libs/ilclient/ находятся исходники оберток над OpenMAX. Это файлы ilclient.c ilclient.h и ilcore.c.
Вернемся к задаче. Есть изображение с камеры, одноканальное (то есть черно-белое), с разрешением 320х240, в нашем случае это структура IplImage из OpenCV, и нужно сохранить ее в контейнер AVI, предварительно прогнав через кодек Н.264. Отсюда вытекают следующие подзадачи и способы, которыми они решались:

  • Перед кодированием необходимо привести изображение к какой-нибудь цветовой модели, например YUV420p, это будем делать с помощью модуля swscale из набора библиотек FFmpeg версии 0.7.1.
  • Кодируем полученный буфер с помощью OpenMAX, предварительно настроив его так, что входным будет буфер, содержащий изображение в YUV420p, а выходным буфер с изображением, после обработки его кодеком H.264.
  • Сохраняем сжатое изображение в AVI контейнер, используя все тот же FFmpeg.

Итак по пунктам:

Конвертирование

Здесь все просто: создаем контекст конвертирования и две структуры AVPicture. Первая – для одноканального изображения, вторая — для YUV420p:

#define WIDTH 320
#define HEIGHT 240

AVFrame *input_frame = avcodec_alloc_frame();
r = avpicture_alloc((AVPicture *) input_frame,
                    PIX_FMT_GRAY8,
                    WIDTH,
                    HEIGHT);

AVFrame *omx_input_frame = avcodec_alloc_frame();
r = avpicture_alloc((AVPicture *) omx_input_frame,
                    PIX_FMT_YUV420P,
                    WIDTH,
                    HEIGHT);

SwsContext *img_convert_ctx = sws_getContext(WIDTH,
                                             HEIGHT,
                                             PIX_FMT_GRAY8,
                                          WIDTH,
                                          HEIGHT,
                                             PIX_FMT_YUV420P,
                                             SWS_BICUBIC, NULL, NULL, NULL);

Конвертирование соответственно выглядит следующим образом:

avpicture_fill ((AVPicture *) input_frame,
           (uint8_t *) frame->imageData,
                PIX_FMT_GRAY8,
                WIDTH,
                HEIGHT);


buf->nFilledLen = avpicture_fill ((AVPicture *) omx_input_frame,
                                  buf->pBuffer,
                                  PIX_FMT_YUV420P,
                                  WIDTH,
                                  HEIGHT);

sws_scale(img_convert_ctx,
          (const uint8_t* const*)input_frame->data,
          input_frame->linesize,
          0,
          HEIGHT,
          omx_input_frame->data,
          omx_input_frame->linesize);

Где buf – это будет входной буфер кодека, а frame – IplImage* с камеры.

Кодирование

Здесь – посложнее, особенно важно правильно и в нужной последовательности выполнить инициализацию кодера:

OMX_VIDEO_PARAM_PORTFORMATTYPE format;
OMX_PARAM_PORTDEFINITIONTYPE def;
COMPONENT_T *video_encode;
ILCLIENT_T *client;
OMX_BUFFERHEADERTYPE *buf; //входной буфер
OMX_BUFFERHEADERTYPE *out; //выходной буфер
int r = 0;

#define VIDEO_ENCODE_PORT_IN 200
#define VIDEO_ENCODE_PORT_OUT 201
#define BITRATE 400000
#define FPS 25

bcm_host_init();

client = ilclient_init();
OMX_Init();
ilclient_create_component(client, &video_encode, "video_encode", 
                          (ILCLIENT_CREATE_FLAGS_T)(ILCLIENT_DISABLE_ALL_PORTS | 
                          ILCLIENT_ENABLE_INPUT_BUFFERS | 
                          ILCLIENT_ENABLE_OUTPUT_BUFFERS));
memset(&def, 0, sizeof(OMX_PARAM_PORTDEFINITIONTYPE));
def.nSize = sizeof(OMX_PARAM_PORTDEFINITIONTYPE);
def.nVersion.nVersion = OMX_VERSION;
def.nPortIndex = VIDEO_ENCODE_PORT_IN;

OMX_GetParameter(ILC_GET_HANDLE(video_encode), OMX_IndexParamPortDefinition, &def);

def.format.video.nFrameWidth = WIDTH;
def.format.video.nFrameHeight = HEIGHT;
def.format.video.xFramerate = FPS << 16;
def.format.video.nSliceHeight = def.format.video.nFrameHeight;
def.format.video.nStride = def.format.video.nFrameWidth;
def.format.video.eColorFormat = OMX_COLOR_FormatYUV420PackedPlanar;

r = OMX_SetParameter(ILC_GET_HANDLE(video_encode),
                   OMX_IndexParamPortDefinition, 
                     &def);

Здесь происходит создание клиента и установка параметров входного буфера: высоты и ширины изображения, fps и цветовой схемы. Порт 200 — это определенный разработчиками входной порт к драйверу компоненты video_encode, 201 — выходной порт данной компоненты. Для других операций (декодирование видео, кодирование-декодирование аудио и т.п.) соответственно используются другие порты.

memset(&format, 0, sizeof(OMX_VIDEO_PARAM_PORTFORMATTYPE));
format.nSize = sizeof(OMX_VIDEO_PARAM_PORTFORMATTYPE);
format.nVersion.nVersion = OMX_VERSION;
format.nPortIndex = VIDEO_ENCODE_PORT_OUT;
format.eCompressionFormat = OMX_VIDEO_CodingAVC;

r = OMX_SetParameter(ILC_GET_HANDLE(video_encode),
                   OMX_IndexParamVideoPortFormat, 
                     &format);


OMX_VIDEO_PARAM_BITRATETYPE bitrateType;
memset(&bitrateType, 0, sizeof(OMX_VIDEO_PARAM_BITRATETYPE));
bitrateType.nSize = sizeof(OMX_VIDEO_PARAM_PORTFORMATTYPE);
bitrateType.nVersion.nVersion = OMX_VERSION;
bitrateType.eControlRate = OMX_Video_ControlRateVariable;
bitrateType.nTargetBitrate = BITRATE;
bitrateType.nPortIndex = VIDEO_ENCODE_PORT_OUT;
r = OMX_SetParameter(ILC_GET_HANDLE(video_encode),
                     OMX_IndexParamVideoBitrate, &bitrateType);


ilclient_change_component_state(video_encode, OMX_StateIdle);

Выше происходит установка параметров выходного буфера и битрейта. Параметр format.eCompressionFormat = OMX_VIDEO_CodingAVC, как раз определяет то, что изображение будет кодироваться в H.264. Оптимальный битрейт вычислили вручную, как описано здесь: www.ezs3.com/public/What_bitrate_should_I_use_when_encoding_my_video_How_do_I_optimize_my_video_for_the_web.cfm.

ilclient_enable_port_buffers(video_encode, VIDEO_ENCODE_PORT_IN, NULL, NULL, NULL);
  
ilclient_enable_port_buffers(video_encode, VIDEO_ENCODE_PORT_OUT, NULL, NULL, NULL);

ilclient_change_component_state(video_encode, OMX_StateExecuting);

Далее включаем буферы и переводим драйвер в состояние иcполнения.
Собственно, само кодирование:

buf = ilclient_get_input_buffer(video_encode, VIDEO_ENCODE_PORT_IN, 1);
OMX_EmptyThisBuffer(ILC_GET_HANDLE(video_encode), buf);

out = ilclient_get_output_buffer(video_encode, VIDEO_ENCODE_PORT_OUT, 1);
OMX_FillThisBuffer(ILC_GET_HANDLE(video_encode), out);

Сохранение видео

Здесь тоже ничего сложного для тех, кто пользовался FFmpeg. Инициализация контекста выходного формата:

AVCodecContext *cc;
char *out_file_name; //имя файла с расширением .avi
AVOutputFormat *fmt;
AVFormatContext *oc;
AVStream *video_st;

av_register_all();

fmt = av_guess_format(NULL, out_file_name, NULL);

oc = avformat_alloc_context();

oc->debug = 1;
oc->start_time_realtime = AV_NOPTS_VALUE;
oc->start_time = AV_NOPTS_VALUE;
oc->duration = 0;
oc->bit_rate = 0;

oc->oformat = fmt;
snprintf(oc->filename, sizeof(out_file_name), "%s", out_file_name);

video_st = avformat_new_stream(oc, NULL);

cc = video_st->codec;

cc->width = WIDTH;
cc->height = HEIGHT;
cc->codec_id = CODEC_ID_H264;
cc->codec_type = AVMEDIA_TYPE_VIDEO;
cc->bit_rate = BITRATE;
cc->profile = FF_PROFILE_H264_HIGH;
cc->level = 41;
cc->time_base.den = FPS;
cc->time_base.num = 1;

video_st->time_base.den = FPS;
video_st->time_base.num = 1;

video_st->r_frame_rate.num = FPS;
video_st->r_frame_rate.den = 1;

video_st->start_time = AV_NOPTS_VALUE;

cc->sample_aspect_ratio.num = video_st->sample_aspect_ratio.num;
cc->sample_aspect_ratio.den = video_st->sample_aspect_ratio.den;

Далее открываем файл на запись и записываем заголовок и информацию о формате содержимого:

avio_open(&oc->pb, out_file_name, URL_WRONLY);

avformat_write_header(oc, NULL);

if (oc->oformat->flags & AVFMT_GLOBALHEADER)
    oc->streams[0]->codec->flags |= CODEC_FLAG_GLOBAL_HEADER;

av_dump_format(oc, 0, out_file_name, 1);

Процесс сохранения закодированного изображения:

AVPacket pkt;
AVRational omxtimebase = { 1, FPS};
OMX_TICKS tick = out->nTimeStamp;

av_init_packet(&pkt);

pkt.stream_index = video_st->index;
pkt.data= out->pBuffer;
pkt.size= out->nFilledLen;

if (out->nFlags & OMX_BUFFERFLAG_SYNCFRAME)
    pkt.flags |= AV_PKT_FLAG_KEY;

pkt.pts = av_rescale_q(((((uint64_t)tick.nHighPart)<<32) | tick.nLowPart), 
                       omxtimebase,
                       oc->streams[video_st->index]->time_base);

pkt.dts = AV_NOPTS_VALUE;

av_write_frame(oc, &pkt);
out->nFilledLen = 0;

Функция av_rescale_q делает приведение временной метки кодека, к соответствующему временной метке фрейма в контейнере.
Для сборки потребуется подключить следующие заголовочные файлы:

#include "opencv2/core/core_c.h"
#include "opencv2/imgproc/imgproc_c.h"

#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libswscale/swscale.h"
#include "libavutil/opt.h"
#include "libavutil/avutil.h"
#include "libavutil/mathematics.h"
#include "libavformat/avio.h"

#include "bcm_host.h"
#include "ilclient.h"

Соответственно, придется также собрать или установить FFmpeg и OpenCV, хотя ничто не мешает использовать другие библиотеки для сохранения видео в файл. Файлы «bcm_host.h» и «ilclient.h» можно найти в подкаталогах пути /opt/vc/. ilclient.c и ilcore.с, в которых находится код OpenMAX клиента, собираются вместе с проектом.
Для линковки обязательно потребуются следующие библиотеки:

-L/opt/vc/lib -lbcm_host -lopenmaxil -lbcm_host -lvcos -lvchiq_arm –lpthread

Ну и плюс нужно будет указать библиотеки FFmpeg и OpenCV, например, как показано ниже:

-L/usr/local/lib -lavcodec -lavformat -lavutil -lswscale 
-L/usr/local/lib -lopencv_imgproc -lopencv_core

Вот, собственно, и все. Добавлю лишь то, что при использовании встроенного кодера fps нашей системы с включенной функцией сохранения видео и без нее практически не отличаются, при том что ранее при использовании софтварных кодеков fps падал на 40-60%. Убедитесь сами:

Автор: AlexanderKozlov

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js