本文包含以下内容
1、AVFilter的基本介绍
2、如何利用ffmpeg命令行工具实现各种视频滤镜
3、如何利用libavfilter编程实现在摄像头直播流中加入各类不同滤镜的功能
具有较强的综合性
AVFilter的基本介绍
AVFilter的功能十分强大,可以实现对多媒体数据的各种处理,包括时间线编辑、视音频特效滤镜的添加或信号处理,还可以实现多路媒体流的合并或叠加,其丰富程度令人叹为观止。这里主要以视频滤镜为例进行介绍。使用AVFilter可以为单路视频添加单个或多个滤镜,也可以为多路视频分别添加不同的滤镜并且在最后将多路视频合并为一路视频,AVFilter为实现这些功能定义了以下几个概念:
Filter:代表单个filter
FilterPad:代表一个filter的输入或输出端口,每个filter都可以有多个输入和多个输出,只有输出pad的filter称为source,只有输入pad的filter称为sink
FilterLink:若一个filter的输出pad和另一个filter的输入pad名字相同,即认为两个filter之间建立了link
FilterChain:代表一串相互连接的filters,除了source和sink外,要求每个filter的输入输出pad都有对应的输出和输入pad
FilterGraph:FilterChain的集合
基本和DirectShow类似,也与视频后期调色软件中的节点等概念类似。具体来看,以下面的命令为例
- [in]split[main][tmp];[tmp]crop=iw:ih/2,vflip[flip];[main][flip]overlay=0:H/2[out]
在该命令中,输入流[in]首先被分[split]为两个流[main]和[tmp],然后[tmp]流经过了剪切[crop]和翻转[vflip]两个滤镜后变为[flip],这时我们将[flip]叠加[overlay]到最开始的[main]上形成最后的输出流[out],最后呈现出的是镜像的效果。下图清晰地表示了以上过程
我们可以认为图中每一个节点就是一个Filter,每一个方括号所代表的就是FilterPad,可以看到split的输出pad中有一个叫tmp的,而crop的输入pad中也有一个tmp,由此在二者之间建立了link,当然input和output代表的就是source和sink,此外,图中有三条FilterChain,第一条由input和split组成,第二条由crop和vflip组成,第三条由overlay和output组成,整张图即是一个拥有三个FilterChain的FilterGraph。
上面的图是人工画出来的,也可以在代码中调用avfilter_graph_dump函数自动将FilterGraph画出来,如下
可以看到,多出来了一个scale滤镜,这是由ffmpeg自动添加的用于格式转换的滤镜。
在FFmpeg命令行工具中使用AVFilter
在命令行中使用AVFilter需要遵循专门的语法,简单来说,就是每个Filter之间以逗号分隔,每个Filter自己的属性之间以冒号分隔,属性和Filter以等号相连,多个Filter组成一个FilterChain,每个FilterChain之间以分号相隔。AVFilter在命令行工具中对应的是-vf或-af或-filter_complex,前两个分别对应于单路输入的视频滤镜和音频滤镜,最后的filter_complex则对应于有多路输入的情况。除了在FFMpeg命令行工具中使用外,在FFplay中同样也可以使用AVFilter。其他一些关于单双引号、转义符号等更详细的语法参考Filter Documentation
下面举几个例子
1、叠加水印
- ffmpeg -i test.flv -vf movie=test.jpg[wm];[in][wm]overlay=5:5[out] out.flv
将test.jpg作为水印叠加到test.flv的坐标为(5,5)的位置上,效果如下
2、镜像
- ffmpeg -i test.flv -vf crop=iw/2:ih:0:0,split[left][tmp];[tmp]hflip[right];[left]pad=iw*2[a];[a][right]overlay=w out.flv
输入[in]和输出[out]可以省略不写,pad用于填充画面,效果如下
3、调整曲线
- ffmpeg -i test.flv -vf curves=vintage out.flv
类似Photoshop里面的曲线调整,这里的vintage是ffmpeg自带的预设,实现复古画风,还可以直接加载其他的Photoshop预设文件并在其基础上加以调整,如下
- ffmpeg -i test.flv -vf curves=psfile='test.acv':green='0.45/0.53' out.flv
其中的acv预设文件实现的是加强对比度,再次基础上调整绿色的显示效果,以上两个命令的最终效果如下
4、多路输入拼接
- ffmpeg -i test1.mp4 -i test2.mp4 -i test3.mp4 -i test4.mp4 -filter_complex "[0:v]pad=iw*2:ih*2[a];[a][1:v]overlay=w[b];[b][2:v]overlay=0:h[c];[c][3:v]overlay=w:h" out.mp4
正如前面所说的,当有多个输入时,需要使用filter_complex,效果如下
通过以上几个例子,基本可以明白在命令行中使用AVFilter时需要遵循的语法。
使用libavfilter编程为直播流添加滤镜
要使用libavfilter,首先要注册相关组件
首先需要构造出一个完整可用的FilterGraph,需要用到输入流的解码参数,参见上一篇文章,如下
- AVFilterContext *buffersink_ctx;
- AVFilterContext *buffersrc_ctx;
- AVFilterGraph *filter_graph;
- AVFilter *buffersrc=avfilter_get_by_name("buffer");
- AVFilter *buffersink=avfilter_get_by_name("buffersink");
- AVFilterInOut *outputs = avfilter_inout_alloc();
- AVFilterInOut *inputs = avfilter_inout_alloc();
- filter_graph = avfilter_graph_alloc();
-
-
- snprintf(args, sizeof(args),
- "video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d",
- ifmt_ctx->streams[0]->codec->width, ifmt_ctx->streams[0]->codec->height, ifmt_ctx->streams[0]->codec->pix_fmt,
- ifmt_ctx->streams[0]->time_base.num, ifmt_ctx->streams[0]->time_base.den,
- ifmt_ctx->streams[0]->codec->sample_aspect_ratio.num, ifmt_ctx->streams[0]->codec->sample_aspect_ratio.den);
-
- ret = avfilter_graph_create_filter(&buffersrc_ctx, buffersrc, "in",
- args, NULL, filter_graph);
- if (ret < 0) {
- printf("Cannot create buffer source\n");
- return ret;
- }
-
-
- ret = avfilter_graph_create_filter(&buffersink_ctx, buffersink, "out",
- NULL, NULL, filter_graph);
- if (ret < 0) {
- printf("Cannot create buffer sink\n");
- return ret;
- }
-
-
- outputs->name = av_strdup("in");
- outputs->filter_ctx = buffersrc_ctx;
- outputs->pad_idx = 0;
- outputs->next = NULL;
-
- inputs->name = av_strdup("out");
- inputs->filter_ctx = buffersink_ctx;
- inputs->pad_idx = 0;
- inputs->next = NULL;
-
- if ((ret = avfilter_graph_parse_ptr(filter_graph, filter_descr,
- &inputs, &outputs, NULL)) < 0)
- return ret;
-
- if ((ret = avfilter_graph_config(filter_graph, NULL)) < 0)
- return ret;
-
- avfilter_inout_free(&inputs);
- avfilter_inout_free(&outputs);
上面介绍的是FilterGraph的构造方法之一,即根据filter命令使用avfilter_graph_parse_ptr自动进行构造,当然也可以由我们自己将各个filter一一联接起来,如下,这里假设我们已经有了buffersrc_ctx、 buffersink_ctx和一个filter_ctx
-
- if (err >= 0) err = avfilter_link(buffersrc_ctx, 0, filter_ctx, 0);
- if (err >= 0) err = avfilter_link(filter_ctx, 0, buffersink_ctx, 0);
- if (err < 0) {
- av_log(NULL, AV_LOG_ERROR, "error connecting filters\n");
- return err;
- }
- err = avfilter_graph_config(filter_graph, NULL);
- if (err < 0) {
- av_log(NULL, AV_LOG_ERROR, "error configuring the filter graph\n");
- return err;
- }
- return 0;
不过在filter较多的情况下,还是直接使用avfilter_graph_parse_ptr比较方便
在构造好FilterGraph之后,就可以开始使用了,使用流程也很简单,先将一个AVFrame帧推入FIlterGraph中,在将处理后的AVFrame从FilterGraph中拉出来即可,这里以上一篇文章的编解码核心模块的代码为例看一下实现过程。可以看到,是将解码得到的pFrame推入filter_graph,将处理后的数据写入picref中,他也是一个AVFrame。需要注意的是,这里依然要将picref转换为YUV420的帧之后再进行编码,一方面是因为我们这里用的是摄像头数据,是RGB格式的,另一方面,诸如curves这样的filter是在RGB空间进行处理的,最后得到的也是对应像素格式的帧,所以需要进行转换。其他部分基本和原来一样。
-
- int64_t start_time=av_gettime();
- while (av_read_frame(ifmt_ctx, dec_pkt) >= 0){
- if (exit_thread)
- break;
- av_log(NULL, AV_LOG_DEBUG, "Going to reencode the frame\n");
- pframe = av_frame_alloc();
- if (!pframe) {
- ret = AVERROR(ENOMEM);
- return -1;
- }
-
-
- ret = avcodec_decode_video2(ifmt_ctx->streams[dec_pkt->stream_index]->codec, pframe,
- &dec_got_frame, dec_pkt);
- if (ret < 0) {
- av_frame_free(&pframe);
- av_log(NULL, AV_LOG_ERROR, "Decoding failed\n");
- break;
- }
- if (dec_got_frame){
- #if USEFILTER
- pframe->pts = av_frame_get_best_effort_timestamp(pframe);
-
- if (filter_change)
- apply_filters(ifmt_ctx);
- filter_change = 0;
-
- if (av_buffersrc_add_frame(buffersrc_ctx, pframe) < 0) {
- printf("Error while feeding the filtergraph\n");
- break;
- }
- picref = av_frame_alloc();
-
-
- while (1) {
- ret = av_buffersink_get_frame_flags(buffersink_ctx, picref, 0);
- if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
- break;
- if (ret < 0)
- return ret;
-
- if (picref) {
- img_convert_ctx = sws_getContext(picref->width, picref->height, (AVPixelFormat)picref->format, pCodecCtx->width, pCodecCtx->height, AV_PIX_FMT_YUV420P, SWS_BICUBIC, NULL, NULL, NULL);
- sws_scale(img_convert_ctx, (const uint8_t* const*)picref->data, picref->linesize, 0, pCodecCtx->height, pFrameYUV->data, pFrameYUV->linesize);
- sws_freeContext(img_convert_ctx);
- pFrameYUV->width = picref->width;
- pFrameYUV->height = picref->height;
- pFrameYUV->format = PIX_FMT_YUV420P;
- #else
- sws_scale(img_convert_ctx, (const uint8_t* const*)pframe->data, pframe->linesize, 0, pCodecCtx->height, pFrameYUV->data, pFrameYUV->linesize);
- pFrameYUV->width = pframe->width;
- pFrameYUV->height = pframe->height;
- pFrameYUV->format = PIX_FMT_YUV420P;
- #endif
- enc_pkt.data = NULL;
- enc_pkt.size = 0;
- av_init_packet(&enc_pkt);
- ret = avcodec_encode_video2(pCodecCtx, &enc_pkt, pFrameYUV, &enc_got_frame);
- av_frame_free(&pframe);
- if (enc_got_frame == 1){
-
- framecnt++;
- enc_pkt.stream_index = video_st->index;
-
-
- AVRational time_base = ofmt_ctx->streams[videoindex]->time_base;
- AVRational r_framerate1 = ifmt_ctx->streams[videoindex]->r_frame_rate;
- AVRational time_base_q = { 1, AV_TIME_BASE };
-
- int64_t calc_duration = (double)(AV_TIME_BASE)*(1 / av_q2d(r_framerate1));
-
-
- enc_pkt.pts = av_rescale_q(framecnt*calc_duration, time_base_q, time_base);
- enc_pkt.dts = enc_pkt.pts;
- enc_pkt.duration = av_rescale_q(calc_duration, time_base_q, time_base);
- enc_pkt.pos = -1;
-
-
- int64_t pts_time = av_rescale_q(enc_pkt.dts, time_base, time_base_q);
- int64_t now_time = av_gettime() - start_time;
- if (pts_time > now_time)
- av_usleep(pts_time - now_time);
-
- ret = av_interleaved_write_frame(ofmt_ctx, &enc_pkt);
- av_free_packet(&enc_pkt);
- }
- #if USEFILTER
- av_frame_unref(picref);
- }
- }
- #endif
- }
- else {
- av_frame_free(&pframe);
- }
- av_free_packet(dec_pkt);
- }
这里我们还可以实现一个按下不同的数字键就添加不同的滤镜的功能,如下
可以看到,首先写好一些要用的filter命令,然后在多线程的回调函数里监视用户的按键情况,根据不同的按键使用对应的filter命令初始化filter_graph,这里“null”也是一个filter命令,用于将输入视频原样输出