浅析 FFmpeg 滤镜 (filter) 原理

同人作品

原文链接: https://juejin.cn/post/7031848836611964958

1. 什么是滤镜 滤镜(filter)是指将未经过处理的原始音频帧(如PCM)或视频帧(如YUV、RGB)经过滤镜器处理后,得到具体“特殊效果”的音频帧或视频帧,比如音频帧被添加回声、视频帧被旋转、缩放、添加水印等等。需要注意的是,滤镜处理的是原始音视频帧数据,输出的仍然是原始数据,因此不会造成数据损伤。FFmpeg的libavfilter库中提供了很多的内置滤镜,我们可以单独使用一个滤镜进行数据处理,也可以将多个滤镜连接起来组合使用,其中一个滤镜的输出可以连接到另一个滤镜的输入,因此滤镜分为简单滤镜和复杂滤镜。在FFmpeg中,滤镜模块支持多路输入和多路输出,其提供了两种方式使用滤镜,即命令和API,首先我们来看下在命令中定义一个滤镜,语法如下:

代码语言:javascript复制[input_link_lable1][input_link_lable2]... filter_name=parameters [output_link_lable1][output_link_lable12]...

其中,[input_link_lable*]是该滤镜的输入连接符号(link label),可以有多个,表示滤镜的输入;[output_link_lable*]是该滤镜的输出连接符号,可以有多个,表示滤镜的输出;filter_name表示该滤镜的具体名称,如缩放滤镜"scale";parameters表示滤镜参数。

注:如果连接符号没标明,则说明上一个滤镜的输出就是当前滤镜的输入。

1.1 简单滤镜(滤镜链) 简单滤镜通常是指处理的滤镜中包含一个或多个滤镜,当包含多个滤镜时,每个滤镜以逗号分隔构成一个滤镜序列,这样的滤镜序列被称之为滤镜链(filterchain)。语法如下:

代码语言:javascript复制filter1,fiter2,filter3,...,filterN-2,filterN-1,filterN

需要注意的是,滤镜链中如果有空格,需要将滤镜链用双引号括起来,因为命令行中空格是分隔参数用的。举例如下:

代码语言:javascript复制ffmpeg -i src.mp4-vf hqdn3d,pad=2*iw dest.mp4

命令参数说明:

-i src.mp4:指定输入音/视频源;-vf:指定简单视频滤镜,“-vf”等同“-filter:v”,

如果处理音频,该参数应为"-af",且“-af”等同“-filter:a”;"hqdn3d,pad=2*iw":表示包含两个滤镜的滤镜链,其中"hqdn3d"滤镜用于降噪、"pad=2*iw"滤镜用于将图像的宽度填充到输入宽度的2倍;dest.mp4:输出视频,为输入视频经过降噪、填充宽度后的输出结果。效果图:

代码语言:javascript复制 _______ _____________ _______ ________

| | | | | | | |

| input | ---> | hqdn3 | ---> | pad | ---> | output |

|_______| |_____________| |_______| |________|

1.2 复杂滤镜(滤镜图) 复杂滤镜通常是指滤镜图(filter graph),用处简单滤镜处理不了的场合。滤镜图(filterchain)由滤镜链(filterchain)序列组成,滤镜链之间用分号分割,整个滤镜图需要用双引号括起来。语法如下:

代码语言:javascript复制"filter1;fiter2;filter3;...;filterN-2;filterN-1;filterN"

根据输入、输出的数量,滤镜图有可分为简单滤镜图(simple filter graph)和复杂滤镜图(complex filter graph)。其中,简单滤镜图只能处理单路输入流和输出流,且要求输入和输出具有相同的流类型;而复杂滤镜图支持多路输入流和(或)多路输出流,或者输出流与输入流类型不同的场合,比如overlay滤镜和amix滤镜就是复杂滤镜图。

简单滤镜图代码语言:javascript复制 _______ _____________________ ________

| | | | | |

| input | ---> | simple filter graph | ---> | output |

|_______| |_____________________| |________|

复杂滤镜图代码语言:javascript复制_________

| |

| input 0 |\ __________

|_________| \ | |

\ _________ /| output 0 |

