FFmpeg过滤器实战:水印处理
过滤器实战:水印处理
这个 Demo 展示了如何使用 FFmpeg 的滤镜系统对视频进行处理,实现将视频上半部分倒置并与下半部分叠加的效果。下面我将详细解析这个代码。
程序整体结构
整个程序完成以下几个步骤:
- 初始化滤镜系统
- 构建滤镜图(Filter Graph)
- 处理输入视频帧
- 输出处理后的视频帧
- 清理资源
滤镜图设计分析
buffer_src → split → overlay (主输出)\→ crop → vflip → overlay (副输入)
代码分析
文件输入初始化
int ret = 0;// 出入文件和输出文件
FILE* in_file = NULL;
FILE* out_file = NULL;const char* in_file_name = NULL;
const char* out_file_name = NULL;if (argc <= 3) {printf("Usage ./ffmpeg_watermark [in_file] [out_file]\n");return -1;
}in_file_name = argv[1];
out_file_name = argv[2];in_file = fopen(in_file_name, "rb+");
if (!in_file) {printf("Failed to open in_file");return -1;
}out_file = fopen(out_file_name, "wb");
if (!out_file) {printf("Failed to open out_file");return -1;
}int width = 768;
int height = 320;
- 我们定义一些出入文件和输出文件的参数,比如文件指针和文件路径。
- 最后的
width
和height
是我们输入视频流 frame 的宽和高。
过滤器图初始化(管理所有滤镜)
// 注册所有内置滤镜
avfilter_register_all();
// 初始化并分配一个空的滤镜图结构
AVFilterGraph* filter_graph = avfilter_graph_alloc();if (!filter_graph) {printf("avfilter_graph_alloc failed\n");return -1;
}char args[512];
sprintf(args, "video_sizeo=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d",
width, height, AV_PIX_FMT_YUV420P, 1, 25, 1, 1);
- 使用
avfilter_graph_alloc
函数来初始化一个空的滤镜图结构,滤镜图结构会统一管理所有的滤镜,所以后续的所有滤镜都需要注册到滤镜图上
滤镜初始化(具体的滤镜)
// 获取名为buffer的滤镜,用于像滤镜图输入原始数据
// 返回值AVFilter指向FFmpeg内置的buffer滤镜
AVFilter* buffer_src = avfilter_get_by_name("buffer");
AVFilterContext* buffer_src_ctx;
ret = avfilter_graph_create_filter(&buffer_src_ctx, buffer_src, "in", args, NULL, filter_graph);
if (ret < 0) {printf("avfilter_graph_create_filter buffer failed\n");return -1;
}// buffersink滤镜 作为滤镜的输出节点,用于从滤镜中提取处理后的帧(如缩放/裁剪后的视频帧)
AVBufferSinkParams* buffer_sink_params; // 配置输出帧的格式(如像素格式、音频采样格式等)
AVFilterContext* buffer_sink_ctx;
// 获取buffersink滤镜
AVFilter* buffer_sink_filter = avfilter_get_by_name("buffersink");
enum AVPixelFormat pix_fmts[] = {AV_PIX_FMT_YUV420P, AV_PIX_FMT_NONE}; // 指定允许的输出像素格式列表
buffer_sink_params->pixel_fmts = pix_fmts;
ret = avfilter_graph_create_filter(&buffer_sink_ctx, buffer_sink_filter, "out", NULL, buffer_sink_params, filter_graph);if (ret < 0) {printf("avfilter_graph_create_filter buffer_sink_params failed\n");return -1;
}AVFilter* split_filter = avfilter_get_by_name("split");
AVFilterContext* split_filter_ctx;
ret = avfilter_graph_create_filter(&split_filter_ctx, split_filter, "split", "outputs=2", NULL, filter_graph);if (ret < 0) {printf("avfilter_graph_create_filter split_filter failed\n");return -1;
}/*
out_w=iw:输出宽度=输入宽度
out_h=ih/2 输出高度=输入高度的一半
x=0 从x=0位置开始裁剪
y=0 从y=0位置开始裁剪
*/
AVFilter* crop_filter = avfilter_get_by_name("crop");
AVFilterContext* crop_filter_ctx;
ret = avfilter_graph_create_filter(&crop_filter_ctx, crop_filter, "crop", "out_w=iw:out_h=ih/2:x=0:y=0", NULL, filter_graph);if (ret < 0) {printf("avfilter_graph_create_filter crop_filter failed\n");return -1;
}AVFilter* vfilp_filter = avfilter_get_by_name("vflip");
AVFilterContext* vflip_filter_ctx;
ret = avfilter_graph_create_filter(&vflip_filter_ctx, vfilp_filter, "vflip", NULL, NULL, filter_graph);if (ret < 0) {printf("avfilter_graph_create_filter vflip_filter failed\n");return -1;
}AVFilter* overlay_filter = avfilter_get_by_name("overlay");
AVFilterContext* overlay_filter_ctx;
ret = avfilter_graph_create_filter(&overlay_filter_ctx, overlay_filter, "overlay", "y=0:H/2", NULL, filter_graph);if (ret < 0) {printf("avfilter_graph_create_filter overlay_filter failed\n");return -1;
}
split
滤镜的作用如下- 将单个输入流复制为多个完全相同的输出流
- 每个输出流包含与输入流完全相同的内容
- 常用于需要将同一个视频源用于多个不同处理流程的场景
crop
是裁剪滤镜,作用如下:- 从输入视频中裁剪出指定的矩形区域
- 可以精确控制裁剪的位置和尺寸
- 支持动态表达式计算裁剪参数
vflip
是垂直翻转滤镜,作用如下:- 将输入视频帧沿水平轴(X 轴)进行垂直翻转
- 实现“上下颠倒”的视觉效果
- 不改变视频的分辨率和像素合适
- 处理效率高,适合实时应用
滤镜组合
// 输入->split
ret = avfilter_link(buffer_src_ctx, 0, split_filter_ctx, 0);
if (ret != 0) {printf("avfilter_link buffer_src_ctx and split_filter_ctx failed\n");return -1;
}
// split主输出->overlay主输入
ret = avfilter_link(split_filter_ctx, 0, overlay_filter_ctx, 0);
if (ret != 0) {printf("avfilter_link split_filter_ctx and overlay_filter_ctx\n");return -1;
}
// split副输出->crop
ret = avfilter_link(split_filter_ctx, 1, crop_filter_ctx, 0);
if (ret != 0) {printf("avfilter_link split_filter_ctx and crop_filter_ctx\n");return -1;
}
// crop->vflip
ret = avfilter_link(crop_filter_ctx, 0, vflip_filter_ctx, 0);
if (ret != 0) {printf("avfilter_link crop_filter_ctx and vflip_filter_ctx\n");return -1;
}
// vflip->overlay副输入
ret = avfilter_link(vflip_filter_ctx, 0, overlay_filter_ctx, 1);
if (ret != 0) {printf("avfilter_link vflip_filter_ctx and overlay_filter_ctx\n");return -1;
}
// overlay->输出
ret = avfilter_link(overlay_filter_ctx, 0, buffer_sink_ctx, 0);
if (ret != 0) {printf("avfilter_link overlay_filter_ctx and buffer_sink_ctx\n");return -1;
}
输出 filter_graph 到文件
char* graph_str = avfilter_graph_dump(filter_graph, NULL);
FILE* graph_file = NULL;
graph_file = fopen("graph_file.txt", "w");
fprintf(graph_file, "%s", graph_str);
av_free(graph_str);
输出结果如下:
+----------+
| in |default--[768x320 1:1 yuv420p]--split:default
| (buffer) |
+----------++--------------+
overlay:default--[768x320 1:1 yuv420p]--default| out || (buffersink) |+--------------++---------+
in:default--[768x320 1:1 yuv420p]--default| split |output0--[768x320 1:1 yuv420p]--overlay:main| (split) |output1--[768x320 1:1 yuv420p]--crop:default+---------++--------+
split:output1--[768x320 1:1 yuv420p]--default| crop |default--[768x160 1:1 yuv420p]--vflip:default| (crop) |+--------++---------+
crop:default--[768x160 1:1 yuv420p]--default| vflip |default--[768x160 1:1 yuv420p]--auto_scaler_0:default| (vflip) |+---------++-----------+
split:output0----------[768x320 1:1 yuv420p]------main| overlay |default--[768x320 1:1 yuv420p]--out:default
auto_scaler_0:default--[768x160 1:1 yuva420p]--overlay| (overlay) |+-----------++---------------+
vflip:default--[768x160 1:1 yuv420p]--default| auto_scaler_0 |default--[768x160 1:1 yuva420p]--overlay:overlay| (scale) |+---------------+
帧处理
VFrame* in_frame = av_frame_alloc();
unsigned char* in_frame_buffer = (unsigned char*)av_malloc(av_image_get_buffer_size(AV_PIX_FMT_YUV420P, width, height, 1));
av_image_fill_arrays(in_frame->data, in_frame->linesize, in_frame_buffer, AV_PIX_FMT_YUV410P, width, height, 1);AVFrame* out_frame = av_frame_alloc();
unsigned char* out_frame_buffer = (unsigned char*)av_malloc(av_image_get_buffer_size(AV_PIX_FMT_YUV420P, width, height, 1));
av_image_fill_arrays(out_frame->data, out_frame->linesize, out_frame_buffer, AV_PIX_FMT_YUV420P, width, height, 1);in_frame->width = width;
in_frame->height = height;
in_frame->format = AV_PIX_FMT_YUV420P;
uint32_t frame_count = 0;while (1) {// 读取yuv帧数据if (fread(in_frame_buffer, 1, width*height*3/2, in_file) != width*height*3/2) {break;}// 设置帧数据指针in_frame->data[0] = in_frame_buffer;in_frame->data[1] = in_frame_buffer + width*height;in_frame->data[2] = in_frame_buffer + width*height*5/4;// 将帧送入滤镜系统if (av_buffersrc_add_frame(buffer_src_ctx, in_frame) < 0) {printf("av_buffersrc_add_frame failed\n");break;}// 从滤镜系统获取处理后的帧ret = av_buffersink_get_frame(buffer_sink_ctx, out_frame);if (ret < 0) {break;}// 写入处理后的帧if (out_frame->format == AV_PIX_FMT_YUV420P) {// 写入Y分量for (int i = 0; i < out_frame->height; i++) {fwrite(out_frame->data[0] + out_frame->linesize[0] * i, 1, out_frame->width, out_file);}// 写入U分量for (int i = 0; i < out_frame->height / 2; i++) {fwrite(out_frame->data[1] + out_frame->linesize[1] * i, 1, out_frame->width / 2, out_file);}// 写入V分量for (int i = 0; i < out_frame->height / 2; i++) {fwrite(out_frame->data[2] + out_frame->linesize[2] * i, 1, out_frame->width / 2, out_file);}}++frame_count;if (frame_count % 25 == 0)printf("process %d frame\n", frame_count);// 释放引用,不释放frame本身av_frame_unref(out_frame);
}
参考资料:https://github.com/0voice