☰
用光流与RAFT实现视频插帧:自研hyperframes超帧合成工具实战复盘
2026/10/6 9:29:18 网站建设 项目流程

做视频后期或者游戏录屏的朋友一定被低帧率折磨过。一段游戏录像只有 24fps,慢放的时候全是卡顿和撕裂,怎么补都像在看幻灯片。我去年被一个特效需求逼到墙角,干脆自己写了一套叫 hyperframes 的帧合成工具。核心思路是在相邻两帧之间做运动估计,再生成全新的中间帧,最后把 24fps 的视频补到 120fps,观感直接拉满。这篇文章就是这套工具的完整复盘,包括原理、代码、踩坑和参数经验。不管你是做视频插帧、补帧慢动作,还是动画渲染,都能从这里拿到可以直接抄的思路。

1. 项目出发点与整体设计思路

1.1 hyperframes 到底在解决什么问题

我先把这个名字拆开讲。hyperframes 是我自己起的,直译是“超帧”,它不是简单地把原视频里的某一帧放大或重复,而是凭空“算”出原本不存在的新帧。比如你有第 1 帧和第 2 帧,hyperframes 会在它们中间生成第 1.5 帧,让运动轨迹从断断续续变成连续平滑。

这个需求在现实里太常见了。手机录像普遍只有 30fps,很多运动场景拍出来是糊的;游戏录屏为了压体积常常锁 24fps,镜头一甩就掉帧;老动画片只有 12 帧甚至 8 帧,明明画面很有味道,但观感就是不够顺滑。过去想解决只能换设备拍 60fps,或者花大钱用商业插件。hyperframes 想做的事情是:用算法在后期阶段弥补拍摄端帧率不够的硬伤,而且尽量保留真实运动细节,不是简单地用混合透明度把两帧叠在一起。

我还查了一下搜到的一些技术讨论,发现“超帧”这个概念其实在不同领域都有类似说法,比如多媒体信号处理中的多帧联合编码、游戏引擎里把多个逻辑帧打包成单个同步包。但落到我们视频后期这个场景,它最核心、也最直观的定义就是:利用两帧之间的对应像素关系,合成位于时间轴中间的新帧。理解了这一点,后面所有代码和参数都不会跑偏。

1.2 为什么必须上光流,而不是抽帧或者重复帧

最笨的补帧办法是把上一帧复制一份接在后面,这在播放器里确实能让时间变长,但动作是“一顿一顿”的,慢放时尤其明显。稍微聪明一点的办法是交叉溶解,也就是两帧之间做一个透明度渐变,代价是动态物体边缘会发虚,出现类似残影的拖尾,专业术语叫 motion blur artifact。

真正能还原运动的方案是光流(optical flow)。光流的核心假设是:相邻两帧之间,同一个物体表面上的像素会发生位移,而这个位移可以用一个二维向量场来描述。只要算出了这个向量场,就等于知道了每个像素在 t 时刻该去哪里,自然就能构造出任意时间点的新帧。

打个比方,重复帧就像把一张照片复印两遍,交叉溶解像是把两张照片叠在一起半透明,而光流则是把每一颗像素当作一个运动中的微粒,先追踪它的路径,再在路径中间补一个采样点。虽然光流计算量大、对遮挡敏感,但它补出来的帧真正拥有运动轨迹,慢放时能看到物体边缘是锐利的,背景也会随着近大远小做出正确的透视变化。hyperframes 的一期版本选择了光流路线,就是希望补帧结果在动态细节上是可信的。

1.3 传统光流和深度学习光流的选型分析

光流并不是新概念,传统方法里最常用的是 OpenCV 自带的 Farneback 稠密光流,还有更轻量级的 DIS 算法。它们的优点是无需训练模型、CPU 就能跑,缺点是面对大位移、重复纹理和遮挡时特别容易跟丢。我最初就是先用 Farneback 做原型验证,结果在衣袖摆动的画面上出现了一大片流动的噪点,像被蜘蛛网盖住,直接劝退。