\ | | / |__________|

_________ \| complex | /

| | | |/

| input 1 |---->| filter |\

|_________| | | \ __________

/| graph | \ | |

/ | | \| output 1 |

_________ / |_________| |__________|

| | /

| input 2 |/

|_________|

示例如下:代码语言:javascript复制ffmpeg -i INPUT -lavfi "split [main][tmp]; [tmp] crop=iw:ih/2:0:0, vflip [flip]; [main][flip] overlay=0:H/2" OUTPUT

参数说明:

-i:指定输入流;

-lavfi:指定复杂滤镜图,"-lavfi"等价于"-filter_complex";2

"split [main][tmp]; [tmp] crop=iw:ih/2:0:0, vflip [flip]; [main][flip] overlay=0:H/2":复杂滤镜图,由三个滤镜链(用分号分割)组成。第一个滤镜链包含一个split滤镜,该滤镜产生两个输出[main]和[tmp];第二个滤镜链包含crop滤镜和vflip滤镜,输入为[tmp],输出为[flip];第三个滤镜链包含一个overlay滤镜,[main] [flip]作为输入。整行命令实现的功能是:**将输入分隔为两路,其中一路经过裁剪和垂直翻转后,再与另一路混合,生成输出文件。**示意图如下所示:

滤镜、滤镜链、滤镜图区别与联系?

从广义角度,滤镜指图像处理中的一个功能,通常以滤镜链和滤镜图的形式使用;从狭义角度,滤镜指滤镜链的单个特例。滤镜图由滤镜链序列组成,滤镜链由多个滤镜特例序列组成。一个滤镜图可以只包含一个滤镜链,而一个滤镜链也可以只包含一个滤镜特例,这种特例情况下,一个滤镜图仅有单个滤镜特例构成,即滤镜图=滤镜链=单个滤镜。

2. 滤镜API介绍与使用2.1 滤镜API介绍2.1.1 结构体AVFilter:表示一个滤镜器(Filter),位于../libavfilter/avfilter.h。 该结构体用于定义一个滤镜,包含滤镜的名称、

代码语言:javascript复制typedef struct AVFilter {

// 滤镜器名称,非空且唯一

const char *name;

// 滤镜器描述信息,可为NULL

const char *description;

// List of inputs, terminated by a zeroed element

const AVFilterPad *inputs;

// List of outputs, terminated by a zeroed element.

const AVFilterPad *outputs;

const AVClass *priv_class;

// 前缀为AVFILTER_FLAG_*的标志

int flags;

// private API

...

} AVFilter;

AVFilterContext:表示一个滤镜器(Filter)的实例(instance),位于../libavfilter/avfilter.h。代码语言:javascript复制struct AVFilterContext {

const AVClass *av_class;

// 该实例对应的滤镜器(Filter)

const AVFilter *filter;

// 滤镜器实例名称

char *name;

AVFilterPad *input_pads; ///< array of input pads

AVFilterLink **inputs; ///< array of pointers to input links

unsigned nb_inputs; ///< number of input pads

AVFilterPad *output_pads; ///< array of output pads

AVFilterLink **outputs; ///< array of pointers to output links

unsigned nb_outputs; ///< number of output pads

void *priv; ///< private data for use by the filter

// 该滤镜器所属的滤镜图

struct AVFilterGraph *graph; ///< filtergraph this filter belongs to

...

}

AVFilterGraph:表示一个滤镜图,位于../libavfilter/avfilter.h。代码语言:javascript复制typedef struct AVFilterGraph {

const AVClass *av_class;

AVFilterContext **filters;

unsigned nb_filters;

...

}AVFilterGraph;

AVFilterInOut: 表示滤镜图的输入/输出端点,两个滤镜图的连接就是通过端点(AVFilterInout)连接完成的,因此被称为端点数据结构,位于../libavfilter/avfilter.h。AVFilterInOut结构体主要用于avfilter_graph_parse()系列函数。代码语言:javascript复制typedef struct AVFilterInOut {

// 输入或输出端点名称

char *name;

// 指定与该端点关联的滤镜器实例(filter contex)

AVFilterContext *filter_ctx;

// 连接时filt_ctx pad的索引

int pad_idx;

// next input/input in the list,

// NULL if this is the last

struct AVFilterInOut *next;

} AVFilterInOut;

