day09-----5-----FFmpeg filter补充之使用复杂的filter过滤视频

其它过滤器文章:
day09-----1-----FFmpeg过滤器框架分析
day09-----2-----视频过滤器实战
day09-----3-----音频过滤器实战(ffmpeg进行混音,将两路音频pcm数据合成一路输出)
day09-----4-----FFmpeg filter补充之使用filter添加水印
day09-----5-----FFmpeg filter补充之使用复杂的filter过滤视频

一 使用复杂的filter过滤视频的流程

本节的功能和day09-----2的功能是一模一样的,只不过9-2使用简单的过滤方法,而本节是复杂的过滤器处理,复杂的过滤处理是指使用字符串处理。

// 1 注册过滤器要用的内容

// 2 初始化,类似简单过滤的过程,只不过使用字符串描述处理。

// 3 开始处理数据
// 3.1 读取一帧yuv420的数据
// 3.2 填充 Y,U,V平面数据
// 3.3 往输入过滤器add帧
// 3.4 从输出过滤器resultsink_ctx中,直接获取处理后的帧数据,filter内部自己处理
// 3.5 写入文件,output Y,U,V

实际上看到,无论用过滤器处理视频还是音频,流程都基本一样的,要想简单的写出过滤代码,重要的是先画好滤波图。

二 代码

关于代码,因为后续需要改动,赖得每次改完后这里还得改,就不贴自己的出来了。当然,我肯定是看自己的打上了注释的代码。

三 过滤效果

这里可以对比看到第一帧的效果,上面是原视频流,下面是经过过滤处理的视频流画面。
day09-----5-----FFmpeg filter补充之使用复杂的filter过滤视频

四 FFmpeg filter的语法等相关知识补充

ffmpeg中有很多已经实现好的滤波器,这些滤波器的实现位于libavfilter⽬录之下,⽤户需要进⾏滤 波时,就是是调⽤这些滤波器来实现的。ffmpeg对于调⽤滤波器有⼀整套的调⽤机制。

1 FFmpeg filter简介

FFmpeg filter提供了很多⾳视频特效处理的功能,⽐如视频缩放、截取、翻转、叠加等。 其中定义了很多的filter,例如以下常⽤的⼀些filter。

  • scale:视频/图像的缩放 overlay:视频/图像的叠加。
  • crop:视频/图像的裁剪。
  • trim:截取视频的⽚段。
  • rotate:以任意⻆度旋转视频。

⽀持的filter的列表可以通过以下命令获得。

ffmpeg -filters

也可以查看⽂档[2],具体某个版本的⽀持情况以命令⾏获取到的结果为准。 以下是filter的⼀个简单的应⽤示例,对视频的宽和⾼减半:

 ffmpeg -i input -vf scale=iw/2:ih/2 output

2 filter的使⽤⽅法

学习filter的使⽤,先需要了解⼀下filter的语法。
FFmpeg中filter包含三个层次,filter->filterchain->filtergraph。
具体参考下图:
注:下图的3个语法是非常重要的,如果不认真看,那么你下面的内容就很难理解。
day09-----5-----FFmpeg filter补充之使用复杂的filter过滤视频

说明:

  • 第⼀层是 filter 的语法。
  • 第⼆层是 filterchain的语法。
  • 第三层是 filtergraph的语法。

filtergraph可以⽤⽂本形式表示,可以作为ffmpeg中的-filter、-vf、-af 和 -filter_complex选项,以及 ffplay 中的-vf、-af和libavfilter/avfilter.h中定义的avfilter_graph_parse2()函数的参数。
为了说明可能的情况,我们看看下⾯的例⼦“把视频的上部分镜像到下半部分”。
处理流程如下:

  1. 使⽤split filter将输⼊流分割为两个流[main]和[temp]。
  2. 其中⼀个流[temp]通过crop filter把下半部分裁剪掉。
  3. 步骤2中的输出再经过vflip filter对视频进⾏和垂直翻转,输出[flip]。
  4. 把步骤3中输出[flip]叠加到[main]的下半部分。 以下整个处理过程的⼀个图示,也就是对filtergraph的⼀个描述[2]。

