![image](https://habrastorage.org/webt/il/ds/hv/ildshvj4y94npstenmkw5yle_xg.jpeg)
рис.1 Компьютер Intel ATOM Z83II и IP-камера ATIS
FFmpeg – это библиотека для создания видеоприложений или даже утилит общего назначения, которая берет на себя всю тяжелую работу по обработке видео, выполняя все декодирование, кодирование, мультиплексирование и демультиплексирование для вас.
Задача: Full HD IP-камера в стандарте h.264 передает RTSP поток. Размер распакованного кадра 1920x1080 пикселей, частота 25 кадров в секунду. Нужно получать декодированные кадры в оперативную память и каждый 25 кадр сохранять на диск.
В данном примере мы будем декодировать кадры программно. Цель — научиться использовать FFmpeg и в дальнейшем сравнить результаты, получаемые с помощью аппаратного декодирования. Вы увидите, FFmpeg – это просто!
Установка FFmpeg: многие предлагают собирать FFmpeg под свою аппаратную часть. Я же предлагаю воспользоваться сборками zeranoe, это значительно упрощает задачу. Очень важно, что сборки zeranoe включают поддержку DXVA2, что пригодится нам в дальнейшем для аппаратного декодирования.
Переходим на сайт https://ffmpeg.zeranoe.com/builds/ и качаем 2 архива shared и dev перед этим выбрав 32 или 64 бит. В архиве dev хранятся библиотеки (.lib) и include. Архив shared содержит необходимые .dll которые необходимо будет переписать в папку с вашей будущей программой.
Итак создадим на диске C:\ папку ffmpeg. В нее мы перепишем файлы из архива dev.
Подключение FFmpeg к Visual Studio 2017: создаем новый проект. Заходим в свойства проекта (Проект — свойства ). Далее C/C++ и выбираем «Дополнительные каталоги включаемых файлов». Задаем значение: «C:\ffmpeg\dev\include;». После этого идем в Компоновщик-Дополнительные каталоги библиотек и задаем значение «C:\ffmpeg\dev\lib;». Все. FFmpeg подключен к нашему проекту.
Первый проект с FFmpeg: программное декодирование видео и запись каждого 25 кадра на диск. Принцип работы с видео файлом в FFmpeg представлен в блок-схеме рис.2
![image](https://habrastorage.org/webt/ki/u6/og/kiu6ogj56lb4boxdznsb2ueluwe.png)
рис.2 Блок-схема работы с видеофайлом.
// 21 апреля 2019
// Данный пример, немного исправленный, взят с http://dranger.com/ffmpeg/tutorial01.html
//
#include "pch.h"
extern "C"
{
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
#include <libavformat/avio.h>
#include <libavutil/pixdesc.h>
#include <libavutil/hwcontext.h>
#include <libavutil/opt.h>
#include <libavutil/avassert.h>
#include <libavutil/imgutils.h>
#include <libavutil/motion_vector.h>
#include <libavutil/frame.h>
}
<cut />
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#pragma comment(lib, "avcodec.lib")
#pragma comment(lib, "avformat.lib")
#pragma comment(lib, "swscale.lib")
#pragma comment(lib, "avdevice.lib")
#pragma comment(lib, "avutil.lib")
#pragma comment(lib, "avfilter.lib")
#pragma comment(lib, "postproc.lib")
#pragma comment(lib, "swresample.lib")
#define _CRT_SECURE_NO_WARNINGS
#pragma warning(disable : 4996)
// compatibility with newer API
#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(55,28,1)
#define av_frame_alloc avcodec_alloc_frame
#define av_frame_free avcodec_free_frame
#endif
void SaveFrame(AVFrame *pFrame, int width, int height, int iFrame) {
FILE *pFile;
char szFilename[32];
int y;
// Создаем или открываес файл для записи изображения
sprintf(szFilename, "frame%d.ppm", iFrame);
pFile = fopen(szFilename, "wb");
if (pFile == NULL)
return;
// Записуем заголовок файла
fprintf(pFile, "P6\n%d %d\n255\n", width, height);
// Записуем пиксельные данные
for (y = 0; y < height; y++)
fwrite(pFrame->data[0] + y * pFrame->linesize[0], 1, width * 3, pFile);
// Закрываем файл
fclose(pFile);
}
<cut />
int main(int argc, char *argv[]) {
AVFormatContext *pFormatCtx = NULL;
int i, videoStream;
AVCodecContext *pCodecCtxOrig = NULL;
AVCodecContext *pCodecCtx = NULL;
AVCodec *pCodec = NULL;
AVFrame *pFrame = NULL;
AVFrame *pFrameRGB = NULL;
AVPacket packet;
int frameFinished;
int numBytes;
uint8_t *buffer = NULL;
struct SwsContext *sws_ctx = NULL;
if (argc < 2) {
printf("Please provide a movie file\n");
return -1;
}
// Регистрируем все форматы и кодеки
av_register_all();
// Пробуем открыть видео файл
if (avformat_open_input(&pFormatCtx, argv[1], NULL, NULL) != 0)
return -1; // Не могу открыть файл
// Пробуем получить информацию о потоке
if (avformat_find_stream_info(pFormatCtx, NULL) < 0)
return -1;
// Получаем подробную информацию о файле: продолжительность, битрейд, контейнер и прочее
av_dump_format(pFormatCtx, 0, argv[1], 0);
// Находим первый кард
videoStream = -1;
for (i = 0; i < pFormatCtx->nb_streams; i++)
if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) {
videoStream = i;
break;
}
if (videoStream == -1)
return -1; // Не нашли
<cut />
// Указатель куда будут сохраняться данные
pCodecCtxOrig = pFormatCtx->streams[videoStream]->codec;
// Находим подходящий декодер для файла
pCodec = avcodec_find_decoder(pCodecCtxOrig->codec_id);
if (pCodec == NULL) {
fprintf(stderr, "Unsupported codec!\n");
return -1; // Декодер не найден
}
// Копируем контекст
pCodecCtx = avcodec_alloc_context3(pCodec);
if (avcodec_copy_context(pCodecCtx, pCodecCtxOrig) != 0) {
fprintf(stderr, "Couldn't copy codec context");
return -1; // Ошибка копирования
}
// Открываем кодек
if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0)
return -1; // Не смогли открыть кодек
// Здесь будет храниться кадр
pFrame = av_frame_alloc();
// Здесь храниться кадр преобразованный в RGB
pFrameRGB = av_frame_alloc();
if (pFrameRGB == NULL)
return -1;
// Определяем необходимый размер буфера и выделяем память
numBytes = avpicture_get_size(AV_PIX_FMT_RGB24, pCodecCtx->width,
pCodecCtx->height);
buffer = (uint8_t *)av_malloc(numBytes * sizeof(uint8_t));
// Связуем кадр с вновь выделенным буфером.
avpicture_fill((AVPicture *)pFrameRGB, buffer, AV_PIX_FMT_RGB24,
pCodecCtx->width, pCodecCtx->height);
// Инициализируем SWS context для программного преобразования полученного кадра в RGB
sws_ctx = sws_getContext(pCodecCtx->width,
pCodecCtx->height,
pCodecCtx->pix_fmt,
pCodecCtx->width,
pCodecCtx->height,
AV_PIX_FMT_RGB24,
SWS_BILINEAR,
NULL,
NULL,
NULL
);
<cut />
// Читаем кадры и каждый 25 копируем на диск
i = 0;
while (av_read_frame(pFormatCtx, &packet) >= 0) {
// Это пакет видео потока?
if (packet.stream_index == videoStream) {
// Декодируем видео кадр
avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished, &packet);
// Мы получили видео кадр?
if (frameFinished) {
// Преобразуем кадр в RGB
sws_scale(sws_ctx, (uint8_t const * const *)pFrame->data,
pFrame->linesize, 0, pCodecCtx->height,
pFrameRGB->data, pFrameRGB->linesize);
// Сохраняем кадр на диск
if (++i % 25 == 0) SaveFrame(pFrameRGB, pCodecCtx->width, pCodecCtx->height,i);
}
}
// Освобождаем пакет
av_free_packet(&packet);
}
// Освобождение памяти и закрытие кодеков
av_free(buffer);
av_frame_free(&pFrameRGB);
av_frame_free(&pFrame);
avcodec_close(pCodecCtx);
avcodec_close(pCodecCtxOrig);
avformat_close_input(&pFormatCtx);
return 0;
}
Т.к. у меня IP-камера имеет IP 192.168.1.168, то вызов программы:
decode.exe rtsp://192.168.1.168
Также данный пример умеет декодировать и видеофайлы, достаточно указать место его размещения.
И так, в этом примере мы научились программно декодировать видео файлы и сохранять полученные кадры на диск. Кадры сохраняются в формате .ppm. Для открытия этих файлов вы можете воспользоваться IrfanView 64 или GIMP в Windows.
Вывод: программное декодирование потока RTSP Full HD H.264 занимает до двух ядер Intel ATOM Z8350 к тому же периодически происходит потеря пакетов, из-за чего часть кадров декодировано неправильно. Данный способ более применим для декодирования записанных видео файлов, т. к. не нужна работа в реальном масштабе времени.
В следующей статье я расскажу, как аппаратно декодировать RTSP поток.
Архив с проектом
Работающая программа
Ссылки на материалы по FFmpeg:
1. Учебник по работе с FFmpeg, немного устарел.
2. Разная полезная информация по FFmpeg.
3. Информация по использованию различных библиотек, предоставляемых FFmpeg.
Комментарии (32)
andreymal
23.04.2019 19:56Задача учебная или реальная? Просто если реальная, то это вроде можно сделать командой-однострочником:
ffmpeg -i 'rtsp://user:passwd@192.168.1.168' -vf "select=not(mod(n\,25))" -vsync vfr -f image2 image%03d.ppm
(здесь же можно поиграться с фильтрами и аппаратным декодированием, но мне сейчас лень)
А ещё я обычно дописываю
-rtsp_transport tcp
, потому что udp у меня работает нестабильно даже по локальной сети2expres Автор
23.04.2019 21:38Вы совершенно правы. С помощью этого примера Вы можете выполнить обработку каждого кадра, так как все кадры пройдут через оперативную память. Есть возможность работать реал-тайм.
andreymal
23.04.2019 21:42Лично я просто подаю stdout из этой команды-однострочника в stdin другой программе для обработки — тоже через оперативную память (вывод в файлы отключаем) и тоже реал-тайм) Впрочем, для более сложных задач это уже не очень подойдёт
Nomad1
23.04.2019 23:07Вам надо декодировать только i-frames. Это требует просто мизерных ресурсов, особенно по сравнению с полным декодированием потока и дает устойчивость к потерям P кадров.
Например, так:
ffmpeg -ss <start_time> -i video.mp4 -t <duration> -q:v 2 -vf select="eq(pict_type\,PICT_TYPE_I)" -vsync 0 frame%03d.jpg
P.S. Естественно, в настройках камеры логично поставить, чтобы I кадры шли каждую секунду.
P.P.S. Еще логичнее сказать камере сохранять jpeg snapshots каждую секунду и не мучать себе мозг.2expres Автор
23.04.2019 23:32Мне нужно декодировать каждый кадр, обновлять background, появился большой объект и если да искать номерную пластину с номером. Здесь же учебный пример: видно где получен кадр и можно сделать более серьезный обработчик.
P.S. автомобиль со скоростью 60 км/ч за секунду проезжает 16,7 м. Если обрабатывать кадр в секунду, то большая часть автомобилей будет проезжать не замеченной.
amaranth
24.04.2019 08:18Мы получили достаточно хорошие результаты в определении российских номеров на статической картинке (до 97%) с неплохим быстродействием без применения нейронных сетей
Можно поподробней на этом моменте? Какие алгоритмы и библиотеки использовали?
leahch
Ох, а почему бы не посмотреть в сторону gstreamer? Я как раз с ним работаю. Там такие вещи гораздо проще делаются. Делаете AppSink и принимаете уже готовые декодированные фоеймы прямо с потока. В добавок есть шина сообщений от всех элементов. В общем — рекомендую попробовать.
2expres Автор
Основная причина использования FFmpeg — это аппаратное декодирование. О нем я напишу в следующей статье. Декодирование dxva2 на Intel Atom Z8350 + возвращение каждого кадра в оперативную память + вывод на экран средствами WinApi занимает около 15%. Что на мой взгляд не плохо.
gstreamer — имеет поддержку dxva2? Позволяет он малыми ресурсами переписывать кадр в оперативную память средствами SSE4?
Я начинал с VLC, он позволяет сделать видеопроигрыватель буквально в 10 строк кода. Загрузка процессора около 5%. Но возврат кадров в оперативную память загружает процессор под 50%. Тогда программный декодер FFmpeg в среднем загружает процессор меньше.
Причина кадр FullHD в NV12 занимает почти 3 МБайта, т.е. 75 МБайт в секунду нужно переписывать.
al_sh
А почему на Cherry Trail HD Graphics не qsv, а dxva2? Камень, вроде, позволяет. У меня малинка спокойно RTSP 1920p 25fps декодит по mmal льет и на диск и в текстуру.
И avcodec_decode_video2 давно deprecated и av_dict_set(&videoOptions, «threads», «auto», 0) хорошо бы на многоядерном камне
2expres Автор
Хотел qsv, но ждало — разочарование. QSV Intel поддерживает только с i3 (не ниже 8 gen). Все что ниже Pentium, Celeron и тем более ATOM программно выключено.
В подтверждение своих слов ссылка: https://trac.ffmpeg.org/wiki/Hardware/QuickSync
Там вы найдете фразу: Ensure the target machine has a supported CPU. Current versions only support gen8/gen9 graphics on expensive CPUs («Xeon»/«Core i» branding). The same graphics cores on cheaper CPUs («Pentium»/«Celeron»/«Atom» branding) are explicitly disabled, presumably for commercial reasons.
Т.е. в ATOM отключено по коммерческим причинам.
P.S. После применения Z83II, у меня, интерес к Raspberry PI пропал. Разница в цене не принципиальна, размеры не на много больше, ток потребления аналогичен, но вычислительные возможности намного выше.
al_sh
Аппаратная камера и возможность лить прямо в EGL текстуру на малинке рулят. Под интеловскую графику в линукс я так и не смог получить EGLый контекст, а необходимость memcpy в текстуру, несмотря, даже на dma через PBO сводят разницу в производительности CPU к нулю, что и подтверждает автор
2expres Автор
Intel — это тоже позволяет. VLC — это хороший пример этого. Загрузка около 5% для Intel ATOM. Т.е. получение потока RTSP + декодирование DXVA2 + вывод на экран.
al_sh
понятно, что позволяет. Вопрос в том, что и решение втрое дешевле позволяет добиться аналогичных результатов. Наличие аппаратной камеры на интел я как-то проглядел. Не подскажете, что к чему?
2expres Автор
Ну не в 3-е дешевле это точно. Я купил за 82 бакса с бесплатной доставкой. Распберри стоит 35+доставка+корпус+блок питания+SD-карта+радиатор. Z83II это все имеет + 2Гб оперативной памяти + Windows10 (бесплатная лицензия на 1 пользователя) + 2 кабеля HDMI (длинный и короткий) + крепление к телевизору/монитору. И мой взгляд для компьютерного зрения Raspberry PI слабоват.
al_sh
Ну так камера тоже денег стоит. Я имел ввиду, что на основе малинки можно сделать камеру, которая будет решать часть поставленных задач. Цена вопроса 4.5 тр без корпуса
2expres Автор
4500 руб это где то 70 баксов (без корпуса)
За 10 баксов можно купить приличную вебку и разница явно не в разы:) А серьезную обработку на Raspberry PI real-time не сделать. Ну это мое мнение.
2expres Автор
У меня есть камера Raspberry PI 8MP. Но честно нет идей, как ее можно применить, т.к. она коротко фокусная и ее необходимо приближать к объекту.
Пробовал на основе нее сделать сканер штрих-кода. Но если нет очень хорошего освещения прочитать штрих-код невозможно. Матрица никакая:(
al_sh
у меня с трансфокатором и ик подсветкой. Трансфокатор, правда ручной. Делал домой IP камеру на ее основе + малина. Льет по RTSP, пишет на SMB шару по движению. 1920p/25. Реалтайм на любой RTSP смотрелке VLС/FFMPEG/на телефоне tinyCam. Архив — на чем угодно по DLNA хоть на телевизоре, хоть на утюге))
2expres Автор
Как Вы подключили объектив?! Родной же приклеен.
al_sh
купил готовый onpad.ru/catalog/cubie/raspberrypi/cameras/1991.html
2expres Автор
Читал, что 5 Мп матрица еще хуже чем 8Мп. Просто смысл, цена такого решения не низкая, если сравнить с ценой камеры IP Atis, то что на фото 50 баксов. Клиенту — это решение сложно поставить.
al_sh
Смысл в гибкости. У меня, например, она кидает в телегу, когда дверь открыта. Возможность независимо управлять 2-мя потоками с камеры. Например 1-ый в 1920 отдает в h264 Annex-b непосредственно для RTP, а второй в 640x480 YUV для детектирования движеня ну или, допустим, предварительной обработки для расп. номеров, а потом уже в кодер и, допустим на экран. Все это хозяйство не надо malloc/memcpy, поскольку используются одни и те же буферы. Наличие полноценного EGL, а значит Qt eglfs, без необходимости грузить иксы. Загрузка меньше 4сек.
dimka11
Разве? На G4560 аппаратное кодирование через quick sync нормально работает, декодирование должно тоже работать. Правда я проверял через handbrake на Windows, не знаю, что он использует.
2expres Автор
Графика Intel? Какой драйвер стоит?
leahch
Да, может, если есть нативные кодеки, то будет их использовать.
al_sh
не будет. Надо руками avcodec_find_decoder_by_name(«h264_qsv»)
2expres Автор
Подтверждаю надо руками:)
leahch
Мы точно про gstreamer?! Кстати там есть и нативный avdec_h264.
al_sh
нет мы про ffmpeg
leahch
Я говорил про gstreamer в самом начале этой ветки.