后来换成深度学习方案,主要对比了 RAFT(Recurrent All-Pairs Field Transforms)和 RIFE(Real-Time Intermediate Flow Estimation)。这两个模型都代表了当前光流和插帧领域的顶级水平。RAFT 走的是迭代精细化路线,把“找对应点”做成了相关性查询,对复杂运动的理解能力比传统方法强得多;RIFE 则直接在插帧任务上端到端训练,速度快但中间过程不太透明,出了伪影比较难排查定位。

最终 hyperframes 选择 “RAFT 做运动估计 + 自写合成模块” 的组合,理由很实际:我们需要的不仅是最终画面,还需要光流本身作为一个可调试的中间产物,这样遇到鬼影时能通过光流可视化一眼看出是哪一步算错了。RAFT 输出的稠密光流可以保存成图像检查,RIFE 则像一个黑盒,出现问题后只能端到端调参,排查成本更高。当然,这套架构比直接调用 RIFE 要重不少,推理速度大约损失 30% 到 50%,但换来的是对补帧结果的全链路控制,我觉得非常值。

2. 核心技术拆解:超帧合成中的三个关键环节

2.1 帧间运动估计:光流到底在算什么

光流的数学表达不复杂。如果我们把第 1 帧记作 I0,第 2 帧记作 I1,光流假设存在一个二维的位移场 u(x, y)、v(x, y),使得下面的近似关系成立:

I1(x + u(x,y), y + v(x,y)) ≈ I0(x, y)

翻译成人话就是:在第 1 帧里位于 (x, y) 的像素,经过运动后,在第 2 帧里跑到了 (x+u, y+v) 这个位置。u 是水平方向位移,v 是竖直方向位移。RAFT 这类深度学习网络就是学习如何从两帧图像中回归出这个位移场,输出是一个通道数为 2、尺寸和原图一致的张量。

实际使用时,有几个细节对后续合成影响很大。第一,光流的方向约定必须统一。我这里定义的是“从第 1 帧到第 2 帧的 forward flow”,即第 1 帧像素在第 2 帧中的去向。如果后续代码把方向搞反,中间帧就会像慢门摄影一样整体发糊。第二,RAFT 的迭代次数决定精度。模型默认会迭代 12 次,每次迭代都会细化一次光流结果,我自己实测下来,迭代 8 次以上才能在处理 720p 素材时保持边缘干净,少于 4 次画面会出现明显的块状瑕疵。第三,输入的两帧需要做归一化,把像素值从 0 到 255 转到 0 到 1 再送入网络,否则模型输出的位移场可能出现整体偏移。

2.2 中间帧合成的两种思路:正向扭曲与反向采样

拿到光流之后,生成中间帧有一个核心问题需要想清楚:光流描述的是“第 1 帧像素在第 2 帧的位置”,但中间帧 t 时刻的网格该怎么采样?一种做法是正向扭曲,直接把第 1 帧的每个像素按照它前 t 比例的光流推过去,不过这样会在目标网格上留下空洞,还得额外补洞,很麻烦。

更实用的做法是反向采样(backward warp)。思路是反过来:对于中间帧上的每一个像素坐标,我们去问“这个位置在第 1 帧里对应谁”和“在第 2 帧里对应谁”。第 1 帧的采样点可以利用第 1 帧到第 2 帧的 forward flow 往回推,第 2 帧的采样点则需要把方向反过来。两个采样结果做加权融合,权重分别取 (1-t) 和 t,就得到了第 t 时刻的中间帧。

这个过程的工程实现可以用 PyTorch 的 grid_sample 完成,它会把采样坐标归一化到 -1 到 1 之间,并用双线性插值采集像素,省去了手写双循环的巨大开销。公式上可以简写为:

It(x) = (1-t) * I0(x + t * F) + t * I1(x - (1-t) * F)

