ClearPix 分享基于 WebGPU 的本地视频超分优化,作者报告静态镜头加速约 4.7 至 7 倍,口播视频约 1.8 倍。文章强调先建立画质评估方法,再优化无需上传的视频处理流水线。
这一轮开始时,我们的视频超分工具有两件事是确定的:它确实很慢,我们也坦诚承认;至于效果究竟好不好,我们心里没底。前一个问题,理论上有办法解决。后一个问题,则是我们为什么花了一周时间,先造一把尺子,再去改优化代码。最终的结果是:静态镜头的超分速度提高了 4.7–7 倍;人物讲话这类画面,也就是背景静止、主体运动、过去完全吃不到优化收益的场景,加速比从原地踏步的 1.00× 提升到约 1.8×;输出画面也比上个月发布的版本更清晰,而且有指标为证。所有处理都在浏览器标签页内完成,视频始终留在你的电脑上。这里说的不是“处理后删除文件”:代码里根本就没有上传路径。
我们开发的是 ClearPix,也就是本系列第 1–10 篇介绍的浏览器端媒体工具箱:无需上传,不用服务器 GPU,通过 WebGPU 在浏览器内运行 Real-ESRGAN 级别的超分辨率处理。视频处理流水线本身,包括硬件解码、逐帧 AI 处理、硬件编码、音频直通和 O(1) 内存占用,是第 2 篇的主题。这一轮,我们要把这条流水线推到 4K,并回答视频工具真正重要的两个问题:有多快?效果有多好?
如今我们有一条内部规矩,这是花了不少代价才学来的:没有分阶段的耗时明细,就不做性能优化;没有能否决改动的指标,就不改画质。
视频处理路径已经满足了前半条。bench-video-upscale.mjs 会用真实视频片段,报告各个输出档位的逐帧耗时,单位是毫秒,并拆分出解码、推理、编码和贴图的耗时。但它缺少画质评分。它的 outputPsnr 衡量的是“开启帧复用”和“关闭帧复用”之间的偏差,是针对某项特定优化的回归保护,并不能说明画面与真实参考图像有多接近。所以,“我们的视频超分效果到底好不好?”这个问题,从科学角度说,当时全凭感觉。
解决办法是建立一个以真实参考图像为基准的测试脚本 bench-video-quality.mjs。我们把 800×500 的自然场景测试照片作为参考原图,分别按指定倍数下采样,合成低分辨率输入,倍数为 f = 2、4、8;随后使用与生产环境完全一致的图像尺寸处理流程,再把结果缩放回参考原图的尺寸,计算相对于原图的 PSNR、SSIM 和高频方差。随机种子相同,模型相同,分块相同,各行测试之间只有尺寸处理方式不同。
先说明一个解读数据时的注意事项,因为它差点把我们也骗过去:Real-ESRGAN 是一种感知型 GAN。与简单的 bicubic 缩放相比,PSNR 会明显低估它的表现。在干净的输入源上,f = 4 时,它的 PSNR 为 23.57 dB,而 bicubic 为 25.13 dB。因此,绝不能拿这些数字来论证“AI 和 bicubic 谁更好”。它们只适用于同一个模型在不同方案之间的比较,而这恰好是我们需要的。只有 PSNR 和 SSIM 这两个独立指标指向同一方向,我们才会相信结果。
这把尺子立刻量出了一个让人尴尬的问题。我们的尺寸处理流程会先把输入缩小到目标尺寸的四分之一:对于 4K 输出,输入宽度就是 960px,然后再交给模型。介于目标尺寸四分之一与输入实际分辨率之间的真实细节,在 AI 看到图像之前就被扔掉了。我们精心搭建的这条流水线,竟然在丢弃用户原本就有的像素。
给模型更大的输入,意味着它会在更大的画面上执行 ×4 放大,然后我们再把结果缩回目标尺寸。这听起来像是在白费力气:模型本来就已经在做放大了。但这并不是无用功。输入更大时,源图像中的真实细节得以保留,×4 放大会围绕这些细节生成更多细节;随后,将这个密集的 4× 图像场平均缩小到目标尺寸,又会悄悄平滑掉模型自身产生的伪影。
我们遍历了不同的输入缩放系数,其中 m = 1.0 是此前发布的方案;测试覆盖全部四张图像和三种下采样倍数,共十二组,最后取平均值:
有两行结果直接否定了对应的想法,还有一行需要贴上警示标签。先看最后一行:“直接按输入源本身的宽度喂给模型”,看起来比 m = 1.5 更划算,收益更大,成本更低。但它不是一个固定设置,而是一种比例关系。使模型输入宽度恰好等于源图像宽度的缩放系数,会随源图像与目标尺寸的比例而变化:f = 2 时是 2,f = 4 时是 1,f = 8 时是 0.5。在较小的一端,它就退化成了我们马上要否定的 m = 0.5。我们直接检验了它背后的直觉:“预先上采样时,不要超过源图像的尺寸。”结果在 f = 8 时,我们最终发布的尺寸处理方案依然在四张测试图像中的三张上领先 0.6–0.8 dB,另一张则落后 0.3 dB。预先上采样,并不是一个我们忘了删掉的 bug。
第一行结果埋葬了一个我们信了好几个月的说法:“给模型更低的分辨率,让它多干点活。”这样做平均损失 1.88 dB。这条路已经走不通,我们把它写进了操作手册,免得六个月后又有人提出来。
最终发布的是 m = 1.5,原因很朴素:十二组测试里,它没有任何一组出现负收益。f = 2 时提升 +1.88 dB,f = 4 时提升 +0.77 dB,f = 8 时提升 +0.14 dB。一项不会让效果倒退的优化,才适合作为选项交给用户。注意成本那一列:它随 AI 输入的面积变化,而不是随分块数量变化。1.5² 的理论值是 2.25,2² = 4,3² = 9;其中每个数据都是实测值,而不是推算出来的。
于是,这个选项成了一个三档旋钮。Fast 沿用旧的尺寸处理方案,Balanced 使用 m = 1.5,Best 使用 m = 2。到了 m = 2,收益曲线开始趋缓:时间接近翻倍,换来约 +0.7 dB 的额外提升。我们也不再让用户猜自己的电脑能承受哪一档。默认档位由设备能力和当前视频共同决定。如果没有 WebGPU,需要走每帧成本约为 25 倍的 WASM 回退路径,或者设备性能较低,就自动选择 Fast。其他情况下,我们会根据你实际拖入的文件,用视频时长和帧预算估算每个档位的完成时间;当估算结果接近决策边界、实际 fps 足以影响选择时,再探测真实 fps。在各自时间预算内,选择成本最高的那一档。一段六秒钟的 720p 视频,在配有独立 GPU 能力的笔记本电脑上默认选择 Best;一个四分钟的 4K 导出任务,则默认选择 Fast,不必让用户先搞懂背后的原因。你一改目标尺寸,按钮旁的预计时间就会立即更新。它不是营销数字,只是算术,而算出来恰好是准的。
速度问题的实际构成有点让人无奈:我们分析了所有环节,发现推理占每帧耗时的 97%。解码几乎不花时间,因为操作系统的媒体引擎会用专用硬件完成;编码也几乎不花时间;把分块贴到 canvas 上的耗时更是可以忽略。真正能动的杠杆只有一个,不是“让推理更快”,而是“别做推理”。视频里有大量根本没变化的帧。
我们已经为此实现了帧级复用:将每一帧与上一张真正送进模型的帧比较,如果画面没动,就复用之前的输出。有个实现细节让我们实打实地浪费了不少时间,而且很反直觉,值得再说一遍:不能用字节完全相等来判断。H.264 解码得到的静态帧,在比特层面并不完全相同。我们测得,连续两张“相同”帧的最大差值约为 9,平均差值为 0.02,因为编码器会在每一帧做出略有不同的决策。对于视觉上完全静止的视频,字节比较的命中率是 0%。解决办法是使用有锚点、有容差边界的比较:每一帧都与上一张真正经过推理的帧比较,而不是与前一帧比较,以免偏差逐帧累积;命中时复用锚点帧的输出,锚点本身不变。容差有界,不发生漂移。这与我们在第 7 篇视频去水印功能中最终采用的原则一致。
在 τ = 1.0 时,真实视频片段上的数据如下:
接下来是一个显而易见的追问,但我们很晚才想到问,晚得有点尴尬:真实视频通常长什么样?人物讲话、带光标的录屏、产品特写。它们都是主体运动的视频:背景静止,中间有东西在动。而这些场景在帧级复用上的命中率全是 0%。互联网上最常见的视频形态,恰好就是我们的优化无能为力的形态。只要一个人在动,整帧就会一直回到完整推理流程。
于是,我们把同样的思路下沉了一层。内部本来就会把每帧拆成 256px 的分块处理,现在每个分块都维护自己的锚点,单独执行有界比较。没有变化的分块,直接从之前的输出 canvas 复制核心区域,无需格式转换、无需推理,也无需经过贴图流程;发生变化的分块则照常送进模型。命中时仍然不更新锚点,因此漂移边界与帧级版本相同。
在合成视频上单独测量时,使用 24 帧、1080p 档位,也就是 4 个分块,结果分得很清楚:
最后一行是诚实性检查:当没有任何内容可以复用时,分块级方案会增加约 1% 的成本,也就是每个分块做一次比较,而且会正确地得出“没有可复用内容”的结果。中间那一行才是重点:实现 1.79× 加速时,相对于关闭复用的输出,偏差对应的 PSNR 为 49.77 dB,相当于不到一个 8-bit 灰阶级的误差。肉眼看不出来。而整帧静止时,帧级复用严格优于分块级复用,所以我们把两者叠加起来:先尝试帧级复用,再用分块级复用兜底。用户不用选,由画面决定。
有个坑差点跟着发布出去,它也很好地说明了为什么应该先建基准测试,再做功能。把分块贴回去时,必须使用核心区域。每个分块周围 8 像素的重叠区域,只是为了给模型提供上下文,不能写入 canvas。putImageData 的 dirty rectangle 坐标相对于源 ImageData,而不是目标位置,因此目标偏移必须使用分块原点;如果误用了核心区域原点,每个分块都会错位 32 像素。我们的 PSNR 门槛捕获了这个偏移:数值从 54 dB 掉到了 16.9 dB。视觉上,“边缘出现 32px 的重影”看起来只是略微发虚,恰恰就是那种会混进正式版本、一个月后才被用户发现的问题。
上面所有处理都在你的浏览器标签页中运行:通过 WebCodecs 进行硬件解码,通过 ONNX Runtime Web 在 WebGPU 上推理,通过 MediaBunny 进行封装,再用硬件编码输出 H.264。视频处理路径完全没有服务器组件。网络传输的内容只有下载的模型权重,以及少量不使用 cookie 的分析事件。模型权重只需下载一次,随后会缓存到 IndexedDB,工具之后就能离线使用;分析事件只包含事件名称,以及 backend、耗时秒数等粗粒度属性,完整清单公布在我们的隐私页面上。不传媒体文件,不传文件名,也不会把视频时长与个人关联起来。
这比“我们不存储你的视频”更进一步,而且外部也能验证:打开 devtools,拖入一段视频,观察 network 标签页。模型会下载,没有东西上传。没试过另一条路的人,常把这一点说成营销定位,但它确实让我们付出了代价。服务器 GPU 可以让长视频的问题迎刃而解,加个队列就能让用了五年的手机也处理 4K。但那也意味着,你的视频必须通过网络传过去才能做超分,这正是我们不愿做的交换。如果你要放大的内容,是你不愿通过邮件发给陌生人的东西,比如家庭视频、尚未发布的产品镜头,或者受 NDA 约束的客户素材,那么这个工具的全部工作,就应该在你已经信任的那台笔记本电脑上完成。
在我们的参考机器上,也就是使用 Apple GPU、WebGPU、处理运动画面时,每帧成本为:720p ≈ 115 ms,1080p ≈ 263 ms,2K ≈ 464 ms,4K ≈ 1.06 s。这些耗时中,推理都占 97%,这也是为什么本文讨论的是减少推理次数,而不是加快推理本身。
这一轮,我们还取消了视频时长的硬性上限。现在有文件大小限制,200 MB,以及帧预算;处理前会展示针对当前视频的真实时间估算。如果预计超过八分钟,我们会询问用户,而不是直接拒绝。现在,慢可以商量,不再只是一条报错。
把走不通的路记下来,免得重走:多 worker 并行没有效果,因为推理受内存带宽限制,增加 worker 只会争抢总线;增大分块没有明显收益;IOBinding 只能带来 1–2% 的提升,所以我们没有采用;降低模型输入分辨率会损失 1.88 dB;k = 0.7 时的细节注入对干净输入有帮助,却会重新放大真实素材中的压缩噪声,所以在没有感知指标证明其价值之前,它继续留在实验台上。再补一条:如果连续测试两个变体,第一个会悄悄把视频编码器的冷启动开销带进比较,让其中一个方案白白显得快了 2.17×。先预热,否则你会把错误的赢家发布出去。
无需账号,无需上传,输出没有水印:直接在浏览器中把视频超分到 4K。档位旋钮和时间估算就在目标尺寸选择器下方,帧预算会在你决定开始前告诉你真实情况。浏览器端图像超分工具共用同一个模型和分块引擎,免费的视频去水印工具共用这条完整流水线。如果你感兴趣的是媒体数据处理与传输这一层,可以读第 2 篇。
九秒钟的等待和塑料般的皮肤质感,其实是同一个抱怨。这一轮对应的领悟是:“快”“清晰”“私密”从来不是三个独立的问题,而是从三个角度观察同一条流水线。
本文是 ClearPix 工程系列的第 11 篇,介绍我们如何在 clearpix.org 构建免费、保护隐私、在浏览器内运行的媒体工具。
如需采取进一步行动,你可以考虑屏蔽此人和/或举报滥用行为。