本文为作者原创,转载请注明出处:https://www.cnblogs.com/leisure_chn/p/10429145.html

在 FFmpeg 中,滤镜(filter)处理的是未压缩的原始音视频数据(RGB/YUV视频帧,PCM音频帧等)。一个滤镜的输出可以连接到另一个滤镜的输入,多个滤镜可以连接起来,构成滤镜链/滤镜图,各种滤镜的组合为 FFmpeg 提供了丰富的音视频处理功能。

比较常用的滤镜有:scale、trim、overlay、rotate、movie、yadif。scale 滤镜用于缩放,trim 滤镜用于帧级剪切,overlay 滤镜用于视频叠加,rotate 滤镜实现旋转,movie 滤镜可以加载第三方的视频,yadif 滤镜可以去隔行。

本文将通过实例详细介绍滤镜 API 的使用方法。

1. 滤镜的构成及命令行用法

参考 “FFmpeg使用基础” 第 4 节 “滤镜”。

2. 滤镜数据结构与API简介

待补充

2.1 struct AVFilter

/**
* Filter definition. This defines the pads a filter contains, and all the
* callback functions used to interact with the filter.
*/
typedef struct AVFilter {
const char *name;
const char *description;
const AVFilterPad *inputs;
const AVFilterPad *outputs;
const AVClass *priv_class;
int flags; // private API
......
} AVFilter;

2.2 struct AVFilterContext

/** An instance of a filter */
struct AVFilterContext {
const AVClass *av_class; ///< needed for av_log() and filters common options const AVFilter *filter; ///< the AVFilter of which this is an instance char *name; ///< name of this filter instance 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 ......
};

2.3 struct AVFilterGraph

typedef struct AVFilterGraph {
const AVClass *av_class;
AVFilterContext **filters;
unsigned nb_filters; ......
} AVFilterGraph;

2.4 struct AVFilterLink

/**
* A link between two filters. This contains pointers to the source and
* destination filters between which this link exists, and the indexes of
* the pads involved. In addition, this link also contains the parameters
* which have been negotiated and agreed upon between the filter, such as
* image dimensions, format, etc.
*
* Applications must not normally access the link structure directly.
* Use the buffersrc and buffersink API instead.
* In the future, access to the header may be reserved for filters
* implementation.
*/
struct AVFilterLink {
AVFilterContext *src; ///< source filter
AVFilterPad *srcpad; ///< output pad on the source filter AVFilterContext *dst; ///< dest filter
AVFilterPad *dstpad; ///< input pad on the dest filter ......
}

2.5 struct AVFilterInOut

/**
* A linked-list of the inputs/outputs of the filter chain.
*
* This is mainly useful for avfilter_graph_parse() / avfilter_graph_parse2(),
* where it is used to communicate open (unlinked) inputs and outputs from and
* to the caller.
* This struct specifies, per each not connected pad contained in the graph, the
* filter context and the pad index required for establishing a link.
*/
typedef struct AVFilterInOut {
/** unique name for this input/output in the list */
char *name; /** filter context associated to this input/output */
AVFilterContext *filter_ctx; /** index of the filt_ctx pad to use for linking */
int pad_idx; /** next input/input in the list, NULL if this is the last */
struct AVFilterInOut *next;
} AVFilterInOut;

2.6 avfilter_graph_create_filter()

/**
* Create and add a filter instance into an existing graph.
* The filter instance is created from the filter filt and inited
* with the parameters args and opaque.
*
* In case of success put in *filt_ctx the pointer to the created
* filter instance, otherwise set *filt_ctx to NULL.
*
* @param name the instance name to give to the created filter instance
* @param graph_ctx the filter graph
* @return a negative AVERROR error code in case of failure, a non
* negative value otherwise
*/
int avfilter_graph_create_filter(AVFilterContext **filt_ctx, const AVFilter *filt,
const char *name, const char *args, void *opaque,
AVFilterGraph *graph_ctx);

2.7 avfilter_graph_parse_ptr()

/**
* Add a graph described by a string to a graph.
*
* In the graph filters description, if the input label of the first
* filter is not specified, "in" is assumed; if the output label of
* the last filter is not specified, "out" is assumed.
*
* @param graph the filter graph where to link the parsed graph context
* @param filters string to be parsed
* @param inputs pointer to a linked list to the inputs of the graph, may be NULL.
* If non-NULL, *inputs is updated to contain the list of open inputs
* after the parsing, should be freed with avfilter_inout_free().
* @param outputs pointer to a linked list to the outputs of the graph, may be NULL.
* If non-NULL, *outputs is updated to contain the list of open outputs
* after the parsing, should be freed with avfilter_inout_free().
* @return non negative on success, a negative AVERROR code on error
*/
int avfilter_graph_parse_ptr(AVFilterGraph *graph, const char *filters,
AVFilterInOut **inputs, AVFilterInOut **outputs,
void *log_ctx);

2.8 avfilter_graph_config()

