用相邻帧亮度窗口减轻视频闪烁
FFmpeg deflicker 缓存连续帧及其平均亮度,每输出一帧就移出旧帧、加入新帧,用窗口内统计调整亮度。
工程问题#
延时摄影或曝光不稳定的视频可能在相邻帧之间出现亮度跳变。逐帧独立处理看不到邻近时间段的变化;保存整段视频又没有必要。
实际实现#
filter_frame 先填充帧队列与 luminance 数组。窗口就绪后,以最早一帧为输出对象,默认算术平均模式将窗口亮度均值与该帧亮度相除得到调整系数;处理完成后弹出旧帧,移动亮度记录,并加入新输入帧。
处理流程#
- 计算输入帧的平均亮度并填充固定大小窗口
- 对窗口内亮度求均值,计算最早一帧的亮度调整系数
- 处理该帧的亮度平面并保留其余所需信息
- 移出旧帧及其亮度,加入新帧,继续处理
源码片段#
固定版本 · 原文摘录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);窗口未满且未到 EOF 时保存平均亮度和帧引用;窗口满后,处理对象是队首旧帧。
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}遍历当前亮度窗口求算术平均,再除以队首帧亮度;这是每次 O(w) 计算,而非滚动和更新。
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 }调用选定模式的统计函数,将系数用于亮度平面,并复制其他所需平面。
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}释放队首帧,前移亮度记录,并将新帧的测量与引用加入窗口。
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 }EOF 后克隆队尾帧供 filter_frame 排出已有窗口;这与稳定输入阶段的正常推进有所不同。
展示窗口建立、统计、应用与推进,并单独保留 EOF 分支。完整函数还负责像素格式、亮度测量、元数据与资源清理。
收益与取舍#
工程分析带来的收益
- 仅保留有限时间窗口,无需缓存整个视频
- 每帧亮度测量结果随窗口复用,不必为已有帧重新计算平均亮度
付出的代价
- 必须缓存多帧,带来内存占用和输出等待;窗口大小也影响跟随亮度变化的速度
- 此实现每次重新遍历亮度数组求均值并 memmove,窗口统计更新为 O(w),不是 O(1) 滚动和;帧像素处理另计
- 真实场景切换和正常曝光变化也会改变窗口均值,窗口平滑不等于自动识别所有闪烁原因
可比较的选择
逐帧独立亮度校正等待较少,但没有邻近帧参照,容易保留或引入时间上的亮度不一致。
指数移动平均可以只保留聚合状态,但使用衰减历史而非固定窗口,响应和边界处理语义不同。
版本与适用范围#
对应 FFmpeg n7.1 deflicker 的稳定输入阶段,重点是默认算术平均模式;源码还提供几何平均、中位数等模式。输入结束时 request_frame 克隆队尾帧排空缓冲,短于窗口的视频与边界行为应按完整代码理解。
参考来源#
源码、官方文档与原始论文相关实现与比较#
2 个相关案例zlib 用哈希桶和按窗口位置组织的前驱链缩小候选范围,并以搜索预算和一次前瞻在速度与压缩率之间取舍。
Zstandard 的 double-fast 策略分别索引长、短前缀,在少量候选之间查验并延长真实匹配。