图形表示,注意带中括号[]的代表自定义的名字,例如[tmp];不带的代表FFmpeg系统过滤器filter,例如split。
day09-----5-----FFmpeg filter补充之使用复杂的filter过滤视频

这个我们之前编程实现过。 可以⽤以下的命令来实现这个流程。

 ffmpeg -i INPUT -vf "split [main][tmp]; [tmp] crop=iw:ih/2:0:0, vflip [flip]; [main][flip] overlay=0:H/2" OUTPUT

处理后就是上面第三点的第二张图片。

对上面命令的每一个选项进行解释:

  • 1)-vf:代表视频过滤器的参数,表示后面必须跟着"xxx"字符串。

  • 2)split:不带[],代表是系统的filter,split是首个接input(即768x320.yuv)的系统filter,那么input是被自动连接在split的。所以根据过滤器的3个语法的filter语句有:
    split [main] [tmp],由于split不用带参数,所以不用加"=",[main]、[tmp]此时代表output_link_label1,output_link_label2。
    注意split与[main]以及[main]与[tmp]之间都不能使用逗号或者其它,只能使用空格,为什么?因为这是filter的语法。
    根据2)的处理后,得到两个output_link_label分支。注意处理完一个完整的内容后,以分号隔开,这是filter的语法3。

  • 3)得到分支后,可能需要一个进行处理,这里使用tmp,即作为input_link_label,并且不是系统自带的filter,所以input_link_label肯定是:[tmp];
    要想裁剪,所以需要使用系统的filter之crop,以filter的第一个语法有,故有:[tmp] crop=iw:ih/2:0:0;
    但是还没处理完,要想镜像反转,需要系统的filter之vflip,以filter的第二个语法Filterchain,它是指:使用多个过滤器处理一个input_link_label时,需要以逗号隔开,所以有:[tmp] crop=iw:ih/2:0:0, vflip;
    由于我们还想继续操作,需要输出output_link_label,所以根据filter的第一个语法有:[tmp] crop=iw:ih/2:0:0, vflip [flip];
    这时候我们就得到了一个flip的output_link_label,它的内容是裁剪过并且镜像反转过的。
    实际上我们应该可以将crop与vflip两个过滤器的操作分开处理,只不过需要经过多一个步骤。

  • 4)因为上面main保存着原画面,而flip保存着镜像反转,并且尺寸是原画面的一半,此时合并即可。可以通过overlay进行,overlay的作用可以保存原画面的任意倍数大小。
    因为有两个input_link_label,并且overlay是需要参数的,同样根据filter的语法1有:[flip] [main] overlay=0:H/2";

  • 5)处理完到想要的步骤后,我们直接写输出的文件名即可。
    注意:ffmpeg使用命令过滤,必须要在-i之前添加-s aaa x bbb的分辨率大小。

好了,看完这5点解释,下面的内容就简单了。

2.1 filter的语法

⽤⼀个字符串描述filter的组成,形式如下:

 [in_link_1]…[in_link_N]filter_name=parameters[out_link_1]…[out_link_M ]

参数说明:

  1. [in_link_N]、[out_link_N]: ⽤来标识输⼊和输出的标签。in_link_N是标签名,标签名可以任意命名,需使⽤⽅括号括起来。在filter_name的前⾯的标签⽤于标识输⼊,在filter_name后⾯的⽤于标识 输出。⼀个filter可以有多个输⼊和多个输出,没有输⼊的filter称为source filter,没有输出的filter称 为sink filter。对输⼊或输出打标签是可选的,打上标签是为了连接其他filter时使⽤。
  2. filter_name: filter的名称。
  3. “=parameters”:包含初始化filter的参数,是可选的。

“=parameters”有以下⼏种形式:

  1. 使⽤’:'字符分隔的⼀个“键=值”对列表。如下所示。
5. 	ffmpeg -i input -vf scale=w=iw/2:h=ih/2 output 
6. 	ffmpeg -i input -vf scale=h=ih/2:w=iw/2 output
  1. 使⽤’:'字符分割的“值”的列表。在这种情况下,键将按照声明的顺序被假定为选项名。
    例如,scale filter 的前两个选项分别是w和h,当参数列表为“iw/2:ih/2”时,iw/2的值赋给w,ih/2的值赋给h。如下所示。