/**
* Check validity and configure all the links and formats in the graph.
*
* @param graphctx the filter graph
* @param log_ctx context used for logging
* @return >= 0 in case of success, a negative AVERROR code otherwise
*/
int avfilter_graph_config(AVFilterGraph *graphctx, void *log_ctx);

2.9 av_buffersrc_add_frame_flags()

/**
* Add a frame to the buffer source.
*
* By default, if the frame is reference-counted, this function will take
* ownership of the reference(s) and reset the frame. This can be controlled
* using the flags.
*
* If this function returns an error, the input frame is not touched.
*
* @param buffer_src pointer to a buffer source context
* @param frame a frame, or NULL to mark EOF
* @param flags a combination of AV_BUFFERSRC_FLAG_*
* @return >= 0 in case of success, a negative AVERROR code
* in case of failure
*/
av_warn_unused_result
int av_buffersrc_add_frame_flags(AVFilterContext *buffer_src,
AVFrame *frame, int flags);

2.10 av_buffersink_get_frame()

/**
* Get a frame with filtered data from sink and put it in frame.
*
* @param ctx pointer to a context of a buffersink or abuffersink AVFilter.
* @param frame pointer to an allocated frame that will be filled with data.
* The data must be freed using av_frame_unref() / av_frame_free()
*
* @return
* - >= 0 if a frame was successfully returned.
* - AVERROR(EAGAIN) if no frames are available at this point; more
* input frames must be added to the filtergraph to get more output.
* - AVERROR_EOF if there will be no more output frames on this sink.
* - A different negative AVERROR code in other failure cases.
*/
int av_buffersink_get_frame(AVFilterContext *ctx, AVFrame *frame);

3. 滤镜API使用方法

在代码中使用滤镜,主要分为两个步骤:

[1]. 滤镜的初始化配置:根据滤镜参数,配置生成滤镜图,此滤镜图供下一步骤使用

[2]. 使用滤镜处理原始音视频帧:向滤镜图提供输入帧(AVFrame),从滤镜图取出经处理后的输出帧(AVFrame)

1. init_filters()                   // 配置生成可用的滤镜图,由用户编写
2. av_buffersrc_add_frame_flags() // 向滤镜图提供输入帧,API函数
3. av_buffersink_get_frame() // 从滤镜图取出处理后的输出帧,API函数

3.1 滤镜配置

在代码中,滤镜配置比滤镜使用复杂,滤镜配置代码如下:

// 功能:创建配置一个滤镜图,在后续滤镜处理中,可以往此滤镜图输入数据并从滤镜图获得输出数据
// filters_descr:输入参数,形如“transpose=cclock,pad=iw+80:ih:40”
// @vfmt:输入参数,描述提供给待生成滤镜图的视频帧和格式
// @fctx:输出参数,返回生成滤镜图的信息,供调用者使用
int init_video_filters(const char *filters_descr, const input_vfmt_t *vfmt, filter_ctx_t *fctx)
{
int ret = 0; // 1. 配置滤镜图输入端和输出端
// 分配一个滤镜图filter_graph
fctx->filter_graph = avfilter_graph_alloc();
if (!fctx->filter_graph)
{
ret = AVERROR(ENOMEM);
goto end;
} char args[512];
char *p_args = NULL;
if (vfmt != NULL)
{
/* buffer video source: the decoded frames from the decoder will be inserted here. */
// args是buffersrc滤镜的参数
snprintf(args, sizeof(args),
"video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d",
vfmt->width, vfmt->height, vfmt->pix_fmt,
vfmt->time_base.num, vfmt->time_base.den,
vfmt->sar.num, vfmt->sar.den);
p_args = args;
} // buffer滤镜:缓冲视频帧,作为滤镜图的输入
const AVFilter *bufsrc = avfilter_get_by_name("buffer");
// 创建滤镜实例fctx->bufsrc_ctx,此滤镜实例从bufsrc中创建,并使用参数p_args进行初始化
// 新创建的滤镜实例命名为"in",并被添加到滤镜图fctx->filter_graph中
ret = avfilter_graph_create_filter(&fctx->bufsrc_ctx, bufsrc, "in",
p_args, NULL, fctx->filter_graph);
if (ret < 0)
{
av_log(NULL, AV_LOG_ERROR, "Cannot create buffer source\n");
goto end;
} // buffersink滤镜:缓冲视频帧,作为滤镜图的输出
const AVFilter *bufsink = avfilter_get_by_name("buffersink");
/* buffer video sink: to terminate the filter chain. */
// 创建滤镜实例buffersink_ctx,此滤镜实例从bufsink中创建
// 新创建的滤镜实例命名为"out",并被添加到滤镜图fctx->filter_graph中
ret = avfilter_graph_create_filter(&fctx->bufsink_ctx, bufsink, "out",
NULL, NULL, fctx->filter_graph);
if (ret < 0)
{
av_log(NULL, AV_LOG_ERROR, "Cannot create buffer sink\n");
goto end;
} #if 0 // 因为后面显示视频帧时有sws_scale()进行图像格式转换,故此处不设置滤镜输出格式也可
enum AVPixelFormat pix_fmts[] = { AV_PIX_FMT_YUV420P, AV_PIX_FMT_YUYV422, AV_PIX_FMT_NONE };
// 设置输出像素格式为pix_fmts[]中指定的格式(如果要用SDL显示,则这些格式应是SDL支持格式)
ret = av_opt_set_int_list(buffersink_ctx, "pix_fmts", pix_fmts,
AV_PIX_FMT_NONE, AV_OPT_SEARCH_CHILDREN);
if (ret < 0) {
av_log(NULL, AV_LOG_ERROR, "Cannot set output pixel format\n");
goto end;
}
#endif
// 1. end // 2. 将filters_descr描述的滤镜图添加到fctx->filter_graph滤镜图中
/*
* Set the endpoints for the filter graph. The filter_graph will
* be linked to the graph described by filters_descr.
*/
// 设置滤镜图的端点,将filters_descr描述的滤镜图连接到此滤镜图,
// 两个滤镜图的连接是通过端点连接(AVFilterInOut)完成的 /*
* The buffer source output must be connected to the input pad of
* the first filter described by filters_descr; since the first
* filter input label is not specified, it is set to "in" by
* default.
*/
// outputs变量意指buffersrc_ctx滤镜的输出引脚(output pad)
// src缓冲区(buffersrc_ctx滤镜)的输出必须连到filters_descr中第一个
// 滤镜的输入;filters_descr中第一个滤镜的输入标号未指定,故默认为
// "in",此处将buffersrc_ctx的输出标号也设为"in",就实现了同标号相连
AVFilterInOut *outputs = avfilter_inout_alloc();
outputs->name = av_strdup("in");
outputs->filter_ctx = fctx->bufsrc_ctx;
outputs->pad_idx = 0;
outputs->next = NULL; /*
* The buffer sink input must be connected to the output pad of
* the last filter described by filters_descr; since the last
* filter output label is not specified, it is set to "out" by
* default.
*/
// inputs变量意指buffersink_ctx滤镜的输入引脚(input pad)
// sink缓冲区(buffersink_ctx滤镜)的输入必须连到filters_descr中最后
// 一个滤镜的输出;filters_descr中最后一个滤镜的输出标号未指定,故
// 默认为"out",此处将buffersink_ctx的输出标号也设为"out",就实现了
// 同标号相连
AVFilterInOut *inputs = avfilter_inout_alloc();
inputs->name = av_strdup("out");
inputs->filter_ctx = fctx->bufsink_ctx;
inputs->pad_idx = 0;
inputs->next = NULL; // 将filters_descr描述的滤镜图添加到fctx->filter_graph滤镜图中
// 调用前:fctx->filter_graph包含两个滤镜fctx->bufsrc_ctx和fctx->bufsink_ctx
// 调用后:filters_descr描述的滤镜图插入到fctx->filter_graph中,fctx->bufsrc_ctx连接到filters_descr
// 的输入,filters_descr的输出连接到fctx->bufsink_ctx,filters_descr只进行了解析而不
// 建立内部滤镜间的连接。filters_desc与fctx->filter_graph间的连接是利用AVFilterInOut inputs
// 和AVFilterInOut outputs连接起来的,AVFilterInOut是一个链表,最终可用的连在一起的
// 滤镜链/滤镜图就是通过这个链表串在一起的。
ret = avfilter_graph_parse_ptr(fctx->filter_graph, filters_descr,
&inputs, &outputs, NULL);
if (ret < 0)
{
goto end;
}
// 2. end // 3. 配置filtergraph滤镜图,建立滤镜间的连接
// 验证有效性并配置filtergraph中所有连接和格式
ret = avfilter_graph_config(fctx->filter_graph, NULL);
if (ret < 0)
{
goto end;
}
// 3. end end:
avfilter_inout_free(&inputs);
avfilter_inout_free(&outputs); return ret;
}

函数参数说明:

  • 输入参数 const char *filters_descr

    以字符串形式提供滤镜选项,例如参数为 "transpose=cclock,pad=iw+80:ih:40" 时,表示将视频帧逆时针旋转 90 度,然后在视频左右各填充 40 像素的黑边。

  • 输入参数 input_vfmt_t *vfmt

    用于描述提供给滤镜图的视频帧和格式,在配置滤镜图中的第一个滤镜 buffer 时需要为滤镜提供参数,就是从 vfmt 参数转换得到。

    input_vfmt_t 为自定义数据结构,定义如下:

typedef struct {
int width;
int height;
enum AVPixelFormat pix_fmt;
AVRational time_base;
AVRational sar;
AVRational frame_rate;
} input_vfmt_t;
  • 输出参数 filter_ctx_t *fctx

    用于返回生成滤镜图的信息,供调用者使用。

    filter_ctx_t 为自定义数据结构,定义如下:
typedef struct {
AVFilterContext *bufsink_ctx;
AVFilterContext *bufsrc_ctx;
AVFilterGraph *filter_graph;
} filter_ctx_t;

此结构中三个成员:bufsrc_ctx 用于滤镜图的输入,bufsink_ctx 用于滤镜图的输出,filter_graph 指向滤镜图。

TODO: 一个滤镜图可能含多个滤镜链,即可能有多个输入节点(bufsrc_ctx)或多个输出节点(bufsink_ctx),此数据结构应改进为支持多输入和多输出

init_video_filters() 函数实现的几个步骤如下:

3.1.1 配置滤镜图输入端和输出端

buffer 滤镜和 buffersink 滤镜是两个特殊的视频滤镜,分别用于视频滤镜链的输入端和输出端。与之相似,abuffer 滤镜和 abuffersink 滤镜是两个特殊的音频滤镜,分别用于音频滤镜链的输入端和输出端。

一个滤镜图可能由多个滤镜链构成,每个滤镜链的输入节点就是 buffer 滤镜,输出节点是 buffersink 滤镜,因此一个滤镜图可能有多个 buffer 滤镜,也可能有多个 buffersink 滤镜。应用程序通过访问 buffer 滤镜和 buffersink 滤镜实现和滤镜图的数据交互。

buffer 滤镜

在命令行中输入 ffmpeg -h filter=buffer 查看 buffer 滤镜的帮助信息,如下:

$ ffmpeg -h filter=buffer
ffmpeg version 4.1 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 <int> ..FV..... (from 0 to INT_MAX) (default 0)
video_size <image_size> ..FV.....
height <int> ..FV..... (from 0 to INT_MAX) (default 0)
pix_fmt <pix_fmt> ..FV..... (default none)
sar <rational> ..FV..... sample aspect ratio (from 0 to DBL_MAX) (default 0/1)
pixel_aspect <rational> ..FV..... sample aspect ratio (from 0 to DBL_MAX) (default 0/1)
time_base <rational> ..FV..... (from 0 to DBL_MAX) (default 0/1)
frame_rate <rational> ..FV..... (from 0 to DBL_MAX) (default 0/1)
sws_param <string> ..FV.....

buffer 滤镜用作滤镜链的输入节点。buffer 滤镜缓冲视频帧,滤镜链可以从 buffer 滤镜中取得视频帧数据。

在上述帮助信息中,Inputs 和 Outputs 指滤镜的输入引脚和输出引脚。buffer 滤镜是滤镜链中的第一个滤镜,因此只有输出引脚而无输入引脚。

滤镜(AVFilter)需要通过滤镜实例(AVFilterContext)引用,为 buffer 滤镜创建的滤镜实例是 fctx->bufsrc_ctx,用户通过往 fctx->bufsrc_ctx 填入视频帧来为滤镜链提供输入。

为 buffer 滤镜创建滤镜实例时需要提供参数,buffer 滤镜需要的参数在帮助信息中的 “buffer AVOptions” 部分列出,由 vfmt 输入参数提供,代码如下:

    char args[512];
char *p_args = NULL;
if (vfmt != NULL)
{
/* buffer video source: the decoded frames from the decoder will be inserted here. */
// args是buffersrc滤镜的参数
snprintf(args, sizeof(args),
"video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d",
vfmt->width, vfmt->height, vfmt->pix_fmt,
vfmt->time_base.num, vfmt->time_base.den,
vfmt->sar.num, vfmt->sar.den);
p_args = args;
} // buffer滤镜:缓冲视频帧,作为滤镜图的输入
const AVFilter *bufsrc = avfilter_get_by_name("buffer");
// 创建滤镜实例fctx->bufsrc_ctx,此滤镜实例从bufsrc中创建,并使用参数p_args进行初始化
// 新创建的滤镜实例命名为"in",并被添加到滤镜图fctx->filter_graph中
ret = avfilter_graph_create_filter(&fctx->bufsrc_ctx, bufsrc, "in",
p_args, NULL, fctx->filter_graph);
if (ret < 0)
{
av_log(NULL, AV_LOG_ERROR, "Cannot create buffer source\n");
goto end;
}

buffersink 滤镜

在命令行中输入 ffmpeg -h filter=buffersink 查看 buffersink 滤镜的帮助信息,如下:

$  ffmpeg -h filter=buffersink
ffmpeg version 4.1 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 <binary> ..FV..... set the supported pixel formats

buffersink 滤镜用作滤镜链的输出节点。滤镜链处理后的视频帧可以缓存到 buffersink 滤镜中。buffersink 滤镜是滤镜链中的最后一个滤镜,因此只有输入引脚而无输出引脚。

为 buffersink 滤镜创建的滤镜实例是 fctx->bufsink_ctx,用户可以从 fctx->bufsink_ctx 中读视频帧来获得滤镜链的输出。

