Компания
45,90
рейтинг
25 декабря 2013 в 13:16

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

В одном из проектов компании 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

Комментарии (15)

  • 0
    Ну хабракат же! Сверните простыню!
  • 0
    Извините, ошибочка с тегами вышла.
  • +1
    насколько мне известно, лицензия приобретена только для него

    думаю что ничего не приобретено
    вопрос лицензирования h264 очень скользкий и темный
    Например, во все последние интегрированные видео карты Intel встроен кодек h264, но в общем случае это не освобождает пользователя(автора ПО, который его использует) от выплат лицензионных отчислений.
    Ссылка на форум Intеl
    т.е. похоже Intel ничего не платит MPEG-LA
    почему тогда производитель RPi должен платить
    • 0
      Сказать платили или нет достоверно нельзя, но разбирательство по этому поводу точно было. Немного об этом можно найти тут: ссылка на официальный сайт разработчиков.
    • 0
      Насколько я понял, они обнаружили, что лицензия на H264 входит в стоимость процессора, а MPEG2, например, не входит, поэтому её приходится приобретать отдельно.
  • +2
    Спасибо. Интересная возможность. Хочу от себя добавить, что работа с переменной частотой кадров, которая обусловлена скоростью обработки видео на каком-то этапе, может существенно ухудшить качество видео. Движения получатся «дергаными». Мне кажется, что лучше выяснить максимальную гарантированную частоту кадров, которую может выдать система (эта частота будет ниже максимально возможной) и работать на ней. При этом иногда аппаратура будет бездействовать, но зато вы получите на выходе качественный видеопоток.
  • 0
    А не подскажите, можно ли таким способом передать видео-поток в сеть? Кодируя аппаратным H264? Если да, то в какую сторону копать. Заранее, спасибо!
    • +1
      Если вы имеете ввиду видео сервер, то это, я думаю, не сложно сделать. Нужно реализовать сишный сервер, который будет слушать сокет и раздавать кодированный буфер подключившимся клиентам. Я в свое время писал такой только для MJPEG видео потока. Будет время сделаю пост как это можно сделать.
      • 0
        Ну конечная задача воспроизвести видео с вебкамеры подключенной к RPi через сеть Wi-Fi в браузере. Делал через mjpg-streamer, картинка отличная, задержка допустимая, но вот трафик очень-очень большой. Да и со звуком не получилось, но это не важно.
    • 0
      Я так понимаю, что то, что вы ищите один из популярнейших вопросов про rpi. У меня подобное заняло пару минут настроить. Погуглите ffmpeg raspberry pi streaming. Вероятно, оно.
      • 0
        Насколько мне известно в FFmpeg нет возможности использовать аппаратное ускорение с использованием OpenMAX, в Gstreamer вроде бы было что-то.
      • 0
        Спасибо! Попробую! Увидел в статье показания CPU всего 2%, так что думаю, что через аппаратный кодер H264 идет.
        • 0
          напиши если получится в каком разрешении и сколько фпс можно выжать максимум?
    • 0
      пример как можно отправлять на сервер видео поток кодируя аппаратным h264: тут
  • 0
    Насколько я успел разобраться (благо, исходники доступны), пока ко мне едет сама плата, ещё есть опция использовать напрямую MMAL — низкоуровневый API, поверх которого работает OpenMAX IL (а не наоборот, как утверждают некоторые товарищи на форумах). Родная утилита raspivid использует именно MMAL.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.

Самое читаемое Дизайн