2.1.2 功能函数avfilter_graph_alloc():该函数用于创建一个滤镜图AVFilterGraph,被声明在../libavfilter/avfilter.h头文件中。代码语言:javascript复制/**

* Allocate a filter graph.

*

* @return 如果成功返回一个AVFilterGraph对象,否则返回NULL

*/

AVFilterGraph *avfilter_graph_alloc(void);

avfilter_graph_free:释放滤镜图关联的内存资源代码语言:javascript复制/**

* 释放滤镜图对象,销毁所有滤镜链,将graph置NULL

*/

void avfilter_graph_free(AVFilterGraph **graph);

avfilter_get_by_name:创建一个指定名称的滤镜器AVFilter,位于../libavfilter/avfilter.h。代码语言:javascript复制/**

* 创建指定名称的滤镜器zhiGet a filter definition matching the given name.

*

* @param name 给定的滤镜器名称,比如“buffersrc”、“buffersink”

* @return AVFilter或NULL

*/

const AVFilter *avfilter_get_by_name(const char *name);

avfilter_graph_create_filter:该函数用于创建一个滤镜器的实例AVFilterContext,并将其添加到已存在的滤镜图中,被声明在../libavfilter/avfilter.h头文件中。代码语言:javascript复制/** 为一个滤镜器创建一个实例AVFilterContext,

* 并将其添加在指定的滤镜图中。

*

* @param filt_ctx 指向被创建的滤镜器实例AVFilterContext指针变量

* @param filt 指定的滤镜器

* @param name 被创建的滤镜器实例的名称

* @param graph_ctx 指定滤镜图

* @return <0表示失败,AVERROR

*/

//

int avfilter_graph_create_filter(AVFilterContext **filt_ctx,

const AVFilter *filt,

const char *name,

const char *args, // 通常为NULL

void *opaque, // 通常为NULL

AVFilterGraph *graph_ctx);

avfilter_inout_alloc/avfilter_inout_free:分配/释放一个AVFilterInOut,该结构体可用于表示一个滤镜图的输入/输出端点,被声明在../libavfilter/avfilter.h头文件中。代码语言:javascript复制/**

* 为AVFilterInOut结构体分配内存空间

* 注:使用完毕后需调用avfilter_inout_free()释放

*/

AVFilterInOut *avfilter_inout_alloc(void);

/**

* 释放AVFilterInOut分配的内存,并将*inout置NULL

*/

void avfilter_inout_free(AVFilterInOut **inout);

avfilter_graph_parse_ptr:将一个字符串描述的滤镜图添加到另一个滤镜图中,该函数被声明在../libavfilter/avfilter.h头文件中。代码语言:javascript复制/**

* 将filters描述的滤镜图添加到graph滤镜图中。这两个滤镜图间的连接是利用输入

* 端点AVFilterInOut和输出端点AVFilterInOut连接起来的,输入对输入,输出对输出。

*

* @param graph 目标滤镜图

* @param filters filters描述的滤镜图

* @param inputs 指向滤镜图的输入的链表的指针

* @param outputs 指向滤镜图的输出的链表的指针

* @return <0失败,>=0成功

*/

int avfilter_graph_parse_ptr(AVFilterGraph *graph, const char *filters,

AVFilterInOut **inputs, AVFilterInOut **outputs,

void *log_ctx);

avfilter_graph_config:该函数用于为滤镜图中的所有滤镜建立连接,被声明在../libavfilter/avfilter.h头文件中。代码语言:javascript复制/**

* 检查有效性并为滤镜图中的所有滤镜建立连接

*

* @param graphctx 滤镜图

* @param log_ctx 通常为NULL

* @return >= 0 建立连接成功

*/

int avfilter_graph_config(AVFilterGraph *graphctx, void *log_ctx);