ffmpeg -i input -vf scale=iw/2:ih/2 output
  1. 使⽤’:’ 字符分隔混合的“值”和“键=值”对的列表。
    “值”的位置必须位于“键=值”对之前,并遵循与前⼀点相同的约束顺序。之后的“键=值”对的顺序不受约束。如下所示。
# iw/2与h=ih/2
ffmpeg -i input -vf scale=iw/2:h=ih/2 output

filter类定义了filter的特性以及输⼊和输出的数量,某个filter的使⽤⽅式可以通过以下命令获知。

# 将会列出该filter的相关参数和说明
ffmpeg -h filter=filter_name

也可以查看⽂档[2],但具体某个版本的参数形式以命令⾏获取到的结果为准。

2.2 filterchain的语法

⽤⼀个字符串描述filterchain的组成,形式如下:

"filter1, filter2, ... filterN-1, filterN"

说明:

  1. 由⼀个或多个filter的连接⽽成,filter之间以逗号“,”分隔。
  2. 每个filter都连接到序列中的前⼀个filter,即前⼀个filter的输出是后⼀个filter的输⼊。

⽐如上面的示例中的crop=iw:ih/2:0:0, vflip两个过滤器,crop、vflip在同⼀个filterchain中:

ffmpeg -i INPUT -vf "split [main][tmp]; [tmp] crop=iw:ih/2:0:0, vflip [flip]; [main][flip] overlay=0:H/2" OUTPUT

2.3 filtergraph的语法

⽤⼀个字符串描述filtergraph的组成,形式如下:

"filterchain1;filterchain2;...filterchainN-1;fiterchainN"

说明:

  1. 由⼀个或多个filter的组合⽽成,filterchain之间⽤分号";"分隔。
  2. filtergraph是连接filter的有向图。它可以包含循环,⼀对filter之间可以有多个连接。
  3. 当在filtergraph中找到两个相同名称的标签时,将创建相应输⼊和输出之间的连接。
  4. 如果输出没有被打标签,则默认将其连接到filterchain中下⼀个filter的第⼀个未打标签的输⼊。

解释第4点,例如以下filterchain中:

nullsrc, split[L1], [L2]overlay, nullsink

说明:
split filter有两个输出,overlay filter有两个输⼊。split的第⼀个输出标记为“L1”,overlay的第⼀ 个输⼊pad标记为“L2”。
那么剩下split的第二个输出(假设标记为L3),与overlay的第二个输入(假设标记为L4),那么split的第⼆个输出将连接到overlay的第⼆个输⼊,即L3指向L4。

  1. 在⼀个filter描述中,如果没有指定第⼀个filter的输⼊标签,则假定为“In”。如果没有指定最后⼀个 filter的输出标签,则假定为“out”。
    即我们上面例子的split的输入是input的原因。

  2. 在⼀个完整的filterchain中,所有没有打标签的filter输⼊和输出必须是连接的。如果所有filterchain的 所有filter输⼊和输出pad都是连接的,则认为filtergraph是有效的[2]。

⽐如示例:

ffmpeg -i INPUT -vf "split [main][tmp]; [tmp] crop=iw:ih/2:0:0, vflip [flip]; [main][flip] overlay=0:H/2" OUTPUT

其中有三个filterchain, 分别是:

  1. “split [main][tmp]”。它只有⼀个filter,即 split,它有⼀个默认的输⼊,即INPUT解码后的frame。 有两个输出, 以 [main], [tmp] 标识。
  2. “[tmp] crop=iw:ih/2:0:0, vflip [flip]”。它由两个filter组成,crop和vflip,crop的输⼊ 为[tmp], vflip的输出标识为[flip]。
  3. “[main][flip] overlay=0:H/2”。它由⼀个filter组成,即overlay。有两个输⼊,[main]和[flip]。有⼀ 个默认的输出。

3 基本结构

我们把⼀整个滤波的流程称为滤波过程。
下⾯是⼀个滤波过程的结构:
day09-----5-----FFmpeg filter补充之使用复杂的filter过滤视频

图中简要指示出了滤波所⽤到的各个结构体,实际上使用滤波器基本就是下面的7个结构体,各个结构体有如下作⽤:
day09-----5-----FFmpeg filter补充之使用复杂的filter过滤视频