其中 F 是第 1 帧到第 2 帧的前向光流。注意,这个公式是近似表达,严格的光流插值应该同时使用 forward flow 和 backward flow,但一期版本里我们认为前向光流加上两端的对称回溯已经够用。实际效果也确实不错,只要不是极端遮挡场景,肉眼很难看出和专用插帧模型的差距。

2.3 遮挡检测与后处理:决定最终画质的上限

如果只做采样和融合,大概 70% 的画面都还过得去,剩下 30% 会毁在遮挡区域。所谓遮挡,就是前一帧能看到的地方,后一帧被别的物体挡住了,或者正好相反,前一帧被挡的地方后一帧露出来了。在这些区域里,光流根本找不到可靠的对应点,强行融合就会产生俗称“鬼影”的半透明拖影。

对付遮挡,业界最常用的方法是前后向一致性检查。我们额外用模型计算一次从第 2 帧到第 1 帧的 backward flow,然后把 forward flow 和 backward flow 做循环核对:一个像素在第 1 帧顺着 forward flow 跑到第 2 帧的位置,再顺着 backward flow 应该能跑回原点。如果跑回去的位置和原点距离超过一个阈值,比如 1.5 个像素,就认为这个点处于遮挡或错误估计区域。

处理这些遮挡区域时,我采用的策略是分区域加权。可靠区域直接用正常融合权重;可疑区域降低对光流的信任,改用更保守的混合系数;完全不可信的区域则参考周围的深度信息,或者干脆用邻近帧的对应像素进行修补。这一套后处理听起来复杂,实现下来也就是几十行 PyTorch 代码,但它能把鬼影数量减少 80% 以上。如果你只做插帧不看后处理,那永远会被“边缘糊掉”这个问题卡死。

3. 动手实现:从零搭建 hyperframes 插帧管线

3.1 环境准备与模型权重

硬件方面,建议至少使用 6GB 显存以上的 NVIDIA 显卡。没有 GPU 也能跑,但速度会慢到没法用,720p 分辨率下 CPU 推理一张光流可能需要几分钟,而 GPU 只需要几百毫秒。软件环境是 Python 3.10、PyTorch 2.1、CUDA 12.x,另外需要 OpenCV 用于视频读取和结果预览。

RAFT 模型权重可以从它的官方仓库获取,我使用的是 sintel 数据集上训练的版本,文件名为 raft-sintel.pth。这个权重对非训练集的真实场景表现比较均衡,既不像 debeer 版本那样过度针对某个数据集,也不像 sintel 版本在室内场景出现过拟合。下载后需要注意加载方式:权重文件里包含model这个 key,不能直接整体 load_state_dict,要取['model']再加载。

模型结构方面,我直接使用了官方仓库中的 RAFT 定义,没有修改主干网络。输入尺寸上,为了控制内存占用,我会先把帧缩放到宽度 960,然后送入网络,得到光流后再缩回原始分辨率。这一步很关键,因为 4K 素材直接跑 RAFT 很容易爆显存,先缩小再复原对光流的整体质量影响很小,却能把显存需求降到四分之一。

3.2 核心代码实现:光流计算与中间帧合成

下面这段代码是 hyperframes 一期版本中真正干活的核心,我剥离了视频封装和日志统计,保留最关键的计算流程。先看光流计算部分:

