FFmepg--28- 滤镜处理 YUV 视频帧:实现上下镜像效果

文章目录

功能概述

输入:768x320.yuv(YUV420P 格式,planar,无压缩)

输出:out_crop_vfilter.yuv(处理后的 YUV420P 帧)

处理逻辑:

将原始帧复制为两路;

第一路保持不变,作为主画面;

第二路裁剪出上半部分(高度 = 原高 / 2);

对裁剪结果进行垂直翻转(vflip);

将翻转后的图像叠加到主画面的下半区域(y = H/2);

输出合成帧。

最终效果:画面下半部分是上半部分的镜像(类似水面倒影)。

代码逐段解析

头文件与初始化
c 复制代码
#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>

引入 FFmpeg 核心库,重点使用 libavfilter 构建滤镜图(Filter Graph)。

c 复制代码
avfilter_register_all(); // ⚠️ 已弃用(FFmpeg ≥4.0 可删除)
AVFilterGraph* filter_graph = avfilter_graph_alloc();

分配空的滤镜图容器。

输入/输出文件处理
c 复制代码
FILE* inFile = fopen("768x320.yuv", "rb");
FILE* outFile = fopen("out_crop_vfilter.yuv", "wb");

读取原始 YUV 文件(768×320,YUV420P),写入处理结果。

构建 Filter Graph
Source Filter (buffer)
c 复制代码
sprintf(args, "video_size=768x320:pix_fmt=%d:time_base=1/25:pixel_aspect=1/1", AV_PIX_FMT_YUV420P);
avfilter_graph_create_filter(&bufferSrc_ctx, avfilter_get_by_name("buffer"), "in", args, NULL, filter_graph);

定义输入格式:尺寸、像素格式、时间基、像素宽高比。

Sink Filter (buffersink)
c 复制代码
enum AVPixelFormat pix_fmts[] = { AV_PIX_FMT_YUV420P, AV_PIX_FMT_NONE };
AVBufferSinkParams *params = av_buffersink_params_alloc();
params->pixel_fmts = pix_fmts;
avfilter_graph_create_filter(&bufferSink_ctx, avfilter_get_by_name("buffersink"), "out", NULL, params, filter_graph);
av_freep(&params); // ✅ 重要:释放 params 内存

限制输出仅支持 YUV420P。

中间滤镜链
滤镜 参数 作用
split outputs=2 将输入帧复制为两路
crop out_w=iw:out_h=ih/2:x=0:y=0 裁剪上半部分
vflip --- 垂直翻转图像
overlay x=0:y=H/2 将翻转图叠加到 y=H/2 位置

❗ 原代码 bug:"y=0:H/2" 是非法参数,应改为 "x=0:y=H/2" 或简写 "0:H/2"。

滤镜连接(Linking)
text 复制代码
[input] 
  └─> split ──┬─> overlay[main]
              └─> crop → vflip ──> overlay[overlay]
                                    ↓
                                  [output]

连接顺序:

c 复制代码
avfilter_link(bufferSrc_ctx, 0, splitFilter_ctx, 0);
avfilter_link(splitFilter_ctx, 0, overlayFilter_ctx, 0);     // 主画面
avfilter_link(splitFilter_ctx, 1, cropFilter_ctx, 0);        // 副本
avfilter_link(cropFilter_ctx, 0, vflipFilter_ctx, 0);
avfilter_link(vflipFilter_ctx, 0, overlayFilter_ctx, 1);     // 叠加层
avfilter_link(overlayFilter_ctx, 0, bufferSink_ctx, 0);
配置与调试
c 复制代码
avfilter_graph_config(filter_graph, NULL); // 验证图结构
char *graph_str = avfilter_graph_dump(filter_graph, NULL);
// 写入 graphFile.txt 用于可视化调试

若配置失败,说明滤镜链存在格式或尺寸不兼容问题。

帧处理循环
内存分配
c 复制代码
AVFrame *frame_in = av_frame_alloc();
uint8_t *buf_in = av_malloc(av_image_get_buffer_size(AV_PIX_FMT_YUV420P, w, h, 1));
av_image_fill_arrays(frame_in->data, frame_in->linesize, buf_in, ...);

手动分配连续内存,并按 YUV420P 组织为 AVFrame。