4 创建简单的滤波过程

创建整个滤波过程包含以下步骤:
1)⾸先需要得到整个滤波过程所需的滤波器(AVFilter),其中buffersrc以及buffersink是作为输⼊以及输出所必须的两个滤波器。

const AVFilter *buffersrc = avfilter_get_by_name("buffer"); 
const AVFilter *buffersink = avfilter_get_by_name("buffersink"); 
const AVFilter *myfilter = avfilter_get_by_name("myfilter");

2)创建统合整个滤波过程的滤波图结构体(AVFilterGraph):

filter_graph = avfilter_graph_alloc();

3)创建⽤于维护滤波相关信息的滤波器实例(AVFilterContext):

AVFilterContext *in_video_filter = NULL; 
AVFilterContext *out_video_filter = NULL; 
AVFilterContext *my_video_filter = NULL; 
avfilter_graph_create_filter(&in_video_filter, buffersrc, "in", args, NULL, filter_graph); 
avfilter_graph_create_filter(&out_video_filter, buffersink, "out", NU LL, NULL, filter_graph); 
avfilter_graph_create_filter(&my_video_filter, myfilter, "myfilter", NULL, NULL, filter_graph);

实际上avfilter_graph_create_filter可能会被其它函数代替。例如复杂的过滤在使用字符串时,会使用avfilter_graph_get_filter获取AVFilterContext。

4)⽤AVFilterLink把相邻的两个滤波实例连接起来:

avfilter_link(in_video_filter, 0, my_video_filter, 0); 
avfilter_link(my_video_filter, 0, out_video_filter, 0);

5)提交整个滤波图:

 avfilter_graph_config(filter_graph, NULL);

上面只是一个大概的流程,具体按实际的需求场景去处理。

5 创建复杂的滤波过程

简单的滤波过程和复杂的滤波过程是不一样的,简单的一般使用avfilter_link进行连接;而对于复杂的滤波过程,使用avfilter_link函数进行连接就会变得繁琐,所以FFmpeg提供了一种使用字符串的方法去描述这种连接。

当滤波过程复杂到⼀定程度时,即需要多个滤波器进⾏复杂的连接来实现整个滤波过程,这时候对于 调⽤者来说,继续采⽤上述⽅法来构建滤波图就显得不够效率。对于复杂的滤波过程,ffmpeg提供了⼀个 更为⽅便的滤波过程创建⽅式。

这种复杂的滤波器过程创建⽅式要求⽤户以字符串的⽅式描述各个滤波器之间的关系。如下是⼀个描 述复杂滤波过程的字符串的例⼦:

[0]trim=start_frame=10:end_frame=20[v0];\ 
[0]trim=start_frame=30:end_frame=40[v1];\ 
[v0][v1]concat=n=2[v2];\ 
[1]hflip[v3];\ 
[v2][v3]overlay=eof_action=repeat[v4];\ 
[v4]drawbox=50:50:120:120:red:t=5[v5]

其实学完上面的第2点后,理解这个例子是非常简单的,甚至不用看下面的8点解释。
上面的例子简单说就是:将[0]输入的10-20s与30-40s重新合成一个视频放在v2。 另一个输入[1]水平镜像后保存到v3。然后v2、v3进行overlay=eof_action=repeat的操作保存到v4。 最后drawbox过滤器操作后保存到v5。

以上是⼀个连续的字符串,为了⽅便分析我们把该字符串进⾏了划分,每⼀⾏都是⼀个滤波器实例, 对于⼀⾏:

  1. 开头是⼀对中括号,中括号内的是输⼊的标识名0。
  2. 中括号后⾯接着的是滤波器名称trim。
  3. 名称后的第⼀个等号后⾯是滤波器参数start_frame=10:end_frame=20,这⾥有两组参数,两组参数 ⽤冒号分开。
  4. 第⼀组参数名称为start_frame,参数值为10,中间⽤等号分开。
  5. 第⼆组参数名称为end_frame,参数值为20,中间⽤等号分开。
  6. 最后也有⼀对中括号,中括号内的是输出的标识名v0。
  7. 如果⼀个滤波实例的输⼊标识名与另⼀个滤波实例的输出标识名相同,则表示这两个滤波实例构成滤波 链。
  8. 如果⼀个滤波实例的输⼊标识名或者输出标识名⼀直没有与其它滤波实例的输出标识名或者输⼊标识名 相同,则表明这些为外部的输⼊输出,通常我们会为其接上buffersrc以及buffersink。