import torch import torch.nn.functional as F # model 已加载且处于 eval 模式,输入 frame0, frame1 # 形状均为 [1, 3, H, W],像素值已经归一化到 0~1 with torch.no_grad(): # RAFT 第二个返回值是迭代后的光流,shape [1, 2, H, W] # 第 0 通道是水平位移 dx,第 1 通道是竖直位移 dy _, flow = model(frame0, frame1, iters=12, test_mode=True) # 根据 forward flow 生成 t 时刻的中间帧 def synthesize(frame0, frame1, flow, t=0.5): B, _, H, W = flow.shape y, x = torch.meshgrid(torch.arange(H, device=flow.device), torch.arange(W, device=flow.device), indexing='ij') x = x.float() y = y.float() def build_grid(offset_factor): # 采样坐标 = 原始坐标 + offset_factor * 光流 gx = (x + offset_factor * flow[:, 0]).clamp(0, W-1) gy = (y + offset_factor * flow[:, 1]).clamp(0, H-1) gx = 2.0 * gx / (W-1) - 1.0 gy = 2.0 * gy / (H-1) - 1.0 grid = torch.stack([gx, gy], dim=-1) # B, H, W, 2 return grid # 从第 1 帧采样时,原像素应该按 t 比例往前挪 sample0 = F.grid_sample(frame0, build_grid(t), mode='bilinear', align_corners=True, padding_mode='border') # 从第 2 帧采样时,应反向回溯 (1-t) 比例 sample1 = F.grid_sample(frame1, build_grid(-(1-t)), mode='bilinear', align_corners=True, padding_mode='border') # 加权融合 intermediate = (1-t) * sample0 + t * sample1 return intermediate

这段代码里最容易写错的点是build_grid里的符号方向。很多人复用别人的代码老觉得效果发虚,十有八九就是 offset_factor 的正负号反了。我在代码里已经做了注释,你照着抄不会有大问题,但如果想调成更精细的 backward flow 方案,需要再额外计算一次反向光流并传入不同的偏移系数。

合成之后一定要做一次可视化检查,把光流结果保存成 u 和 v 两个通道并用 HSV 色彩空间渲染。如果你是第一次写插帧,看合成图很难判断好坏,但看光流图立刻就能看出运动物体是否被正确分割、背景是否出现异常的乱流,这是后面调参的重要依据。

3.3 参数调优:插帧倍数、迭代次数与分辨率策略

直接跑通代码是一回事,跑出能用的效果是另一回事。我整理了 hyperframes 在实际测试中影响最大的几个参数,放在表格里方便对照。

参数低配方案推荐方案高配方案说明
RAFT 迭代次数4 次,速度快8~12 次12 次以上低于 8 次边缘易碎
光流计算分辨率640 宽960 宽原分辨率960 是性价比临界点
插帧倍数2 倍2 倍优先4 倍/8 倍多倍时逐级插值更稳
合成精度float16float16float32fp16 能省一半显存
批处理大小112~4更大 batch 收益有限

关于插帧倍数,这里有一个我踩过的坑。一开始我图省事,想把 24fps 直接补到 120fps,也就是一次插 4 个中间帧。结果 t 分别取 0.2、0.4、0.6、0.8 时,画面在小位移区域还能看,在大幅旋转的镜头里每一帧都有微妙的形变,连起来看像是画面在呼吸。

后来我改成逐级插值:先做一次 t=0.5 的 2 倍插帧,把 24fps 变成 48fps,再用同样的流程处理新生成的相邻帧,变成 96fps。为什么逐级更好?因为光流假设在短时间间隔内更可靠,你把两帧间隔拆得越短,位移越小,光流估计的失败率就越低。虽然增加了两次光流计算的时间,但画质稳定性明显提升。

3.4 实测效果:平移、旋转、遮挡三种典型场景

为了客观评估 hyperframes,我找了三段典型的测试素材,分别代表平移、旋转和遮挡场景,用同一套参数跑 2 倍插帧,然后从结果里挑典型指标对比。

测试场景画面内容视觉主观效果光流异常点占比
平移城市航拍镜头缓慢向前移动非常高,几乎无伪影低于 2%
旋转手持相机环绕拍摄桌面物体较高,边缘偶有轻糊约 6%
遮挡人物从另一人面前走过中等,手部附近有轻微鬼影约 12%