主循环
c 复制代码
while (fread(buf_in, 1, w*h*3/2, inFile) == w*h*3/2) {
    av_buffersrc_add_frame(bufferSrc_ctx, frame_in);   // 推入
    av_buffersink_get_frame(bufferSink_ctx, frame_out); // 拉出

    // 按 planar 格式写入文件(注意 linesize!)
    for (int i = 0; i < h; i++)
        fwrite(frame_out->data[0] + i * frame_out->linesize[0], 1, w, outFile);
    // 同理处理 U/V 平面...

    av_frame_unref(frame_out);
}

⚠️ 潜在问题:写入时使用 w 而非 linesize[0],若存在 padding 会出错。建议改用 frame_out->linesize[0] 作为 stride。

资源清理
c 复制代码
fclose(inFile); fclose(outFile);
av_frame_free(&frame_in); av_frame_free(&frame_out);
avfilter_graph_free(&filter_graph);

正确释放所有动态资源。

已知问题与改进建议

问题 风险 修复建议
overlay 参数错误 ("y=0:H/2") 滤镜行为异常或崩溃 改为 "x=0:y=H/2"
未释放 bufferSink_params 内存泄漏 调用 av_freep(&bufferSink_params)
YUV 写入忽略 linesize 图像错位(若有 padding) 使用 linesize[i] 作为行 stride
argv 类型错误 编译警告 改为 char **argv
avfilter_register_all() 旧版兼容性 FFmpeg ≥4.0 可安全删除

修正后关键代码片段 :

c 复制代码
// 1. 正确 overlay 参数
ret = avfilter_graph_create_filter(&overlayFilter_ctx, overlayFilter, "overlay",
                                   "x=0:y=H/2", NULL, filter_graph);

// 2. 释放 params
av_freep(&bufferSink_params);

// 3. 安全写入 YUV(示例:Y 平面)
for (int i = 0; i < frame_out->height; i++) {
    fwrite(frame_out->data[0] + i * frame_out->linesize[0],
           1, frame_out->width, outFile);
}

问题1: 送入滤镜图处理、再将结果写入输出文件的流程

问题2: 推入和拉出的数据有什么区别吗

推入(push)和拉出(pull)的数据在内容上通常不同------推入的是原始输入帧,拉出的是经过滤镜链处理后的输出帧。它们的尺寸、像素格式、内容甚至时间戳都可能发生变化。

推入的帧 (frame_in) vs. 拉出的帧 (frame_out)

特性 推入的帧 (frame_in) 拉出的帧 (frame_out)
来源 用户提供(文件/摄像头等) 滤镜图生成
内容 原始数据 经过滤镜处理(crop/vflip/overlay 等)
尺寸 输入尺寸(如 768×320) 可能改变(取决于滤镜)
像素格式 用户指定(如 YUV420P) 可能转换(受 buffersink 限制)
内存管理 用户分配/释放 FFmpeg 分配,用户需 unref
时间戳 用户设置(或默认) 可能被滤镜修改
用途 输入源 处理结果(用于显示/编码/保存)

问题3:yuv文件读取的计算过程?

YUV420P 是一种 planar(平面)格式的 YUV 表示,其内存布局如下:

  • Y 平面:包含所有亮度(Luma)数据,尺寸 = W × H 字节。
  • U 平面:包含色度(Cb)数据,水平和垂直方向都减半,尺寸 = (W/2) × (H/2) = W×H/4 字节。
  • V 平面:包含色度(Cr)数据,尺寸同样 = W×H/4 字节。

总字节数 = W×H + W×H/4 + W×H/4 = W×H × (1 + 0.25 + 0.25) = W×H × 1.5

内存排列顺序为:Y 全部 → U 全部 → V 全部

复制代码
|<-------- W×H -------->|<-- W×H/4 -->|<-- W×H/4 -->|  
[YYYYYYYYYYYYYYYYYYYYYY][UUUUUUUUUUUU][VVVVVVVVVVVV]  
 ↑                      ↑             ↑  
 data[0]                data[1]       data[2]  

假设:

  • 图像宽度 = W
  • 图像高度 = H
  • 起始地址 = frame_buffer_in

Y 平面(data[0])

c 复制代码
frame_in->data[0] = frame_buffer_in;  

Y 平面从缓冲区开头开始。

大小:W × H 字节。

U 平面(data[1])

c 复制代码
frame_in->data[1] = frame_buffer_in + W * H;  

U 紧跟在 Y 之后。

Y 占用 W×H 字节,所以 U 的起始偏移 = W×H。

V 平面(data[2])

c 复制代码
frame_in->data[2] = frame_buffer_in + W * H * 5 / 4;  

