Привет! Начну с того, что я занимаюсь разработкой программы определения автомобильных номеров на дешевом слабомощном процессоре типа Intel ATOM Z8350. Мы получили достаточно хорошие результаты в определении российских номеров на статической картинке (до 97%) с неплохим быстродействием без применения нейронных сетей. Дело осталось за малым — работа с IP-камерой рис 1.

image
рис.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
рис.2 Блок-схема работы с видеофайлом.

Здесь код проекта C++
// 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)


  1. leahch
    23.04.2019 19:55

    Ох, а почему бы не посмотреть в сторону gstreamer? Я как раз с ним работаю. Там такие вещи гораздо проще делаются. Делаете AppSink и принимаете уже готовые декодированные фоеймы прямо с потока. В добавок есть шина сообщений от всех элементов. В общем — рекомендую попробовать.


    1. 2expres Автор
      23.04.2019 21:57

      Основная причина использования FFmpeg — это аппаратное декодирование. О нем я напишу в следующей статье. Декодирование dxva2 на Intel Atom Z8350 + возвращение каждого кадра в оперативную память + вывод на экран средствами WinApi занимает около 15%. Что на мой взгляд не плохо.
      gstreamer — имеет поддержку dxva2? Позволяет он малыми ресурсами переписывать кадр в оперативную память средствами SSE4?
      Я начинал с VLC, он позволяет сделать видеопроигрыватель буквально в 10 строк кода. Загрузка процессора около 5%. Но возврат кадров в оперативную память загружает процессор под 50%. Тогда программный декодер FFmpeg в среднем загружает процессор меньше.
      Причина кадр FullHD в NV12 занимает почти 3 МБайта, т.е. 75 МБайт в секунду нужно переписывать.


      1. al_sh
        24.04.2019 12:10

        А почему на Cherry Trail HD Graphics не qsv, а dxva2? Камень, вроде, позволяет. У меня малинка спокойно RTSP 1920p 25fps декодит по mmal льет и на диск и в текстуру.
        И avcodec_decode_video2 давно deprecated и av_dict_set(&videoOptions, «threads», «auto», 0) хорошо бы на многоядерном камне


        1. 2expres Автор
          24.04.2019 12:32

          Хотел 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 пропал. Разница в цене не принципиальна, размеры не на много больше, ток потребления аналогичен, но вычислительные возможности намного выше.


          1. al_sh
            24.04.2019 12:48

            Аппаратная камера и возможность лить прямо в EGL текстуру на малинке рулят. Под интеловскую графику в линукс я так и не смог получить EGLый контекст, а необходимость memcpy в текстуру, несмотря, даже на dma через PBO сводят разницу в производительности CPU к нулю, что и подтверждает автор


            1. 2expres Автор
              24.04.2019 13:02

              Intel — это тоже позволяет. VLC — это хороший пример этого. Загрузка около 5% для Intel ATOM. Т.е. получение потока RTSP + декодирование DXVA2 + вывод на экран.


              1. al_sh
                24.04.2019 16:20

                понятно, что позволяет. Вопрос в том, что и решение втрое дешевле позволяет добиться аналогичных результатов. Наличие аппаратной камеры на интел я как-то проглядел. Не подскажете, что к чему?


                1. 2expres Автор
                  24.04.2019 16:45

                  Ну не в 3-е дешевле это точно. Я купил за 82 бакса с бесплатной доставкой. Распберри стоит 35+доставка+корпус+блок питания+SD-карта+радиатор. Z83II это все имеет + 2Гб оперативной памяти + Windows10 (бесплатная лицензия на 1 пользователя) + 2 кабеля HDMI (длинный и короткий) + крепление к телевизору/монитору. И мой взгляд для компьютерного зрения Raspberry PI слабоват.


                  1. al_sh
                    26.04.2019 09:50

                    Ну так камера тоже денег стоит. Я имел ввиду, что на основе малинки можно сделать камеру, которая будет решать часть поставленных задач. Цена вопроса 4.5 тр без корпуса


                    1. 2expres Автор
                      26.04.2019 11:40

                      4500 руб это где то 70 баксов (без корпуса)
                      За 10 баксов можно купить приличную вебку и разница явно не в разы:) А серьезную обработку на Raspberry PI real-time не сделать. Ну это мое мнение.


                    1. 2expres Автор
                      26.04.2019 12:08

                      У меня есть камера Raspberry PI 8MP. Но честно нет идей, как ее можно применить, т.к. она коротко фокусная и ее необходимо приближать к объекту.
                      Пробовал на основе нее сделать сканер штрих-кода. Но если нет очень хорошего освещения прочитать штрих-код невозможно. Матрица никакая:(


                      1. al_sh
                        26.04.2019 12:53

                        у меня с трансфокатором и ик подсветкой. Трансфокатор, правда ручной. Делал домой IP камеру на ее основе + малина. Льет по RTSP, пишет на SMB шару по движению. 1920p/25. Реалтайм на любой RTSP смотрелке VLС/FFMPEG/на телефоне tinyCam. Архив — на чем угодно по DLNA хоть на телевизоре, хоть на утюге))


                        1. 2expres Автор
                          26.04.2019 13:01

                          Как Вы подключили объектив?! Родной же приклеен.


                          1. al_sh
                            26.04.2019 13:05

                            1. 2expres Автор
                              26.04.2019 13:13

                              Читал, что 5 Мп матрица еще хуже чем 8Мп. Просто смысл, цена такого решения не низкая, если сравнить с ценой камеры IP Atis, то что на фото 50 баксов. Клиенту — это решение сложно поставить.


                              1. al_sh
                                26.04.2019 13:26

                                Смысл в гибкости. У меня, например, она кидает в телегу, когда дверь открыта. Возможность независимо управлять 2-мя потоками с камеры. Например 1-ый в 1920 отдает в h264 Annex-b непосредственно для RTP, а второй в 640x480 YUV для детектирования движеня ну или, допустим, предварительной обработки для расп. номеров, а потом уже в кодер и, допустим на экран. Все это хозяйство не надо malloc/memcpy, поскольку используются одни и те же буферы. Наличие полноценного EGL, а значит Qt eglfs, без необходимости грузить иксы. Загрузка меньше 4сек.


          1. dimka11
            24.04.2019 15:47

            Разве? На G4560 аппаратное кодирование через quick sync нормально работает, декодирование должно тоже работать. Правда я проверял через handbrake на Windows, не знаю, что он использует.


            1. 2expres Автор
              24.04.2019 15:54

              Графика Intel? Какой драйвер стоит?


      1. leahch
        24.04.2019 12:17

        Да, может, если есть нативные кодеки, то будет их использовать.


        1. al_sh
          24.04.2019 12:31
          +1

          не будет. Надо руками avcodec_find_decoder_by_name(«h264_qsv»)


          1. 2expres Автор
            24.04.2019 12:34

            Подтверждаю надо руками:)


          1. leahch
            25.04.2019 13:46

            Мы точно про gstreamer?! Кстати там есть и нативный avdec_h264.


            1. al_sh
              25.04.2019 17:18

              нет мы про ffmpeg


              1. leahch
                25.04.2019 19:19

                Я говорил про gstreamer в самом начале этой ветки.


  1. 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 у меня работает нестабильно даже по локальной сети


    1. 2expres Автор
      23.04.2019 21:38

      Вы совершенно правы. С помощью этого примера Вы можете выполнить обработку каждого кадра, так как все кадры пройдут через оперативную память. Есть возможность работать реал-тайм.


      1. andreymal
        23.04.2019 21:42

        Лично я просто подаю stdout из этой команды-однострочника в stdin другой программе для обработки — тоже через оперативную память (вывод в файлы отключаем) и тоже реал-тайм) Впрочем, для более сложных задач это уже не очень подойдёт


  1. 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 каждую секунду и не мучать себе мозг.


    1. 2expres Автор
      23.04.2019 23:32

      Мне нужно декодировать каждый кадр, обновлять background, появился большой объект и если да искать номерную пластину с номером. Здесь же учебный пример: видно где получен кадр и можно сделать более серьезный обработчик.
      P.S. автомобиль со скоростью 60 км/ч за секунду проезжает 16,7 м. Если обрабатывать кадр в секунду, то большая часть автомобилей будет проезжать не замеченной.


  1. amaranth
    24.04.2019 08:18

    Мы получили достаточно хорошие результаты в определении российских номеров на статической картинке (до 97%) с неплохим быстродействием без применения нейронных сетей

    Можно поподробней на этом моменте? Какие алгоритмы и библиотеки использовали?


  1. vba
    24.04.2019 10:47

    Мне казалось что обычно используют OpenCV под подобные цели.


    1. 2expres Автор
      24.04.2019 11:16

      OpenCV для работы с RTSP потоком использует FFmpeg. К тому же встроенный в OpenCV FFmpeg без аппаратного ускорения.