通过帮助信息可以看到,buffersink 滤镜参数只有一个 “pix_fmt”,用于设置滤镜链输出帧的像素格式列表,这个像素格式有多种,以限制输出帧格式不超过指定的范围。

    // buffersink滤镜:缓冲视频帧,作为滤镜图的输出
const AVFilter *bufsink = avfilter_get_by_name("buffersink");
// 为buffersink滤镜创建滤镜实例buffersink_ctx,命名为"out"
// 将新创建的滤镜实例buffersink_ctx添加到滤镜图filter_graph中
ret = avfilter_graph_create_filter(&fctx->bufsink_ctx, bufsink, "out",
NULL, NULL, fctx->filter_graph); #if 0 // 因为后面显示视频帧时有sws_scale()进行图像格式转换,故此处不设置滤镜输出格式也可
enum AVPixelFormat pix_fmts[] = { AV_PIX_FMT_YUV420P, AV_PIX_FMT_YUYV422, AV_PIX_FMT_NONE };
// 设置输出像素格式为pix_fmts[]中指定的格式(如果要用SDL显示,则这些格式应是SDL支持格式)
ret = av_opt_set_int_list(buffersink_ctx, "pix_fmts", pix_fmts,
AV_PIX_FMT_NONE, AV_OPT_SEARCH_CHILDREN);
#endif

将 buffer 滤镜和 buffsink 滤镜添加进滤镜图中后,如下图所示:

3.1.2 将 filters_descr 描述的滤镜插入滤镜图中

解析滤镜选项(filters_descr),将解析得到的滤镜插入第 1 步构造的滤镜图中,并与滤镜图输入端和输出端连接起来

    // 设置滤镜图的端点,将filters_descr描述的滤镜图连接到此滤镜图
// 两个滤镜图的连接是通过端点(AVFilterInOut)连接完成的
// 端点数据结构AVFilterInOut主要用于avfilter_graph_parse()系列函数 // outputs变量意指buffersrc_ctx滤镜的输出引脚(output pad)
// src缓冲区(buffersrc_ctx滤镜)的输出必须连到filters_descr中第一个
// 滤镜的输入;filters_descr中第一个滤镜的输入标号未指定,故默认为
// "in",此处将buffersrc_ctx的输出标号也设为"in",就实现了同标号相连
AVFilterInOut *outputs = avfilter_inout_alloc();
outputs->name = av_strdup("in");
outputs->filter_ctx = fctx->bufsrc_ctx;
outputs->pad_idx = 0;
outputs->next = NULL; // inputs变量意指buffersink_ctx滤镜的输入引脚(input pad)
// sink缓冲区(buffersink_ctx滤镜)的输入必须连到filters_descr中最后
// 一个滤镜的输出;filters_descr中最后一个滤镜的输出标号未指定,故
// 默认为"out",此处将buffersink_ctx的输出标号也设为"out",就实现了
// 同标号相连
AVFilterInOut *inputs = avfilter_inout_alloc();
inputs->name = av_strdup("out");
inputs->filter_ctx = fctx->bufsink_ctx;
inputs->pad_idx = 0;
inputs->next = NULL; // 将filters_descr描述的滤镜图添加到filter_graph滤镜图中
// 调用前:filter_graph包含两个滤镜buffersrc_ctx和buffersink_ctx
// 调用后:filters_descr描述的滤镜图插入到filter_graph中,buffersrc_ctx连接到filters_descr
// 的输入,filters_descr的输出连接到buffersink_ctx,filters_descr只进行了解析而不
// 建立内部滤镜间的连接。filters_desc与filter_graph间的连接是利用AVFilterInOut inputs
// 和AVFilterInOut outputs连接起来的,AVFilterInOut是一个链表,最终可用的连在一起的
// 滤镜链/滤镜图就是通过这个链表串在一起的。
ret = avfilter_graph_parse_ptr(fctx->filter_graph, filters_descr,
&inputs, &outputs, NULL);

filters_descr 描述的滤镜如下图所示:

调用 avfilter_graph_parse_ptr() 后,滤镜图如下所示:

3.1.3. 建立滤镜连接

调用 avfilter_graph_config() 将上一步得到的滤镜图进行配置,建立滤镜间的连接,此步完成后即生了一个可用的滤镜图,如下图所示:

3.2 使用滤镜处理原始帧

配置好滤镜后,可在音视频处理过程中使用滤镜。使用滤镜比配置滤镜简单很多,主要调用如下两个 API 函数:

  1. 调用 av_buffersrc_add_frame_flags() 将音视频帧发送给滤镜
  2. 调用 av_buffersink_get_frame() 取得经滤镜处理后的音视频帧

4. 滤镜 API 应用实例分析

滤镜接收原始音视频帧,经过各种效果的滤镜处理后输出的仍然是原始音视频帧。在滤镜 API 应用实例中,核心内容是 “滤镜配置” 和 “滤镜使用” 两个部分,滤镜接收什么样的输入源不重要,对滤镜的输出做什么处理也不重要。不同的输入源,及不同的输出处理方式仅仅是为了加深对滤镜 API 使用的理解,以及方便观察滤镜的处理效果。