验证表达式:

  • Y 大小:W×H
  • U 大小:W×H / 4
  • V 的起始位置 = Y 起始 + Y 大小 + U 大小
    = 0 + W×H + (W×H)/4
    = W×H × (1 + 1/4)
    = W×H × 5/4

if (fread(frame_buffer_in, 1, in_widthin_height * 3 / 2, inFile) != in_width in_height * 3 / 2) {

break;

}

Y 分量:每个像素都有自己的 Y 值 → 分辨率 = W × H

U 和 V 分量:每 2×2 像素共享一个值 → 分辨率 = (W/2) × (H/2)

(1+ 1/4 + 1/4 ) = 1.5

问题4:处理后的 YUV420P 帧的 Y如何写入文件?

frame_out->data[0]

指向 Y 平面的起始地址(第一行 Y 数据的首字节)。YUV420P 格式中:

  • data[0] 存储 Y 分量(亮度)
  • data[1] 存储 U 分量(色度)
  • data[2] 存储 V 分量(色度)

frame_out->linesize[0]

表示 Y 平面每行的实际字节数(含可能的填充)。

  • 实际宽度可能小于 linesize[0],因内存对齐(如 SIMD/GPU 要求)会在行尾添加填充字节。
  • 示例:图像宽度 width = 768,但 linesize[0] = 800(32 字节对齐)。

地址计算逻辑

通过 linesize[0] * i 定位第 i 行的起始地址:

  • 第 0 行:data[0] + 0
  • 第 1 行:data[0] + linesize[0]
  • i 行:data[0] + linesize[0] * i

fwrite 参数说明

c 复制代码
fwrite(frame_out->data[0] + frame_out->linesize[0] * i, 1, frame_out->width, outFile)
  • ptr: 当前行有效数据的起始地址(跳过填充)
  • size: 1(每个像素占 1 字节)
  • count: frame_out->width(每行写入的有效像素数)
  • stream: 输出文件指针

code

c 复制代码
#include <stdio.h>

