引文 - FFmpeg Filter的介绍
Filter,一般被译为"过滤器"或者"滤镜",本篇文章统一以"过滤器"著称。
那么过滤器的作用是什么呢?FFmpeg中的过滤器系统是在解码之后、编码之前对媒体流进行处理的关键组件。
下图是一个FFmpeg音视频处理的流程图,可以看到在解码之后可以将数据经由过滤器处理,处理过后的内容再进行编码合成操作,这样就可以实现对一个视频进行诸如视频分辨率转换、音频混音、模糊、锐化等一系列的操作。
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
所以,过滤器的功能十分强大,FFmpeg中内置了几百种过滤器。可以使用 ffmpeg -filters
命令查看所有的过滤器,也可以用 ffmpeg -h filter=xxx
命令查看指定的过滤器,或者访问:FFmpeg Filters Documentation 查看官方文档。
FFmpeg过滤器实战
Filter结构分析
简单来说,FFmpeg Filter的使用可以概括为三个步骤,分别是FilterGraph绘制、数据输入、输出数据。可以理解为,"FilterGraph绘制"就是绘制一张图纸并按照图纸设置好一台机器,"数据输入"表示将原料放入这台机器,"输出数据"就是原料经过机器处理最终将目标产物输出出来。
一个完整的FilterGraph结构如下图所示,可以看到,FilterGraph本质上就是一个流程图,图中的每一个组件就是一个过滤器,例如"buffer"、“crop”、"overlay"等,而每两个过滤器之间的关系就是一个link,其描述了数据在过滤器之间的走向。
关于这个FilterGraph不用把它想的太复杂,将其看作就是一个流程图即可。
实战流程分析
首先来认识结构体:
AVFilterGraph
是FilterGraph的结构体,用于描绘整个Filter流程图
AVFilter
和AVFilterContext
用于表示一个Filter
AVFilterLink是Filter链的结构体,不过一般不常用,相反avfilter_link
函数比较常见
具体流程如下:
- 首先要创建一张图:
avfilter_graph_alloc
- 接着查找要使用的Filter:
avfilter_get_by_name
(获取与给定名称匹配的过滤器定义)- 然后在图中创建并初始化这个Filter:
avfilter_graph_alloc_filter
- 在FilterGraph中创建新的过滤器实例。接着用avfilter_init_str
或``avfilter_init_dict`初始化对应的Filter,两个函数作用一样,只是传入的参数不一样。- 连接这些过滤器:
avfilter_link
,link操作就相当于是将a连向b,具体含义是将前者的输出作为后者的输入,对应的pad参数中pad为buffer_ctx->output_pads数组中的下标(从0开始)avfilter_graph_config
检查有效性,并配置图表中的所有链接和格式。av_buffersrc_add_frame
将frame作为输入源传入到过滤器中av_buffersink_get_frame
获取经过滤器处理后的frame
注意事项
- avfilter_init_str和avfilter_graph_create_filter是使用字符串初始化buffer参数的,所以对其有严格的格式要求,必须是 “key=value” 形式的 “:” 分隔的选项列表,例如:“video_size=1280x720:time_base=1/25”
- FFmpeg Filter是有安全检查的,如果一个filter的输入或输出部分没有任何连接就会报错(这是FFmpeg内部自己做的)
- overlay过滤器的参数,“y=0:W/2"这种写法表示的实际含义就相当于"y=W/2”,此时的x默认为0。"y=0:x=W/2"这种写法才表示的是从y=0,x=W/2的位置开始绘制。而且,
y=W/2
和y=w/2
的区别在于它们分别引用了不同的宽度值,大写的W、H等表示的是输出视频的宽度,小写的则是覆盖视频的宽度。- 特殊的filter,buffer和buffersink。buffer通常作为输入缓冲区,buffersink通常作为输出缓冲区。数据先经过buffer流向一系列的过滤器中,最终从buffersink中流出数据。
- 经过filter流程处理后的一帧图像,其大小并不会因为覆盖了多组图像而变大,像素设置的是多少就是多少。
- buffer options的相关定义:buffersrc.c - line 322(版本FFmpeg-7.0)
demo样例
demo用到的filter
buffer
:通常是作为过滤器的输入源来使用,必须有。buffersink
:通常是作为过滤器的目标输出源来使用。split
:接收一个输入流,并将其分成指定数量的输出流,每个输出流都是输入流的一个完全拷贝。crop
:视频/图像的裁剪。vflip
:垂直翻转输入视频。除此之外,hflip filter用于水平翻转输入视频。overlay
:视频叠加。
demo样例流程图
/**********************************************************
* filter 流程分析 *
***********************************************************
* [main] pad-0 *
* buffer ------> split ------> overlay ---> buffersink *
* | pad-1 | *
* | [tmp] [flip]| *
* +-----> crop --> vflip --->--+ *
***********************************************************/
#include <iostream>
#include <fstream>
#include <sstream>
using namespace std;
extern "C"
{
#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>
}
int main(int argc, char* argv)
{
const char* inFileName = "yuv420p_1280x720.yuv";
const char* outFileName = "out.yuv";
ifstream inFile(inFileName, ios_base::in | ios_base::binary);
ofstream outFile(outFileName, ios_base::out | ios_base::binary);
int in_width = 1280;
int in_height = 720;
/* Step 1
* 创建并初始化用到的filter:
* graph、buffer、sinkbuffer、
* split、crop、vflip、overlay
*****************************/
// filter fraph
AVFilterGraph* filter_graph = avfilter_graph_alloc();
// buffer filter
const AVFilter* buffer = avfilter_get_by_name("buffer");
AVFilterContext* buffer_ctx =
avfilter_graph_alloc_filter(filter_graph, buffer, "buffer_src");
string args = "video_size=" + to_string(in_width) + "x" + to_string(in_height)
+ ":pix_fmt=" + to_string(AV_PIX_FMT_YUV420P) + ":time_base=1/25"
+ ":pixel_aspect=1/1";
avfilter_init_str(buffer_ctx, args.c_str());
// sinkbuffer filter
const AVFilter* buffersink = avfilter_get_by_name("buffersink");
AVFilterContext* buffersink_ctx =
avfilter_graph_alloc_filter(filter_graph, buffersink, "buffer_dst");
avfilter_init_str(buffersink_ctx, nullptr);
// split filter
const AVFilter *splitFilter = avfilter_get_by_name("split");
AVFilterContext *splitFilter_ctx =
avfilter_graph_alloc_filter(filter_graph, splitFilter, "splitFilter");
avfilter_init_str(splitFilter_ctx, nullptr);
// crop filter
const AVFilter *cropFilter = avfilter_get_by_name("crop");
AVFilterContext *cropFilter_ctx =
avfilter_graph_alloc_filter(filter_graph, cropFilter, "cropFilter");
avfilter_init_str(cropFilter_ctx, "out_w=iw:out_h=ih/2:x=0:y=0");
// vflip filter
const AVFilter *vflipFilter = avfilter_get_by_name("vflip");
AVFilterContext *vflipFilter_ctx =
avfilter_graph_alloc_filter(filter_graph, vflipFilter, "vflipFilter");
avfilter_init_str(vflipFilter_ctx, nullptr);
// overlay filter
const AVFilter *overlayFilter = avfilter_get_by_name("overlay");
AVFilterContext *overlayFilter_ctx =
avfilter_graph_alloc_filter(filter_graph, overlayFilter, "overlayFilter");
avfilter_init_str(overlayFilter_ctx, "y=0:H/2");
/* Step2: link filter and check
********************************/
// buffer_src link to splitFilter
avfilter_link(buffer_ctx, 0, splitFilter_ctx, 0);
// split filter's first pad to overlay filter's main pad
avfilter_link(splitFilter_ctx, 0, overlayFilter_ctx, 0);
// split filter's second pad to crop filter
avfilter_link(splitFilter_ctx, 1, cropFilter_ctx, 0);
// crop filter to vflip filter
avfilter_link(cropFilter_ctx, 0, vflipFilter_ctx, 0);
// vflip filter to overlay filter's second pad
avfilter_link(vflipFilter_ctx, 0, overlayFilter_ctx, 1);
// overlay filter to sink filter
avfilter_link(overlayFilter_ctx, 0, buffersink_ctx, 0);
// check filter graph
avfilter_graph_config(filter_graph, nullptr);
/* Step3: 写入到输出文件
***********************/
int buf_size = av_image_get_buffer_size(AV_PIX_FMT_YUV420P,
in_width, in_height, 1);
AVFrame *frame_in = av_frame_alloc();
uint8_t *in_buf = new uint8_t[buf_size];
AVFrame *frame_out = av_frame_alloc();
frame_in->width = in_width;
frame_in->height = in_height;
frame_in->format = AV_PIX_FMT_YUV420P;
uint32_t frame_counts = 0;
// tips:yuv420p格式,一个y对应一个uv,所以一个像素点平均占3/2个像素大小
while (inFile.read((char*)in_buf, in_width * in_height * 3/2))
{
// 写入frame数据
av_image_fill_arrays(frame_in->data, frame_in->linesize, in_buf,
AV_PIX_FMT_YUV420P, in_width, in_height, 1);
// 将frame的内容作为输入添加到第一个参数所指向的AVFilterContext
av_buffersrc_add_frame(buffer_ctx, frame_in);
// 从第一个参数所指向的AVFilterContext中读取内容到frame
av_buffersink_get_frame(buffersink_ctx, frame_out);
// 将处理好的内容写入到文件
// 暂定格式为 YUV420P
for (int i = 0; i < frame_out->height; i++) // Y分量
outFile.write((char*)frame_out->data[0] +
frame_out->linesize[0] * i, frame_out->width);
for (int i = 0; i < frame_out->height / 2; i++) // U分量
outFile.write((char*)frame_out->data[1] +
frame_out->linesize[1] * i, frame_out->width / 2);
for (int i = 0; i < frame_out->height / 2; i++) // V分量
outFile.write((char*)frame_out->data[2] +
frame_out->linesize[2] * i, frame_out->width / 2);
// 每隔25帧打印一次
if(++frame_counts % 25 == 0)
cout << "Process " << frame_counts << "frames!" << endl;
// 及时释放,防止内存泄漏
av_frame_unref(frame_out);
}
// clear and exit
inFile.close();
outFile.close();
av_frame_free(&frame_in);
av_frame_free(&frame_out);
avfilter_free(buffer_ctx);
avfilter_free(buffersink_ctx);
avfilter_free(splitFilter_ctx);
avfilter_free(cropFilter_ctx);
avfilter_free(vflipFilter_ctx);
avfilter_free(overlayFilter_ctx);
avfilter_graph_free(&filter_graph);
cout << "filter work over!" << endl;
return 0;
}
参考资料
- Filter | ffmpeg-examples (andy-zhangtao.github.io)
- FFmpeg filter简介 - 博客园
- ffmpeg入门篇-过滤器的基本使用 - 白狼栈 - 博客园 (cnblogs.com)
/andy-zhangtao.github.io/ffmpeg-examples/filter.html)
- FFmpeg filter简介 - 博客园
- ffmpeg入门篇-过滤器的基本使用 - 白狼栈 - 博客园 (cnblogs.com)