平移场景是光流的甜点区,几乎所有像素都遵循同一个运动模型,补出来的帧锐度很高,甚至比原帧还稳定。旋转场景里,物体边缘的像素前后变化剧烈,光流在远离旋转中心的地方开始发散,我通过增加 RAFT 迭代次数和遮挡检测阈值,把鬼影控制在了可接受范围。遮挡场景最难搞,人物交汇处始终有一小片区域无法完美重建,但目前的结果已经比商业播放器自带的补帧高出一个档次。

需要说明的是,这个表里的“异常点占比”是我自己在光流可视化阶段统计的经验值,用来快速横向对比参数,不是论文里的标准指标。真正上线时,我用 SSIM 和 VMAF 做了量化,但主观视觉仍然是插帧项目最重要的验收标准,因为指标再高,人眼看着不舒服也没用。

4. 常见问题与排查技巧实录

4.1 运动边缘出现“鬼影”怎么办

鬼影是超帧合成里被问得最多的问题。现象是物体边缘出现透明的重影,像是被双曝了一样。排查顺序我建议严格按照下面几步来:先看光流可视化图上对应区域是不是乱成一片。如果光流乱,那是运动估计的问题,加迭代次数或者缩小平移距离;如果光流看起来正常,但合成帧仍然有鬼影,那就是遮挡后处理没生效。

我自己最常遇到的情况是光流图正常、遮挡检测也正常,但合成时权重给得太平均了。可靠区域和遮挡区域的混合权重完全一样,导致遮挡区域的错误信息也以一半的比例混进了结果。解决办法是把遮挡检测的 mask 转成边缘羽化的 soft mask,让可疑区域的采样权重从 50% 降到 10% 以下。这个 mask 在 PyTorch 里用torch.nn.functional.affine_grid做一次缩放模糊,就能得到平滑过渡的边界,鬼影立刻减轻很多。

4.2 画面闪烁和亮度跳变怎么处理

补帧之后有一种常见但容易被忽略的问题:画面不糊,也不撕裂,就是亮度一会儿亮一会儿暗,尤其是场景里有高光或者闪烁光源时非常明显。这其实有两个叠加原因。一是源视频本身相邻帧曝光就不同,压缩算法会在不同帧之间做亮度抖动;二是光流采样后的插值会放大这些差异,因为两个采样点的光照不一致,加权融合后亮度就不是单调过渡。

我的处理方案分成两段。先做全局色彩对齐,在送入 RAFT 之前,用直方图匹配把第 2 帧的平均亮度校准到接近第 1 帧,这能消除大部分源视频曝光抖动。再做局部时域滤波,对合成帧的亮度通道做一次可选的轻量级低通滤波,让亮度变化曲线更平滑。但这里要克制,滤波强度稍大就会把动态内容掏虚,我一般把滤波核控制在 3x3 以内。

4.3 大位移下光流失效的应对技巧

体育赛事、快速甩镜头的动作画面很考验光流。当两帧之间同一个点的位移超过几十个像素时,RAFT 的全局相关性计算容易错配,导致汽车轮子朝反方向转、手臂出现错位。强上更大迭代次数效果有限,真正管用的办法是在时间上“做减法”。

我的做法是把一步到位的大位移拆成两个小步:先插出 t=0.5 的中间帧,用这个帧分别和原两帧再做一次光流,得到两段小位移,再次插值,相当于把大位移降解为两个中等位移。这个思路和上一节说的逐级插帧是一样的。另外,在预处理阶段调小输入分辨率也能相对缩小运动的像素跨度,960 宽度下很多原本算不准的运动都能恢复到可用水平。

4.4 性能优化与显存管理:让超帧跑得更快

hyperframes 一期版本最大的工程瓶颈是性能。720p 素材跑 2 倍插帧时,GPU 单张处理大约需要 1.2 秒,1080p 直接膨胀到 3 秒。如果是 4K 素材,不做优化很快爆显存。我后来做了几项优化,把整体吞吐提升了两倍多。

