ffmpeg 多个-vf_FFmpeg过滤器实战(3)

首先,阅读本文前,可以参考前面几篇文章。

详细分析FFmpeg过滤器框架

FFmpeg过滤器实战(1)

FFmpeg过滤器实战(2)

ffmpeg已经实现了很多滤波器,这些实现位于于libavfilter⽬录之下,具有一整套机制。

官网地址:http://ffmpeg.org/libavfilter.html

71dd645e09800ca9a4a1607877b8c8fa.png

http://ffmpeg.org/ffmpeg-filters.html

cff9027c2dd8075a4be9e42893e3444d.png

FFmpeg有些常用的fliter,如下:

scale:视频/图像的缩放。

overlay:视频/图像的叠加。

crop:视频/图像的裁剪。

trim:截取视频的⽚段。

rotate:以任意⻆度旋转视频。

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

ffmpeg -filters

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

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

学习filter的使⽤,先需要了解⼀下filter的语法。FFmpeg中filter包含三个层次,filter->filterchain->filtergraph。

具体参考下图:

a6320521619de2ed3effb9d6f5902f7b.png

注意:

第⼀层是 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]的下半部分。

以下就是一个完整的展示。在之前的文章中也有。

83b8bb7fe2faebb73dd0866135b11da1.png

这个我们之前编程实现过。可以⽤以下的命令来实现这个流程。以命令中的分号为界线,代表一个流程。

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

处理结果如下图所示:

e6dc60ae3adfeb621a23fe2fce268396.png

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的参数,是可选的。

使⽤':'字符分隔的⼀个“键=值”对列表。如下所示。

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

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

使⽤':'字符分割的“值”的列表。在这种情况下,键按照声明的顺序被假定为选项名。例如,scale filter的前两个选项分别是w和h,当参数列表为“iw/2:ih/2”时,iw/2的值赋给w,ih/2的值赋给h。如下所示。

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

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

ffmpeg -h filter=filter_name

以下是rotate filter的使⽤⽅式:

可以看出它⽀持slice threading。

Inputs下⾯定义的是输⼊。可以看出rotate filter有⼀个输⼊,格式为Video。

Outputs下⾯定义的是输出。可以看出rotate filter有有⼀个输出,格式为video。

AVOptions下⾯定义了⽀持的参数,后⾯有默认值描述。为了简化输⼊参数,对⻓的参数名提供⼀个简化的名称。⽐如rotate filter中,“angle”的简化名称是“a”。

以下是使⽤到fiter的标签名的⼀个示例:抽取视频Y、U、V分量到不同的⽂件。extractplanes filter指定了三个输出,分别是 [y][u][v],抽取后,将不同的输出保存到不同的⽂件中。

ffmpeg -i input.mp4 -filter_complex "extractplanes=y+u+v[y][u][v]" -map "[y]" input_y.mp4 -map "[u]" input_u.mp4 -map "[v]" input_v.mp4

filterchain的语法

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

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

注意:

(1)由⼀个或多个filter的连接⽽成,filter之间以逗号“,”分隔。

(2)每个filter都连接到序列中的前⼀个filter,即前⼀个filter的输出是后⼀个filter的输⼊。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

filtergraph的语法

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

注意:

(1)由⼀个或多个filter的组合⽽成,filterchain之间⽤分号";"分隔。

(2)filtergraph是连接filter的有向。它可以包含循环,⼀对filter之间可以有多个连接

(3)当在filtergraph中找到两个相同名称的标签时,将创建相应输⼊和输出之间的连接。

(4)如果输出没有被打标签,则就把输出标签连接到filterchain中下一个filter,并且是第一个没有打标签的输入。如下filterchain。

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

解读上面命令:

split filter有两个输出,overlay filter有两个输⼊。split的第⼀个输出标记为“L1”,overlay的第⼀个输⼊pad标记为“L2”。split的第⼆个输出将连接到overlay的第⼆个输⼊。要注意会解读这些命令。

(5)在⼀个filter描述中,如果没有指定第⼀个filter的输⼊标签,则假定为“In”。如果没有指定最后⼀个filter的输出标签,则假定为“out”。

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

解读如下命令:

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

其中有三个filterchain, 分别是:

(7)"split [main][tmp]"。它只有⼀个filter,即 split,它有⼀个默认的输⼊,即INPUT解码后的frame。有两个输出, 以 [main], [tmp] 标识。

"[tmp] crop=iw:ih/2:0:0, vflip [flip]"。它由两个filter组成,crop和vflip,crop的输⼊ 为[tmp],vflip的输出标识为[flip]。

"[main][flip] overlay=0:H/2"。它由⼀个filter组成,即overlay。有两个输⼊,[main]和[flip]。有⼀个默认的输出。

基本结构

下⾯是⼀个过滤器过程的结构,整个过程就是AVFliterGraph控制。

c99d4e96717d6893cf3eb004672edd5c.png

图中简要指示出了滤波所⽤到的各个结构体,各个结构体有如下作⽤:

AVFilterGraph:⽤于管理这整个过滤过程的结构体。

AVFilter:过滤器的的实现是通过AVFilter以及位于其下的结构体/函数来维护。