av_buffersrc_add_frame_flags:该函数用于将原始音视频帧发送给滤镜器处理,被声明在../libavfilter/buffersrc.h头文件中。代码语言:javascript复制/**

* 将原始音视频帧发送给滤镜器处理

*

* @param buffer_src 滤镜器实例

* @param frame 原始音视频帧,EOF结束标志为传入NULL时

* @param flags 前缀为AV_BUFFERSRC_FLAG_*标志

* @return >= 0 成功

*/

int av_buffersrc_add_frame_flags(AVFilterContext *buffer_src,

AVFrame *frame, int flags);

av_buffersink_get_frame_flags:从过滤器中读取被处理过的音视频帧,并将其存储到AVFrame中,该函数被声明在../libavfilter/buffersink.h头文件中。代码语言:javascript复制/**

* 读取过滤音视频帧数据,存储到AVFrame中

*

* @param ctx 滤镜器实例

* @param frame AVFrame指针变量,用于存储处理后的帧数据。每次使用需要调用

* av_frame_unref() / av_frame_free()初始化空间

* @param flags 前缀为AV_BUFFERSINK_FLAG_*的标志

* @return >= 0 成功

*/

int av_buffersink_get_frame_flags(AVFilterContext *ctx, AVFrame *frame, int flags);

2.2 滤镜API的使用 FFmpeg中的滤镜使用分为两个步骤:滤镜配置和滤镜使用。滤镜配置目的是创建一个滤镜图并为其创建两个特殊的滤镜作为该滤镜图的输入端和输出端(视频:buffer滤镜和buffersink滤镜;音频:abuffer滤镜和abuffersink滤镜),然后将filters_descr字符串描述的滤镜图插入到上述滤镜图中,当滤镜图中所有的滤镜建立连接后,应用程序通过访问滤镜图的输入端和输出端实现和滤镜图进行数据交互。滤镜使用指的是将原始音视频帧发送到滤镜的输入端,并从滤镜图的输出端读取处理后的音视频帧。假如现有filters_descr字符串描述的视频滤镜图:"split [main][tmp]; [tmp] crop=iw:ih/2:0:0, vflip [flip]; [main][flip] overlay=0:H/2",那么得到的最终滤镜图为:

2.2.1 配置滤镜图创建、配置滤镜图 创建一个滤镜图,并配置滤镜图的输入端和输出端(特殊滤镜)。如果处理的是音频帧,两个特殊滤镜分别为abuffer滤镜和abuffersink滤镜;如果处理的是视频帧,两个特殊滤镜分别为buffer滤镜和buffersink滤镜。

代码语言:javascript复制// 1. 创建滤镜图

AVFilterGraph *filter_graph = avfilter_graph_alloc();

if(! filter_graph) {

RLOG_E("alloc filter graph failed.");

return -1;

}

// 2. 配置滤镜图的输入端,即创建buffer滤镜,然后创建其滤镜实例并命名为"in",

// 并将该滤镜实例添加到之前创建的滤镜图中。需要注意的是,在创建buffer滤镜实例

// AVFilterContext时,要传入创建所需参数args

char args[512];

snprintf(args, sizeof(args),

"video_size=%d%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d",

decode_ctx->width, decode_ctx->height, decode_ctx->pix_fmt,

time_base.num, time_base.den, decode_ctx->sample_aspect_ratio.num,

decode_ctx->sample_aspect_ratio.den);

AVFilterContext *buffer_ctx;

const AVFilter *buffer_filter = avfilter_get_by_name("buffer");

int ret = avfilter_graph_create_filter(&buffer_ctx, // buffer滤镜的实例

buffer_filter, // buffer滤镜

"in", // 滤镜实例命名

args, // 创建buffer滤镜实例所需参数

NULL,

filter_graph); // 被添加的滤镜图

if(ret < 0) {

RLOG_E_("avfilter_graph_create_filter failed,err=%d", ret);

return ret;

}

// 3. 配置滤镜图的输出端,即创建buffersink滤镜,然后创建其滤镜实例并命名为"out",

// 同时将该滤镜实例添加到之前创建的滤镜图中。另外,buffersink滤镜输出有一个输出参数

// 即"pix_fmt",表示输出像素格式,假如后面视频的视频帧由sws_scale进行转换可以不设置

AVFilterContext *buffersink_ctx;