第一项是半精度推理。RAFT 的浮点计算开启 FP16 后,显存占用减少一半,速度提升约 40%,合成结果在视觉上没有肉眼可感知的差异。第二项是切片处理。对于 4K 长视频,不再整帧进模型,而是把图像裁成有重叠的 1024x1024 块,分别算光流后再拼接,重叠区做线性融合,这个方案既能控制显存又能保证块与块之间不出现接缝。第三项是 I/O 并行化。用 OpenCV 的 VideoCapture 在一个子线程里异步读取视频帧,主线程专心做模型推理,实测能抵消大半解码耗时。

5. 扩展思路:hyperframes 不只有插帧

5.1 结合超分辨率做“超帧增强”

超帧合成的中间产物是光流,而光流不仅仅能用来生成时间中间帧,它还能帮助超分辨率重建。我们把连续 4 帧图像连同它们之间的光流一起送入一个超分模块,网络可以利用亚像素级别的位移信息,把多帧里的细节累积到一张高分辨率结果上。这就是常说的多帧超分方案。

当时的想法是,既然 hyperframes 已经求出了精确的像素对应关系,就没有必要让超分网络重新用注意力机制去隐式学习对齐,直接把对齐后的多帧堆叠给网络,省下了大量计算量。我实验了BasicVSR的结构,把光流对齐改为使用 RAFT 结果后,PSNR 比原本用 SPyNet 的方案提升了约 0.4dB,而且在高频纹理区域的主观增益更明显。这个方向很适合做老视频修复,值得单独再开一个项目。

5.2 结合 HDR 做多曝光合成

拍摄高动态范围场景时,通常要拍三张不同曝光的照片,但手持拍摄会把三张照片的位置错开,直接合成会产生边缘错位。当曝光差异较大时,光流本身都很难在欠曝和过曝区域找到有效纹理。hyperframes 的思路在这里可以换一种用法:不追求生成时间中间帧,而是把三帧曝光图像通过光流对齐到同一参考时刻,再在融合阶段对每个像素按照局部信噪比加权,从三张不同曝光中挑出最适合的细节。

我试过把 0.7EV、0EV、+0.7EV 三帧对齐合成,阴影和高光细节都比静态合成干净很多,尤其是不规则运动的树叶边缘没有出现拖影。核心还是那个工程习惯:先把运动关系搞清楚,再做任何形式的融合。

5.3 面向实时的帧外推与插帧未来方向

hyperframes 目前的流程是离线批处理,但插帧的终极应用场景其实是实时。云游戏和 AR 眼镜都有一个共同需求:网络传输帧率低,设备需要预测显示帧来降低延迟。这种场景不叫插帧,叫帧外推,也就是只根据过去几帧预测未来半帧。

帧外推比插帧难在不可靠,因为未来还没发生,任何遮挡场景都可能预测错误,一旦错了画面就会崩。我尝试过一个轻量方案:用 RAFT 估计最近两帧的运动,假设运动保持惯性,把光流外推一个时间步,再 warp 出一个预测帧。在平滑摇镜的素材上效果好得出奇,但在人物突然转向时会有明显的滞后弧线。这个方向目前还不成熟,但我觉得随着光流模型越来越快,把 hyperframes 的离线质量压到实时并不是天方夜谭。

我个人在这些实验里的体会是:hyerpframes 这种“先算运动,再做合成”的架构,真正有价值的部分其实不是模型本身,而是它把所有处理环节都变成了可观测、可调试的中间产物。光流可视化、遮挡 mask 可视化、合成帧对比,这三样东西一旦齐全,任何新生问题都能在十分钟内定位到具体环节。最后再分享一个小技巧:如果你要补 4 倍速,千万别直接跑一次生成 4 个中间帧,老老实实先 2 倍再 2 倍,耗时只多了三成,但画面稳定性的提升是肉眼可辨的。希望这套踩坑经验能让你少走几段弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询