ffmpeg中有很多已经实现好的滤波器,这些滤波器的实现位于libavfilter⽬录之下,⽤户需要进⾏滤 波时,就是是调⽤这些滤波器来实现的。ffmpeg对于调⽤滤波器有⼀整套的调⽤机制。
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
filter的使⽤⽅法
学习filter的使⽤,先需要了解⼀下filter的语法。 FFmpeg中filter包含三个层次,filter->filterchain->filtergraph。 具体参考下图
说明:
- 第⼀层是 filter 的语法。
- 第⼆层是 filterchain的语法。
- 第三层是 filtergraph的语法。
filtergraph可以⽤⽂本形式表示,可以作为ffmpeg中的-filter/-vf/-af和-filter_complex选项以及 ffplay中的-vf/-af和libavfilter/avfilter.h中定义的avfilter_graph_parse2()函数的参数。 为了说明可能的情况,我们考虑下⾯的例⼦“把视频的上部分镜像到下半部分”。
处理流程如下:
- 使⽤split filter将输⼊流分割为两个流[main]和[temp]。
- 其中⼀个流[temp]通过crop filter把下半部分裁剪掉。
- 步骤2中的输出再经过vflip filter对视频进⾏和垂直翻转,输出[flip]。
- 把步骤3中输出[flip]叠加到[main]的下半部分。
以下整个处理过程的⼀个图示,也就是对filtergraph的⼀个描述[2]。
[main]
input --> split ---------------------> overlay --> output
| ^
|[tmp] [flip]|
+-----> crop --> vflip -------+
这个我们之前编程实现过。
可以⽤以下的命令来实现这个流程。
ffmpeg -i INPUT -vf "split [main][tmp]; [tmp] crop=iw:ih/2:0:0, vflip [flip]; [main][flip] overlay=0:H/2" OUTPUT
处理结果如下图所示。
下⾯具体了解每⼀层的语法,从⽽理解上⾯这个命令的含义
1.filter的语法
⽤⼀个字符串描述filter的组成,形式如下
[in_link_1]…[in_link_N]filter_name=parameters[out_link_1]…[out_link_M ]
参数说明:
- [in_link_N]、[out_link_N]:⽤来标识输⼊和输出的标签。 in_link_N是标签名,标签名可以任意命 名,需使⽤⽅括号括起来。在filter_name的前⾯的标签⽤于标识输⼊,在filter_name后⾯的⽤于标识 输出。⼀个filter可以有多个输⼊和多个输出,没有输⼊的filter称为source filter,没有输出的filter称 为sink filter。 对输⼊或输出打标签是可选的,打上标签是为了连接其他filter时使⽤。
- filter_name:filter的名称。
- “=parameters”:包含初始化filter的参数,是可选的。
“=parameters”有以下⼏种形式
4. 使⽤’:'字符分隔的⼀个“键=值”对列表。如下所示。
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
- 使⽤’:’ 字符分隔混合“值”和“键=值”对的列表。“值”必须位于“键=值”对之前,并遵循与前⼀点相同的 约束顺序。之后的“键=值”对的顺序不受约束。如下所示。
ffmpeg -i input -vf scale=iw/2:h=ih/2 output
filter类定义了filter的特性以及输⼊和输出的数量,某个filter的使⽤⽅式可以通过以下命令获知。
ffmpeg -h filter=filter_name
也可以查看⽂档[2],但具体某个版本的参数形式以命令⾏获取到的结果为准。
是rotate filter的使⽤⽅式
- 它⽀持slice threading。
- Inputs下⾯定义的是输⼊。可以看出rotate filter有⼀个输⼊,格式为Video。
- Outputs下⾯定义的是输出。可以看出rotate filter有有⼀个输出,格式为video。
- AVOptions下⾯定义了⽀持的参数,后⾯有默认值描述。为了简化输⼊参数,对⻓的参数名提供⼀个简 化的名称。⽐如rotate filter中,“angle”的简化名称是“a”。
以下是使⽤到fiter的标签名的⼀个示例:抽取视频Y、U、V分量到不同的⽂件
ffmpeg -i input.mp4 -filter_complex "extractplanes=y+u+v[y][u][v]" -m ap "[y]" input_y.mp4 -map "[u]" input_u.mp4 -map "[v]" input_v.mp4
extractplanes filter指定了三个输出,分别是 [y][u][v],抽取后,将不同的输出保存到不同的⽂件中。
filterchain的语法
⽤⼀个字符串描述filterchain的组成,形式如下
"filter1, filter2, ... filterN-1, filterN"
说明:
- 由⼀个或多个filter的连接⽽成,filter之间以逗号“,”分隔。
- 每个filter都连接到序列中的前⼀个filter,即前⼀个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
示例说明:
crop、vflip在同⼀个filterchain中
filtergraph的语法
⽤⼀个字符串描述filtergraph的组成,形式如下
“filterchain1;filterchain2;…filterchainN-1;fiterchainN”
说明:
- 由⼀个或多个filter的组合⽽成,filterchain之间⽤分号";"分隔。
- filtergraph是连接filter的有向图。它可以包含循环,⼀对filter之间可以有多个连接。
- 当在filtergraph中找到两个相同名称的标签时,将创建相应输⼊和输出之间的连接。
- 如果输出没有被打标签,则默认将其连接到filterchain中下⼀个filter的第⼀个未打标签的输⼊。例如 以下filterchain中。
nullsrc, split[L1], [L2]overlay, nullsink
说明:split filter有两个输出,overlay filter有两个输⼊。split的第⼀个输出标记为“L1”,overlay的第⼀ 个输⼊pad标记为“L2”。split的第⼆个输出将连接到overlay的第⼆个输⼊。
- 在⼀个filter描述中,如果没有指定第⼀个filter的输⼊标签,则假定为“In”。如果没有指定最后⼀个 filter的输出标签,则假定为“out”。
- 在⼀个完整的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, 分别是:
- “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]。有⼀ 个默认的输出。
基本结构
我们把⼀整个滤波的流程称为滤波过程。下⾯是⼀个滤波过程的结构
图中简要指示出了滤波所⽤到的各个结构体,各个结构体有如下作⽤:
结构体 | 作用 |
AVFilterGraph | ⽤于统合这整个滤波过程的结构体。 |
AVFilter | 滤波器,滤波器的实现是通过AVFilter以及位于其下的结构体/函数来维护的。 |
AVFilterContext | ⼀个滤波器实例,即使是同⼀个滤波器,但是在进⾏实际的滤波时,也会由于输⼊ 的参数不同⽽有不同的滤波效果,AVFilterContext就是在实际进⾏滤波时⽤于维护 滤波相关信息的实体。 |
AVFilterLink | 滤波器链,作⽤主要是⽤于连接相邻的两个AVFilterContext。为了实现⼀个滤波过 程,可能会需要多个滤波器协同完成,即⼀个滤波器的输出可能会是另⼀个滤波器 的输⼊,AVFilterLink的作⽤是串联两个相邻的滤波器实例,形成两个滤波器之间的 通道。 |
AVFilterPad | 滤波器的输⼊输出端⼝,⼀个滤波器可以有多个输⼊以及多个输出端⼝,相邻滤波 器之间是通过AVFilterLink来串联的,⽽位于AVFilterLink两端的分别就是前⼀个滤 波器的输出端⼝以及后⼀个滤波器的输⼊端⼝。 |
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", NU LL, 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
以上是⼀个连续的字符串,为了⽅便分析我们把该字符串进⾏了划分,每⼀⾏都是⼀个滤波器实例, 对于⼀⾏:
- 开头是⼀对中括号,中括号内的是输⼊的标识名0。
- 中括号后⾯接着的是滤波器名称trim。
- 名称后的第⼀个等号后⾯是滤波器参数start_frame=10:end_frame=20,这⾥有两组参数,两组参数 ⽤冒号分开。
- 第⼀组参数名称为start_frame,参数值为10,中间⽤等号分开。
- 第⼆组参数名称为end_frame,参数值为20,中间⽤等号分开。
- 最后也有⼀对中括号,中括号内的是输出的标识名v0。
- 如果⼀个滤波实例的输⼊标识名与另⼀个滤波实例的输出标识名相同,则表示这两个滤波实例构成滤波 链。
- 如果⼀个滤波实例的输⼊标识名或者输出标识名⼀直没有与其它滤波实例的输出标识名或者输⼊标识名 相同,则表明这些为外部的输⼊输出,通常我们会为其接上buffersrc以及buffersink。 按照这种规则,上⾯的滤波过程可以被描绘成以下滤波图:
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);
其中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_config(filter_graph, NULL);
滤波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则表示提取成功。
代码
#include <stdio.h>
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavfilter/avfilter.h>
#include <libavfilter/buffersink.h>
#include <libavfilter/buffersrc.h>
#include <libavutil/opt.h>
#include <libavutil/imgutils.h>
AVFilterContext *mainsrc_ctx = NULL;
AVFilterContext *resultsink_ctx = NULL;
AVFilterGraph *filter_graph = NULL;
int init_filters(const int width, const int height, const int format)
{
int ret = 0;
AVFilterInOut *inputs = NULL;
AVFilterInOut *outputs = NULL;
char filter_args[1024] = { 0 };
filter_graph = avfilter_graph_alloc();
if (!filter_graph) {
printf("Error: allocate filter graph failed\n");
return -1;
}
snprintf(filter_args, sizeof(filter_args),
"buffer=video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d[v0];" // Parsed_buffer_0
"[v0]split[main][tmp];" // Parsed_split_1
"[tmp]crop=iw:ih/2:0:0,vflip[flip];" // Parsed_crop_2 Parsed_vflip_3
"[main][flip]overlay=0:H/2[result];" // Parsed_overlay_4
"[result]buffersink", // Parsed_buffersink_5
width, height, format, 1, 25, 1, 1);
// snprintf(filter_args, sizeof(filter_args),
// "buffer=video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d[v0];" // Parsed_buffer_0
// "[v0]split[main][tmp];" // Parsed_split_1
// "[tmp]crop=iw:ih/2:0:0,vflip[flip];" // Parsed_crop_2 Parsed_vflip_3
// "[main]buffersink;" // Parsed_buffersink_4
// "[flip]buffersink", // Parsed_buffersink_5
// width, height, format, 1, 25, 1, 1);
ret = avfilter_graph_parse2(filter_graph, filter_args, &inputs, &outputs);
if (ret < 0) {
printf("Cannot parse graph\n");
return ret;
}
ret = avfilter_graph_config(filter_graph, NULL); // 提交过滤器
if (ret < 0) {
printf("Cannot configure graph\n");
return ret;
}
// Get AVFilterContext from AVFilterGraph parsing from string
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));
return 0;
}
// ffmpeg -i 9.5.flv -vf "split[main][tmp];[tmp]crop=iw:ih/2:0:0,vflip [flip];[main][flip]overlay=0:H/2" -b:v 500k -vcodec libx264 9.5_out.flv
int main(int argc, char* argv)
{
int ret = 0;
int in_width = 768;
int in_height = 320;
avfilter_register_all();
if(init_filters(in_width, in_height, AV_PIX_FMT_YUV420P) < 0) {
printf("init_filters failed\n");
return -1;
}
// input yuv
FILE* inFile = NULL;
const char* inFileName = "768x320.yuv";
fopen_s(&inFile, inFileName, "rb+");
if (!inFile) {
printf("Fail to open file\n");
return -1;
}
// output yuv
FILE* outFile = NULL;
const char* outFileName = "out_crop_vfilter_2.yuv";
fopen_s(&outFile, outFileName, "wb");
if (!outFile) {
printf("Fail to create file for output\n");
return -1;
}
char *graph_str = avfilter_graph_dump(filter_graph, NULL);
FILE* graphFile = NULL;
fopen_s(&graphFile, "graphFile.txt", "w"); // 打印filtergraph的具体情况
fprintf(graphFile, "%s", graph_str);
av_free(graph_str);
AVFrame *frame_in = av_frame_alloc();
unsigned char *frame_buffer_in = (unsigned char *)av_malloc(av_image_get_buffer_size(AV_PIX_FMT_YUV420P, in_width, in_height, 1));
av_image_fill_arrays(frame_in->data, frame_in->linesize, frame_buffer_in,
AV_PIX_FMT_YUV420P, in_width, in_height, 1);
AVFrame *frame_out = av_frame_alloc();
unsigned char *frame_buffer_out = (unsigned char *)av_malloc(av_image_get_buffer_size(AV_PIX_FMT_YUV420P, in_width, in_height, 1));
av_image_fill_arrays(frame_out->data, frame_out->linesize, frame_buffer_out,
AV_PIX_FMT_YUV420P, in_width, in_height, 1);
frame_in->width = in_width;
frame_in->height = in_height;
frame_in->format = AV_PIX_FMT_YUV420P;
uint32_t frame_count = 0;
while (1) {
// 读取yuv数据
if (fread(frame_buffer_in, 1, in_width*in_height * 3 / 2, inFile) != in_width*in_height * 3 / 2) {
break;
}
//input Y,U,V
frame_in->data[0] = frame_buffer_in;
frame_in->data[1] = frame_buffer_in + in_width*in_height;
frame_in->data[2] = frame_buffer_in + in_width*in_height * 5 / 4;
if (av_buffersrc_add_frame(mainsrc_ctx, frame_in) < 0) {
printf("Error while add frame.\n");
break;
}
// filter内部自己处理
/* pull filtered pictures from the filtergraph */
ret = av_buffersink_get_frame(resultsink_ctx, frame_out);
if (ret < 0)
break;
//output Y,U,V
if (frame_out->format == AV_PIX_FMT_YUV420P) {
for (int i = 0; i < frame_out->height; i++) {
fwrite(frame_out->data[0] + frame_out->linesize[0] * i, 1, frame_out->width, outFile);
}
for (int i = 0; i < frame_out->height / 2; i++) {
fwrite(frame_out->data[1] + frame_out->linesize[1] * i, 1, frame_out->width / 2, outFile);
}
for (int i = 0; i < frame_out->height / 2; i++) {
fwrite(frame_out->data[2] + frame_out->linesize[2] * i, 1, frame_out->width / 2, outFile);
}
}
++frame_count;
if(frame_count % 25 == 0)
printf("Process %d frame!\n",frame_count);
av_frame_unref(frame_out);
}
fclose(inFile);
fclose(outFile);
av_frame_free(&frame_in);
av_frame_free(&frame_out);
avfilter_graph_free(&filter_graph); // 内部去释放AVFilterContext产生的内存
printf("finish\n");
return 0;
}