滤镜的输入可以是解码器的输出、原始 YUV 文件及测试图。本文三个示例只针对视频滤镜:

示例 1:编码器的输出作为滤镜的输入,滤镜的输出简单处理,无法观察滤镜效果。

示例 2:编码器的输出作为滤镜的输入,滤镜的输出可以播放,可直观观察滤镜效果。

示例 3:测试图作为滤镜的输入(而测试图本身也是由特殊滤镜生成),滤镜的输出可以播放,可直接观察滤镜效果。

示例 1 源码下载:https://github.com/FFmpeg/FFmpeg/blob/n4.1/doc/examples/filtering_video.c

示例 2 与示例 3 源码下载(shell中运行如下命令):

svn checkout https://github.com/leichn/exercises/trunk/source/ffmpeg/ffmpeg_vfilter/

4.1 示例 1:官方例程

官方例程实现的功能是:打开一个视频文件,解码后经过滤镜处理,然后以简单灰度模式在命令窗口中播放视频帧。

例程中使用的滤镜选项是 "scale=78:24,transpose=cclock",表示先用 scale 滤镜将视频帧缩放到 78x24 像素,再用 transpose 滤镜将视频帧逆时针旋转 90 度。

简述一下例程的步骤:

  1. 打开视频文件,调用 open_input_file() 实现
  2. 初始化滤镜,调用 init_filters() 实现
  3. 解码得到视频帧,调用 avcodec_send_packet() 和 avcodec_receive_frame() 获得解码后的原始视频帧
  4. 将视频帧发给滤镜,调用 av_buffersrc_add_frame_flags() 实现
  5. 从滤镜输出端取视频帧,调用 av_buffersink_get_frame() 实现
  6. 播放视频帧,调用 display_frame() 实现

例程核心是滤镜相关的代码,因此视频帧播放部分做了简化处理。

4.2 示例 2:可播放版本

官方例程主要演示滤镜 API 的使用方法,代码量较少,简化了视频播放部分,这样使得滤镜的处理效果无法直观观察。示例 2 针对此问题,在官方代码基础上增加了正常的视频播放效果。

4.2.1 代码

下载代码后,源码目录下有如下几个文件,说明如下:

vfilter_filesrc.c   用于示例2:输入源为视频文件,经滤镜处理后播放
vfilter_testsrc.c 用于示例3:输入源为测试图,经滤镜处理后播放
video_filter.c 滤镜处理功能
video_play.c 视频播放功能
Makefile

video_filter.c 封装了滤镜处理相关代码,详参本文第 3 节。

video_play.c 实现了视频播放功能,本例无需过多关注,实现原理可参考如下两篇文章:

FFmpeg简易播放器的实现-视频播放

ffplay源码分析5-图像格式转换

vfilter_filesrc.c 是示例 2 的主程序,实现了打开视频文件、解码、滤镜处理、播放主流程

4.2.2 编译

进入源码目录,编译生成 vf_file 可执行文件:

make vf_file

4.2.3 测试

在命令行运行:

./vf_file ./ring.flv -vf crop=iw/2:ih:0:0,pad=iw*2:ih`

滤镜选项 "-vf crop=iw/2:ih:0:0,pad=iw*2:ih" 表示先将视频裁剪为一半宽度,再填充为二倍宽度,预期结果为视频的右半部分为黑边。

测试文件下载(右键另存为):ring.flv

未经滤镜处理和经过滤镜处理的视频效果对比如下两图所示:



4.3 示例 3:测试图作输入源

示例 3 使用测试图(test pattern)作为滤镜的输入,测试图(test pattern)是由 FFmpeg 内部产生的测试图案,用于测试非常方便。

因测试图直接输出原始视频帧,不需解码器,因此示例 3 中用到 AVFilter 库,不需要用到 AVFormat 库。

4.3.1 代码

4.2 节下载得到的源码中的 vfilter_testsrc.c 就是示例 3 的主程序,实现了构建测试源,滤镜处理,播放的主流程。除滤镜输入源的获取方式与示例 2 不同之外,其他过程并无不同。

示例 3 增加的关键内容是构造测试源,参考 vfilter_testsrc.c 中如下函数:

// @filter [i]  产生测试图案的filter
// @vfmt [i] @filter的参数
// @fctx [o] 用户定义的数据类型,输出供调用者使用
static int open_testsrc(const char *filter, const input_vfmt_t *vfmt, filter_ctx_t *fctx)
{
int ret = 0; // 分配一个滤镜图filter_graph
fctx->filter_graph = avfilter_graph_alloc();
if (!fctx->filter_graph)
{
return AVERROR(ENOMEM);
} // source滤镜:合法值有"testsrc"/"smptebars"/"color"/...
const AVFilter *bufsrc = avfilter_get_by_name(filter);
// 为buffersrc滤镜创建滤镜实例buffersrc_ctx,命名为"in"
// 将新创建的滤镜实例buffersrc_ctx添加到滤镜图filter_graph中
ret = avfilter_graph_create_filter(&fctx->bufsrc_ctx, bufsrc, "in",
NULL, NULL, fctx->filter_graph);
if (ret < 0)
{
av_log(NULL, AV_LOG_ERROR, "Cannot create filter testsrc\n");
goto end;
} // "buffersink"滤镜:缓冲视频帧,作为滤镜图的输出
const AVFilter *bufsink = avfilter_get_by_name("buffersink");
/* buffer video sink: to terminate the filter chain. */
// 为buffersink滤镜创建滤镜实例buffersink_ctx,命名为"out"
// 将新创建的滤镜实例buffersink_ctx添加到滤镜图filter_graph中
ret = avfilter_graph_create_filter(&fctx->bufsink_ctx, bufsink, "out",
NULL, NULL, fctx->filter_graph);
if (ret < 0)
{
av_log(NULL, AV_LOG_ERROR, "Cannot create filter buffersink\n");
goto end;
} if ((ret = avfilter_link(fctx->bufsrc_ctx, 0, fctx->bufsink_ctx, 0)) < 0)
{
goto end;
} // 验证有效性并配置filtergraph中所有连接和格式
ret = avfilter_graph_config(fctx->filter_graph, NULL);
if (ret < 0)
{
goto end;
} vfmt->pix_fmt = av_buffersink_get_format(fctx->bufsink_ctx);
vfmt->width = av_buffersink_get_w(fctx->bufsink_ctx);
vfmt->height = av_buffersink_get_h(fctx->bufsink_ctx);
vfmt->sar = av_buffersink_get_sample_aspect_ratio(fctx->bufsink_ctx);
vfmt->time_base = av_buffersink_get_time_base(fctx->bufsink_ctx);
vfmt->frame_rate = av_buffersink_get_frame_rate(fctx->bufsink_ctx); av_log(NULL, AV_LOG_INFO, "probe video format: "
"%dx%d, pix_fmt %d, SAR %d/%d, tb %d/%d, rate %d/%d\n",
vfmt->width, vfmt->height, vfmt->pix_fmt,
vfmt->sar.num, vfmt->sar.den,
vfmt->time_base.num, vfmt->time_base.den,
vfmt->frame_rate.num, vfmt->frame_rate.den); return 0; end:
avfilter_graph_free(&fctx->filter_graph);
return ret;
}

测试源的本质是使用 FFmpeg 提供的用于产生测试图案的滤镜来生成视频数据。具体到代码实现层面,将 testsrc/smptebars 等滤镜代替常用的 buffer 滤镜作为源滤镜,然后直接与 buffersink 滤镜相连,以输出测试图案,如下图:

4.3.2 编译

进入源码目录,编译生成 vf_test 可执行文件:

make vf_test

4.3.3 测试

滤镜选项 "-vf transpose=cclock,pad=iw+80:ih:40" 表示先将视频逆时针旋转 90 度,然后将视频左右两边各增加 40 像素宽度的黑边

使用“testsrc”测试图作输入源

运行如下命令:

ffplay -f lavfi -i testsrc

无滤镜处理的效果如图所示:

运行带滤镜选项的 ffplay 命令:

ffplay -f lavfi -i testsrc -vf transpose=cclock,pad=iw+80:ih:40

运行带滤镜选项的测试程序(效果等同于上述 ffplay 命令):

./vf_test testsrc -vf transpose=cclock,pad=iw+80:ih:40

经滤镜处理的效果如图所示:

使用“smptebars”测试图作输入源

运行如下命令:

ffplay -f lavfi -i smptebars

无滤镜处理的效果如图所示:

运行带滤镜选项的ffplay命令:

ffplay -f lavfi -i smptebars -vf transpose=cclock,pad=iw+80:ih:40

运行带滤镜选项的测试程序(效果等同于上述ffplay命令):

./vf_test smptebars -vf transpose=cclock,pad=iw+80:ih:40

经滤镜处理的效果如图所示:

5. 遗留问题

[1] 不支持多输入多输出的复杂滤镜图,待改进验证

[2] 如何使用 API 以类似打开普通输入文件的方法来获取测试图的格式,即ffprobe -f lavfi -i testsrc的内部原理是什么?

think@linux-1phi:~> ffprobe -f lavfi -i testsrc
ffprobe version 4.1 Copyright (c) 2007-2018 the FFmpeg developers
Input #0, lavfi, from 'testsrc':
Duration: N/A, start: 0.000000, bitrate: N/A
Stream #0:0: Video: rawvideo (RGB[24] / 0x18424752), rgb24, 320x240 [SAR 1:1 DAR 4:3], 25 tbr, 25 tbn, 25 tbc

6. 参考资料

[1] 刘歧,FFmpeg Filter深度应用https://yq.aliyun.com/articles/628153?utm_content=m_1000014065

7. 修改记录

2019-02-24 V1.0 初稿

FFmpeg原始帧处理-滤镜API用法详解的更多相关文章

  1. Hadoop生态圈-zookeeper的API用法详解

    Hadoop生态圈-zookeeper的API用法详解 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.测试前准备 1>.开启集群 [yinzhengjie@s101 ~] ...

  2. JavaEE基础(02):Servlet核心API用法详解

    本文源码:GitHub·点这里 || GitEE·点这里 一.核心API简介 1.Servlet执行流程 Servlet是JavaWeb的三大组件之一(Servlet.Filter.Listener) ...

  3. Java基础篇(04):日期与时间API用法详解

    本文源码:GitHub·点这里 || GitEE·点这里 一.时间和日期 在系统开发中,日期与时间作为重要的业务因素,起到十分关键的作用,例如同一个时间节点下的数据生成,基于时间范围的各种数据统计和分 ...

  4. Java并发编程(06):Lock机制下API用法详解

    本文源码:GitHub·点这里 || GitEE·点这里 一.Lock体系结构 1.基础接口简介 Lock加锁相关结构中涉及两个使用广泛的基础API:ReentrantLock类和Condition接 ...

  5. Vue1.0用法详解

    Vue.js 不支持 IE8 及其以下版本,因为 Vue.js 使用了 IE8 不能实现的 ECMAScript 5 特性. 开发环境部署 可参考使用 vue+webpack. 基本用法 1 2 3 ...

  6. Android GLSurfaceView用法详解(二)

    输入如何处理       若是开发一个交互型的应用(如游戏),通常需要子类化 GLSurfaceView,由此可以获取输入事件.下面有个例子: java代码: package eoe.ClearTes ...

  7. 教程-Delphi中Spcomm使用属性及用法详解

    Delphi中Spcomm使用属性及用法详解 Delphi是一种具有 功能强大.简便易用和代码执行速度快等优点的可视化快速应用开发工具,它在构架企业信息系统方面发挥着越来越重要的作用,许多程序员愿意选 ...

  8. Python中第三方库Requests库的高级用法详解

    Python中第三方库Requests库的高级用法详解 虽然Python的标准库中urllib2模块已经包含了平常我们使用的大多数功能,但是它的API使用起来让人实在感觉不好.它已经不适合现在的时代, ...

  9. c++中vector的用法详解

    c++中vector的用法详解 vector(向量): C++中的一种数据结构,确切的说是一个类.它相当于一个动态的数组,当程序员无法知道自己需要的数组的规模多大时,用其来解决问题可以达到最大节约空间 ...

随机推荐

  1. [leetcode]335. Self Crossing

    You are given an array x of n positive numbers. You start at point (,) and moves x[] metres to the n ...

  2. 关于Bootstrap的入门知识

    问:Bootstrap是什么? 答:开源的前端框架,就是一些事先写好的css.js等. 问:Bootstrap在哪儿下载? 答:官方(https://getbootstrap.com/),中文(htt ...

  3. threading.local()源码分析

    前段时间写了个多线程的程序,了解到Python中有个与众不同的thread.local()方法,可以创建一个全局对象,各个线程可以用这个全局对象保存各自的局部变量,而在使用时不受其他线程的影响.于是抽 ...

  4. 用python优雅打开文件及上下文管理协议

    有次面试被问到如何优雅地打开一个文件?   那就是用with语句,调用过后可以自动关闭.   但是为什么使用with语句就可以自动关闭呢,原因就是上下文管理协议.   上下文管理协议:包含方法 __e ...

  5. vue.js建立一个简单的表格

    <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding= ...

  6. 35.Spring-jdbc支持.md

    目录 1.JdbcTemplate类 1.1导入jar包 1.2创建Dao对象 1.3将上述例子封装后 2. 3. 1.JdbcTemplate类 传统的jdbc开始,需要对Connection.St ...

  7. Beta冲刺——第二天

    beat冲刺:第二天 各个成员今日完成的任务 成员 冯晓.马思远 彭辉.王爽 吴琼.郝延婷 今日完成任务 ·管理员功能模块的代码规范与测试 ·网站的前端调整 ·代码规范 ·系统管理模块功能测试 ·博客 ...

  8. Java中的包装数据类型

    基本类型 包装器类型 boolean Boolean char Character int Integer byte Byte short Short long Long float Float do ...

  9. react-native 新手爬坑经历(Could not connect to development server.)

    来,先说下报错出现场景,刚跑完项目加载完是好的,但是双击R后就开始耍小脾气了-红屏出现,如下图 首先检查包服务器是否运行正常.在项目文件夹下输入react-native start或者npm star ...

  10. ubuntu安装jdk,maven,tomcat

    ubuntu16.04安装jdk8 -jdk 检查是否安装成功 java -version 出现如上信息即安装成功 安装maven,先去官网下载指定版本的maven,个人使用apache-maven- ...