按照这种规则,上⾯的滤波过程可以被描绘成以下滤波图:
day09-----5-----FFmpeg filter补充之使用复杂的filter过滤视频

ffmpeg提供⼀个函数⽤于解析这种字符串:avfilter_graph_parse2。
这个函数会把输⼊的字符串⽣ 成如上⾯的滤波图,不过我们需要⾃⾏⽣成buffersrc以及buffersink的实例,并通过该函数提供的输⼊以 及输出接⼝把buffersrc、buffersink与该滤波图连接起来。
整个流程包含以下步骤:
1)创建统合整个滤波过程的滤波图结构体(AVFilterGraph):

filter_graph = avfilter_graph_alloc();

2)解析字符串,并构建该字符串所描述的滤波图:

avfilter_graph_parse2(filter_graph, graph_desc, &inputs, &outputs);

3)其中inputs与outputs分别为输⼊与输出的接⼝集合,我们需要为这些接⼝接上输⼊以及输出。

for (cur = inputs, i = 0; cur; cur = cur->next, i++) 
{ 
	const AVFilter *buffersrc = avfilter_get_by_name("buffer"); 
	avfilter_graph_create_filter(&filter, buffersrc, name, args, NUL L, filter_graph);
	avfilter_link(filter, 0, cur->filter_ctx, cur->pad_idx);
}
avfilter_inout_free(&inputs);

for (cur = outputs, i = 0; cur; cur = cur->next, i++) { 
	const AVFilter *buffersink = avfilter_get_by_name("buffersink"); 
	avfilter_graph_create_filter(&filter, buffersink, name, NULL, NU LL, filter_graph); 
	avfilter_link(cur->filter_ctx, cur->pad_idx, filter, 0); 
} 
avfilter_inout_free(&outputs);

实际上在操作时并不一定需要循环和使用avfilter_graph_create_filter、avfilter_link。
例如本9-5的代码例子是这样处理的:

// 从解析完字符串的AVFilterGraph中,获取输入输出的AVFilterContext,名字要唯一。
// 这个参2的名字一定是 "Parsed_" + "系统过滤器名字" + "_" + "本次字符串中系统过滤器的序号" 的格式吗?
// 确实是,可以看filter_graph->filters->name变量
mainsrc_ctx = avfilter_graph_get_filter(filter_graph, "Parsed_buffer_0");
if(!mainsrc_ctx) {
    printf("avfilter_graph_get_filter Parsed_buffer_0 failed\n");
    return -1;
}
resultsink_ctx = avfilter_graph_get_filter(filter_graph, "Parsed_buffersink_5");
if(!resultsink_ctx) {
    printf("avfilter_graph_get_filter Parsed_buffersink_5 failed\n");
    return -1;
}
printf("sink_width:%d, sink_height:%d\n", av_buffersink_get_w(resultsink_ctx), av_buffersink_get_h(resultsink_ctx));

4)提交整个滤波图:

avfilter_graph_config(filter_graph, NULL);

6 滤波API

上⾯主要讨论了如何创建滤波过程,不过要进⾏滤波还需要把帧传输进⼊该过程,并在滤波完成后从 该过程中提取出滤波完成的帧。

buffersrc提供了向滤波过程输⼊帧的API:av_buffersrc_add_frame。向指定的buffersrc实例输⼊ 想要进⾏滤波的帧就可以把帧传⼊滤波过程。

av_buffersrc_add_frame(c->in_filter, pFrame);

buffersink提供了从滤波过程提取帧的API:av_buffersink_get_frame。可以从指定的buffersink实 例提取滤波完成的帧。

av_buffersink_get_frame(c->out_filter, pFrame);

当av_buffersink_get_frame返回值⼤于0则表示提取成功。

FFmpeg过滤器的学习到此结束。

上一篇:Qt:QCustomPlot使用教程(二)——基本绘图


下一篇:UIButton图片设置