const AVFilter *buffersink_filter = avfilter_get_by_name("buffersink");

ret = avfilter_graph_create_filter(&buffersink_ctx, // 被创建的滤镜实例

buffersink_filter,// buffersink滤镜

"out", // 滤镜实例命名

NULL,

NULL,

filter_graph); // 被添加的滤镜图

if(ret < 0) {

RLOG_E_("avfilter_graph_create_filter failed,err=%d", ret);

return ret;

}

enum AVPixelFormat pix_fmts[] = {AV_PIX_FMT_YUV420P, AV_PIX_FMT_YUYV422, AV_PIX_FMT_NONE};

ret = av_opt_set_int_list(buffersink_ctx, "pix_fmts", pix_fmts,

AV_PIX_FMT_NONE, AV_OPT_SEARCH_CHILDREN);

if(ret < 0) {

RLOG_E_("set output pixel format failed,err=%d", ret);

return ret;

}

这里重点讲下在buffer滤镜和buffersink滤镜在创建滤镜实例时,为什么需要分别传入args参数信息和设定输出像素格式,以及如何知道具体需要传入什么参数?我们在命令行中分别输入ffmpeg -h filter=buffer和ffmpeg -h filter=buffersink命令,得到的信息如下:

代码语言:javascript复制# buffer滤镜帮助信息

$ ffmpeg -h filter=buffer

ffmpeg version 4.0.2 Copyright (c) 2000-2018 the FFmpeg developers

Filter buffer

Buffer video frames, and make them accessible to the filterchain.

Inputs:

none (source filter)

Outputs:

#0: default (video)

buffer AVOptions:

width ..FV..... (from 0 to INT_MAX) (default 0)

video_size ..FV.....

height ..FV..... (from 0 to INT_MAX) (default 0)

pix_fmt ..FV..... (default none)

sar ..FV... sample aspect ratio (from 0 to DBL_MAX) (default 0/1)

pixel_aspect ..FV... sample aspect ratio (from 0 to DBL_MAX) (default 0/1)

time_base ..FV..... (from 0 to DBL_MAX) (default 0/1)

frame_rate ..FV..... (from 0 to DBL_MAX) (default 0/1)

sws_param ..FV.....

# buffer滤镜帮助信息

$ ffmpeg -h filter=buffersink

ffmpeg version 4.0.2 Copyright (c) 2000-2018 the FFmpeg developers

Filter buffersink

Buffer video frames, and make them available to the end of the filter graph.

Inputs:

#0: default (video)

Outputs:

none (sink filter)

buffersink AVOptions:

pix_fmts ..FV..... set the supported pixel formats

首先,我们来看buffer滤镜。buffer滤镜是之前创建的滤镜图的输入节点,它是一个特殊的视频滤镜,用于缓存原始视频帧以供给滤镜图读取。在上述帮助信息中,Inputs和Outputs表示滤镜的输入引脚和输出引脚,由于buffer滤镜是滤镜图的第一个滤镜,因此只有输出引脚而无输入引脚,而buffer AVOptions表示buffer的参数信息,我们在创建buffer滤镜实例时可以选择性指定,但是不能为NULL。然后,再看buffersink滤镜。buffersink滤镜是滤镜图的输出节点,它也是一个特殊的视频滤镜,用于缓存滤镜图处理后的视频帧,由于buffersink滤镜是滤镜图的最后一个滤镜,因此只有输入引脚(Inputs)而无输出引脚(Outputs),另外,buffersink滤镜只有一个参数,即"pix_fmts"用于设置滤镜图输出帧的像素格式列表,这个像素格式有多种,用于限制输出帧格式不超过指定的范围。filter_graph表示的滤镜图示意图如下:

插入filters_descr描述的滤镜到滤镜图代码语言:javascript复制// 4. 构建buffer滤镜的输出端outputs,该端被连接到filters_descr字符串描述滤镜图的

// 第一个滤镜输入端,该滤镜输入端标签默认为"in"

AVFilterInOut *outputs = avfilter_inout_alloc();

if(! outputs) {

RLOG_E("alloc outputs failed");

return -1;

}

outputs->name = av_strdup("in");