#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
//#include <libavfilter/avfiltergraph.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)
{
    int ret = 0;

    // 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;
    }

    int in_width = 768;
    int in_height = 320;

    // output yuv
    FILE* outFile = NULL;
    const char* outFileName = "out_crop_vfilter.yuv";
    fopen_s(&outFile, outFileName, "wb");
    if (!outFile) {
        printf("Fail to create file for output\n");
        return -1;
    }

    avfilter_register_all();

    AVFilterGraph* filter_graph = avfilter_graph_alloc();
    if (!filter_graph) {
        printf("Fail to create filter graph!\n");
        return -1;
    }

    // source filter
    char args[512];
    sprintf(args,
        "video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d",
        in_width, in_height, AV_PIX_FMT_YUV420P,
        1, 25, 1, 1);
    AVFilter* bufferSrc = avfilter_get_by_name("buffer");   // AVFilterGraph的输入源
    AVFilterContext* bufferSrc_ctx;
    ret = avfilter_graph_create_filter(&bufferSrc_ctx, bufferSrc, "in", args, NULL, filter_graph);
    if (ret < 0) {
        printf("Fail to create filter bufferSrc\n");
        return -1;
    }

    // sink filter
    AVBufferSinkParams *bufferSink_params;
    AVFilterContext* bufferSink_ctx;
    AVFilter* bufferSink = avfilter_get_by_name("buffersink");
    enum AVPixelFormat pix_fmts[] = { AV_PIX_FMT_YUV420P, AV_PIX_FMT_NONE };
    bufferSink_params = av_buffersink_params_alloc();
    bufferSink_params->pixel_fmts = pix_fmts;
    ret = avfilter_graph_create_filter(&bufferSink_ctx, bufferSink, "out", NULL,
                                       bufferSink_params, filter_graph);
    if (ret < 0) {
        printf("Fail to create filter sink filter\n");
        return -1;
    }

    // split filter
    AVFilter *splitFilter = avfilter_get_by_name("split");
    AVFilterContext *splitFilter_ctx;
    ret = avfilter_graph_create_filter(&splitFilter_ctx, splitFilter, "split", "outputs=2",
                                       NULL, filter_graph);
    if (ret < 0) {
        printf("Fail to create split filter\n");
        return -1;
    }

    // crop filter
    AVFilter *cropFilter = avfilter_get_by_name("crop");
    AVFilterContext *cropFilter_ctx;
    ret = avfilter_graph_create_filter(&cropFilter_ctx, cropFilter, "crop",
                                       "out_w=iw:out_h=ih/2:x=0:y=0", NULL, filter_graph);
    if (ret < 0) {
        printf("Fail to create crop filter\n");
        return -1;
    }

    // vflip filter
    AVFilter *vflipFilter = avfilter_get_by_name("vflip");
    AVFilterContext *vflipFilter_ctx;
    ret = avfilter_graph_create_filter(&vflipFilter_ctx, vflipFilter, "vflip", NULL, NULL, filter_graph);
    if (ret < 0) {
        printf("Fail to create vflip filter\n");
        return -1;
    }

    // overlay filter
    AVFilter *overlayFilter = avfilter_get_by_name("overlay");
    AVFilterContext *overlayFilter_ctx;
    ret = avfilter_graph_create_filter(&overlayFilter_ctx, overlayFilter, "overlay",
                                       "y=0:H/2", NULL, filter_graph);
    if (ret < 0) {
        printf("Fail to create overlay filter\n");
        return -1;
    }

    // src filter to split filter
    ret = avfilter_link(bufferSrc_ctx, 0, splitFilter_ctx, 0);
    if (ret != 0) {
        printf("Fail to link src filter and split filter\n");
        return -1;
    }
    // split filter's first pad to overlay filter's main pad
    ret = avfilter_link(splitFilter_ctx, 0, overlayFilter_ctx, 0);
    if (ret != 0) {
        printf("Fail to link split filter and overlay filter main pad\n");
        return -1;
    }
    // split filter's second pad to crop filter
    ret = avfilter_link(splitFilter_ctx, 1, cropFilter_ctx, 0);
    if (ret != 0) {
        printf("Fail to link split filter's second pad and crop filter\n");
        return -1;
    }
    // crop filter to vflip filter
    ret = avfilter_link(cropFilter_ctx, 0, vflipFilter_ctx, 0);
    if (ret != 0) {
        printf("Fail to link crop filter and vflip filter\n");
        return -1;
    }
    // vflip filter to overlay filter's second pad
    ret = avfilter_link(vflipFilter_ctx, 0, overlayFilter_ctx, 1);
    if (ret != 0) {
        printf("Fail to link vflip filter and overlay filter's second pad\n");
        return -1;
    }
    // overlay filter to sink filter
    ret = avfilter_link(overlayFilter_ctx, 0, bufferSink_ctx, 0);
    if (ret != 0) {
        printf("Fail to link overlay filter and sink filter\n");
        return -1;
    }

    // check filter graph
    ret = avfilter_graph_config(filter_graph, NULL);
    if (ret < 0) {
        printf("Fail in filter graph\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(bufferSrc_ctx, frame_in) < 0) {
            printf("Error while add frame.\n");
            break;
        }
        // filter内部自己处理
        /* pull filtered pictures from the filtergraph */
        ret = av_buffersink_get_frame(bufferSink_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产生的内存
    return 0;
}

总结

该程序展示了如何使用 FFmpeg 的 Filter Graph API 实现复杂的实时视频帧处理。通过组合 split、crop、vflip 和 overlay 滤镜,无需手动像素操作即可完成高级图像合成。

相关推荐
ganqiuye1 小时前
向ffmpeg官方源码仓库提交patch
大数据·ffmpeg·video-codec
草明1 小时前
ffmpeg 把 ts 转换成 mp3
ffmpeg
aqi003 小时前
FFmpeg开发笔记(九十二)基于Kotlin的开源Android推流器StreamPack
android·ffmpeg·kotlin·音视频·直播·流媒体
Together_CZ4 小时前
Cambrian-S: Towards Spatial Supersensing in Video——迈向视频中的空间超感知
人工智能·机器学习·音视频·spatial·cambrian-s·迈向视频中的空间超感知·supersensing
Android系统攻城狮4 小时前
Android16音频之设置音频属性AudioTrack.Builder().setAudioAttributes:用法实例(一百一十九)
音视频·android16·音频进阶
空影星4 小时前
轻量日记神器RedNotebook,高效记录每一天
python·数据挖掘·数据分析·音视频
Black蜡笔小新4 小时前
视频汇聚平台EasyCVR赋能石油管道计量站精准监控与安全管理
安全·音视频
nuoxin1145 小时前
GSV1011-富利威-HDMI芯片选型
arm开发·驱动开发·fpga开发·ffmpeg·射频工程
马剑威(威哥爱编程)14 小时前
鸿蒙6开发视频播放器的屏幕方向适配问题
java·音视频·harmonyos