AVFilterContext:一个过滤器实例,即使是同一个过滤器,但是在进⾏实际的滤波时,也会由于输⼊的参数不同⽽有不同的效果,AVFilterContext就是在实际进⾏处理时,用户维护过滤器相关信息的实体。

AVFilterLink:过滤器链,作⽤主要是⽤于连接相邻的两个AVFilterContext。为了实现⼀个过滤过程,可能会需要多个过滤器协同完成,即⼀个过滤器的输出可能会是另一个滤波器的输入,AVFilterLink的作⽤是串联两个相邻的过滤器实例,形成两个过滤器间的通道。

AVFilterPad:过滤器的输入输出端口,一个过滤器有多个输入以及多个输出端口,相邻过滤器之间是通过AVFilterLink来串联,⽽位于AVFilterLink两端的分别就是前⼀个过滤器的输出端⼝(output pad)以及后⼀个过滤器的输⼊端⼝(input pad)。

buffersrc:⼀个特殊的过滤器,这个过滤器的作⽤就是充当整个滤波过程的⼊⼝,通过调⽤该过滤器提供的函数(如av_buffersrc_add_frame)可以把需要过滤的帧传输进⼊过滤器过程。在创建该过滤器实例的时候需要提供⼀些关于所输⼊的帧的格式的必要参数(如:time_base、图像的宽⾼、图像像素格式等)。

buffersink:⼀个特殊的过滤器,这个过滤器的作⽤就是充当整个过滤过程的出⼝,通过调⽤该过滤器提供的函数(如av_buffersink_get_frame)可以提取出被过滤完成后的帧

创建简单的过滤过程

创建整个滤波过程包含以下步骤:

⾸先需要得到整个过滤过程所需的过滤器(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");

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

filter_graph = avfilter_graph_alloc();

创建⽤于维护过滤相关信息的过滤器实例(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", NULL, NULL, filter_graph);

avfilter_graph_create_filter(&my_video_filter, myfilter, "myfilter",NULL, NULL, filter_graph);

AVFilterLink相邻的两个滤波实例连接起来。

avfilter_link(in_video_filter, 0, my_video_filter, 0);

avfilter_link(my_video_filter, 0, out_video_filter, 0);

关键是提交整个过滤器图:

avfilter_graph_config(filter_graph, NULL);

创建复杂的过滤过程

当过滤器过程复杂到⼀定程度时,即需要多个过滤器进⾏复杂的连接来实现整个过滤过程,这时候对于调⽤者来说,继续采⽤上述⽅法来构建过滤图就显得不够效率。对于复杂的过滤过程,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]

按照上面这种规则,下图已标记了完整的节点,上⾯的过滤过程可以被描绘成以下过滤图:

81ca4439b0ed4e79460f53c0908e3fee.png

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

(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

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

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

filter_graph = avfilter_graph_alloc();

解析字符串,并构建该字符串所描述的过滤图。

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

其中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, NULL, 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_config(filter_graph, NULL);

过滤API

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

//把frame送进过滤器

av_buffersrc_add_frame(c->in_filter, pFrame);

buffersink提供了从过滤过程提取帧的API:av_buffersink_get_frame。可以从指定的buffersink实例提取过滤完成的帧。当av_buffersink_get_frame返回值⼤于0则表示提取成功。

av_buffersink_get_frame(c->out_filter, pFrame);

代码解析:

(1)注册过滤器

ae95dfcba2810850c5b694240781f618.png

(2)自己封装一个函数,根据宽、高、采样格式,来进行适配。

//分配filter_graph

daaee7c983db9f3cd84672fa39e5287d.png

把输入和过滤器命令的参数打印到filter_args里。

70e4fad8201dae667eb8d25208d9d9c6.png

(3)用这个关键函数,去解析这些命令,并加上输入和输出。

3f1c0f0e3b65b927f47f1b7323a8004c.png

(4)提交过滤器配置器

484f9207275c9252c7913137adce83fe.png

(5)解析上面命令中,并使用过滤器配置器指定输入和输出,分别获得输入和输出的AVFilterContext。

31071e7b67c12804308ef62abdf2a65b.png

(6)打开输入和输出文件,获取avfliter相关信息。

e0bd87d24355e99ce24c132b68e5bff8.png

(7)根据传递的参数,分配输入和输出帧。

f29518ecaaddbe4870a8a0e2c175298a.png

(8)读取文件数据,放到frame_in,并从frame_in获取数据,拷贝到输入。从输出获取数据拷贝到frame_out。最后把这些数据拷贝到文件中,可以限制拷贝的帧数。

9d9f975d612b26db56931943b7c02d53.png

(9)关闭输入和输出文件,并释放AVFilterContext产生的内存。

d044515bb006b03f493ec844f7447b1c.png

以上代码,大部分根之前的一样,只是这个init_filters函数不一样,需要改变。实现的是,比较复杂的过滤器规则,能够使用ffmpeg的API去自动解析。附上一张效果图。

c0561c52630348d08e0a6248cdbe2800.png
原文地址:https://www.cnblogs.com/lidabo/p/15397125.html