FFmpeg/libavfilter · deflicker

用相邻帧亮度窗口减轻视频闪烁

FFmpeg deflicker 缓存连续帧及其平均亮度,每输出一帧就移出旧帧、加入新帧,用窗口内统计调整亮度。

n7.1音视频处理核验 2026-10-12
相关概念

工程问题#

延时摄影或曝光不稳定的视频可能在相邻帧之间出现亮度跳变。逐帧独立处理看不到邻近时间段的变化;保存整段视频又没有必要。

实际实现#

filter_frame 先填充帧队列与 luminance 数组。窗口就绪后,以最早一帧为输出对象,默认算术平均模式将窗口亮度均值与该帧亮度相除得到调整系数;处理完成后弹出旧帧,移动亮度记录,并加入新输入帧。

处理流程#

  1. 计算输入帧的平均亮度并填充固定大小窗口
  2. 对窗口内亮度求均值,计算最早一帧的亮度调整系数
  3. 处理该帧的亮度平面并保留其余所需信息
  4. 移出旧帧及其亮度,加入新帧,继续处理

源码片段#

固定版本 · 原文摘录
libavfilter/vf_deflicker.cn7.1 · L353–L370
查看原文件
353static int filter_frame(AVFilterLink *inlink, AVFrame *buf)354{355    AVFilterContext *ctx = inlink->dst;356    AVFilterLink *outlink = ctx->outputs[0];357    DeflickerContext *s = ctx->priv;358    AVDictionary **metadata;359    AVFrame *out, *in;360    float f;361    int y;362363    if (s->q.available < s->size && !s->eof) {364        s->luminance[s->available] = s->calc_avgy(ctx, buf);365        ff_bufqueue_add(ctx, &s->q, buf);366        s->available++;367        return 0;368    }369370    in = ff_bufqueue_peek(&s->q, 0);
filter_frame:填充连续帧窗口

窗口未满且未到 EOF 时保存平均亮度和帧引用;窗口满后,处理对象是队首旧帧。

libavfilter/vf_deflicker.cn7.1 · L205–L218
查看原文件
205static void get_am_factor(AVFilterContext *ctx, float *f)206{207    DeflickerContext *s = ctx->priv;208    int y;209210    *f = 0.0f;211212    for (y = 0; y < s->size; y++) {213        *f += s->luminance[y];214    }215216    *f /= s->size;217    *f /= s->luminance[0];218}
get_am_factor

遍历当前亮度窗口求算术平均,再除以队首帧亮度;这是每次 O(w) 计算,而非滚动和更新。

libavfilter/vf_deflicker.cn7.1 · L378–L386
查看原文件
378    s->get_factor(ctx, &f);379    if (!s->bypass)380        s->deflicker(ctx, in->data[0], in->linesize[0], out->data[0], out->linesize[0],381                     outlink->w, outlink->h, f);382    for (y = 1 - s->bypass; y < s->nb_planes; y++) {383        av_image_copy_plane(out->data[y], out->linesize[y],384                            in->data[y], in->linesize[y],385                            s->planewidth[y] * (1 + (s->depth > 8)), s->planeheight[y]);386    }
filter_frame:应用亮度调整系数

调用选定模式的统计函数,将系数用于亮度平面,并复制其他所需平面。

libavfilter/vf_deflicker.cn7.1 · L403–L410
查看原文件
403    in = ff_bufqueue_get(&s->q);404    av_frame_free(&in);405    memmove(&s->luminance[0], &s->luminance[1], sizeof(*s->luminance) * (s->size - 1));406    s->luminance[s->available - 1] = s->calc_avgy(ctx, buf);407    ff_bufqueue_add(ctx, &s->q, buf);408409    return ff_filter_frame(outlink, out);410}
filter_frame:移出旧帧并加入新帧

释放队首帧,前移亮度记录,并将新帧的测量与引用加入窗口。

libavfilter/vf_deflicker.cn7.1 · L418–L430
查看原文件
418    ret = ff_request_frame(ctx->inputs[0]);419    if (ret == AVERROR_EOF && s->available > 0) {420        AVFrame *buf = ff_bufqueue_peek(&s->q, s->available - 1);421        if (!buf)422            return AVERROR(ENOMEM);423        buf = av_frame_clone(buf);424        if (!buf)425            return AVERROR(ENOMEM);426427        s->eof = 1;428        ret = filter_frame(ctx->inputs[0], buf);429        s->available--;430    }
request_frame:输入结束处理

EOF 后克隆队尾帧供 filter_frame 排出已有窗口;这与稳定输入阶段的正常推进有所不同。

展示窗口建立、统计、应用与推进,并单独保留 EOF 分支。完整函数还负责像素格式、亮度测量、元数据与资源清理。

收益与取舍#

工程分析

带来的收益

  • 仅保留有限时间窗口,无需缓存整个视频
  • 每帧亮度测量结果随窗口复用,不必为已有帧重新计算平均亮度

付出的代价

  • 必须缓存多帧,带来内存占用和输出等待;窗口大小也影响跟随亮度变化的速度
  • 此实现每次重新遍历亮度数组求均值并 memmove,窗口统计更新为 O(w),不是 O(1) 滚动和;帧像素处理另计
  • 真实场景切换和正常曝光变化也会改变窗口均值,窗口平滑不等于自动识别所有闪烁原因

可比较的选择

逐帧独立亮度校正等待较少,但没有邻近帧参照,容易保留或引入时间上的亮度不一致。

指数移动平均可以只保留聚合状态,但使用衰减历史而非固定窗口,响应和边界处理语义不同。

版本与适用范围#

对应 FFmpeg n7.1 deflicker 的稳定输入阶段,重点是默认算术平均模式;源码还提供几何平均、中位数等模式。输入结束时 request_frame 克隆队尾帧排空缓冲,短于窗口的视频与边界行为应按完整代码理解。

参考来源#

源码、官方文档与原始论文
01
filter_frame:填充连续帧窗口

libavfilter/vf_deflicker.c · filter_frame:填充连续帧窗口

源码n7.1
02
get_am_factor

libavfilter/vf_deflicker.c · get_am_factor

源码n7.1
03
filter_frame:应用亮度调整系数

libavfilter/vf_deflicker.c · filter_frame:应用亮度调整系数

源码n7.1
04
filter_frame:移出旧帧并加入新帧

libavfilter/vf_deflicker.c · filter_frame:移出旧帧并加入新帧

源码n7.1
05
request_frame:输入结束处理

libavfilter/vf_deflicker.c · request_frame:输入结束处理

源码n7.1

相关实现与比较#

2 个相关案例
4 个匹配案例