
做视频后期或者游戏录屏的朋友一定被低帧率折磨过。一段游戏录像只有 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 做原型验证结果在衣袖摆动的画面上出现了一大片流动的噪点像被蜘蛛网盖住直接劝退。后来换成深度学习方案主要对比了 RAFTRecurrent All-Pairs Field Transforms和 RIFEReal-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 帧里跑到了 (xu, yv) 这个位置。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, iters12, test_modeTrue) # 根据 forward flow 生成 t 时刻的中间帧 def synthesize(frame0, frame1, flow, t0.5): B, _, H, W flow.shape y, x torch.meshgrid(torch.arange(H, deviceflow.device), torch.arange(W, deviceflow.device), indexingij) 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), modebilinear, align_cornersTrue, padding_modeborder) # 从第 2 帧采样时应反向回溯 (1-t) 比例 sample1 F.grid_sample(frame1, build_grid(-(1-t)), modebilinear, align_cornersTrue, padding_modeborder) # 加权融合 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 时画面在小位移区域还能看在大幅旋转的镜头里每一帧都有微妙的形变连起来看像是画面在呼吸。后来我改成逐级插值先做一次 t0.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 的全局相关性计算容易错配导致汽车轮子朝反方向转、手臂出现错位。强上更大迭代次数效果有限真正管用的办法是在时间上“做减法”。我的做法是把一步到位的大位移拆成两个小步先插出 t0.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 倍耗时只多了三成但画面稳定性的提升是肉眼可辨的。希望这套踩坑经验能让你少走几段弯路。