outputs->filter_ctx = buffer_ctx;

outputs->pad_idx = 0;

outputs->next = NULL;

// 5.构建buffersink滤镜的输入端inputs,该端被连接到filters_descr字符串描述滤镜图

// 的最后一个滤镜输出端,该滤镜的输出端默认标签为"out"

AVFilterInOut *inputs = avfilter_inout_alloc();

if(! inputs) {

RLOG_E("alloc inputs failed");

return -1;

}

inputs->name = av_strdup("out");

inputs->filter_ctx = buffersink_ctx;

inputs->pad_idx = 0;

inputs->next = NULL;

// 7. 将filters_descr字符串描述滤镜图添加到之前创建的滤镜图中

// filters_descr意思是:缩放后,再旋转

const char *filter_descr = "scale=78:24,transpose=cclock";

ret = avfilter_graph_parse_ptr(filter_graph, filter_descr, &inputs, &outputs, NULL);

if(ret < 0) {

RLOG_E_("add filter_descr graph failed,err=%d", ret);

return ret;

}

在1步过程中,我们创建了一个滤镜图,该滤镜图只含有两个滤镜,即buffer滤镜和buffersink滤镜,其中,buffer滤镜为滤镜图的输入节点,而buffersink滤镜为滤镜图的输出节点。现在有一个filter_descr字符串描述的滤镜图,如果我们希望解码的视频帧经过filter_descr滤镜图处理后输出,那么就需要将这个filter_descr描述的滤镜图添加到之前创建的滤镜图中。也就是说,我们需要将这两个滤镜图连接起来,在FFmpeg中连接两个滤镜图是通过端点/引脚(AVFilterInOut)连接完成的。需要分三步实现:

首先,构建buffer滤镜的输出引脚outputs,该端被连接到filters_descr字符串描述滤镜图的第一个滤镜输入端,该滤镜输入端标签默认为"in";其次,构建buffersink滤镜的输入引脚inputs,该端被连接到filters_descr字符串描述滤镜图的最后一个滤镜输出端,该滤镜的输出端默认标签为"out";最后,调用avfilter_graph_parse_ptr函数将两个滤镜图连接起来。效果如下:

建立滤镜图中滤镜连接 在第2步中,我们已经成功将filters_descr字符串描述的滤镜图插入到filter_graph滤镜图中,但是filters_descr描述的滤镜图中的滤镜是没有连接起来的,因此,FFmpeg提供了avfilter_graph_config函数用于将filters_descr滤镜图中的所有滤镜连接起来。

代码语言:javascript复制// 8. 校验、为所有滤镜建立连

ret = avfilter_graph_config(filter_graph, NULL);

if(ret < 0) {

RLOG_E("configure all the links and formats in the graph failed");

return ret;

}

效果如下:

2.2.2 使用滤镜图 在上一小节中我们谈到,buffer滤镜是建立连接后的滤镜图的第一个滤镜,用于缓存未处理的原始视频帧,而buffersink滤镜是建立连接后的滤镜图的最后一个滤镜,用于缓存滤镜图处理后的视频帧。也就是说,接下来我们就需要将解码后的原始视频帧写入到buffer滤镜缓存区,滤镜图就会从该缓存区读取原始视频帧进行处理,然后再将处理后的视频帧写入到buffersink滤镜的缓存区,我们也就可以从该缓存区获取到最终的视频帧数据。其中,FFmpeg提供了av_buffersrc_add_frame_flags函数和av_buffersink_get_frame_flags函数来完成上述两个操作。具体代码如下:

代码语言:javascript复制// 写buffer滤镜

int send_frame_to_buffer(AVFilterContext *buffer_ctx,AVFrame *inframe) {

if(! buffer_ctx || ! inframe) {

return -1;

}

return av_buffersrc_add_frame_flags(buffer_ctx, inframe, 0)

}

// 读buffersink滤镜

int receiver_frame_from_buffersink(AVFilterContext *buffersink_ctx,

AVFrame *outframe) {

if(! buffersink_ctx || ! outframe) {

return -1;

}

return av_buffersink_get_frame_flags(buffersink_ctx, outframe, 0);

}

效果如下:

-- END --