☰
老动画画质修复实战:APISR+Vulkan+WinForms打造AI超分工具
2026/10/16 11:01:33 网站建设 项目流程

最近在家用投影仪给孩子放一部八十年代末的老动画,孩子盯着屏幕问了一句"为什么公主的脸是糊的",我突然意识到:我们这代人习以为常的"经典老番",对新一代孩子来说,画质门槛已经高到劝退的程度。480p的解析度在4K大屏上被放大六倍,线条全部发虚,色块边缘全是锯齿和色斑,观感确实很糟糕。

那周我动手做了个视频超分工具,用 C# WinForms 做界面,APISR 的模型做超分推理,Vulkan 做GPU执行后端,最终把一个拖进去的老旧动画文件变成分辨率翻倍、线条干净、色带减轻的修复版。这篇文章就是把整个复盘写下来:为什么这么选型、模型如何落地到桌面应用、中间踩了哪些坑、最终的参数和效果对照。目标读者是手里有老片源、想在家里自己修收藏的朋友,以及想学习"深度学习模型如何跑到桌面客户端"这条路线的人。

1. 老番为什么糊:先搞清楚画质差在哪,再谈修复

在看任何超分方案之前,必须先明白老动画画质差到底差在哪。如果只是"分辨率低",那用播放器的插值算法就能解决,完全不需要深度学习。

1.1 老动画画质损伤的四类源头

第一类是最直观的:源分辨率低。老动画从胶片转数字或者早期DVD母带,标准就是480p甚至更低。放到1080p屏幕上等于每四个像素猜一个,放大必然糊。

第二类是压缩损伤。DVD时代普遍是MPEG-2编码,码率又不高,色度抽样4:2:0,导致颜色过渡区域出现块状感,边缘出现振铃和蚊噪。这和我们看低码率在线视频的感觉类似。

第三类是来源劣化。很多老番根本没有干净的原始数字母带,市面上流传的版本是多年前从录像带转的,或者经过多次转压。这类片源自带噪声、扫描线、亮度抖动,甚至还有隔行扫描残留。

第四类是动画特有的:线条与色块的退化。动画画面不是自然场景,而是大面积平涂色块、锐利线条、网点纹理。低分辨率下线条变成锯齿,渐变背景变成色带,网点变成一团灰。普通的自然影像超分模型面对这些内容会手足无措,因为它没见过"明明是平的画面却必须保持边缘锐利"这种组合。

1.2 为什么普通锐化/插值救不了老番

播放器自带的Lanczos、Bicubic插值,本质是数学插值。它能增加像素数量,但不能增加信息量。你看到的边缘"清晰",其实只是把原有锯齿放大得更锐利,同时把色块边缘衬出一圈白边。

锐化滤镜则是另一条路:它会把每个边缘都加一条高对比过渡带,客观上"看起来清楚了",但代价是物体边缘出现光晕(halo),噪点也被放大。老动画本来就有压缩噪声,锐化一次等于噪声翻倍。这两类方法都解决不了"图像中间缺少信息"的问题,它们只是让缺失信息的方式变得不那么扎眼。

1.3 深度学习超分与传统方案的差异

超分模型做的事情本质是猜测:它学习了几百万张高清动画图片,知道"一个动漫人物的线稿边缘在4倍分辨率下应该长什么样",所以当它看到一幅低分辨率、模糊、带压缩痕迹的画面时,它能基于先验知识把缺失的高频细节补回去。

这和插值有本质区别。插值是按数学规则填空,模型是按视觉记忆再创作。所以超分能把色块边缘修平整、能把锯齿修成光滑弧线、能消除部分色带,这些是任何传统算法做不到的。

自然,这种"再创作"也会带来副作用,比如细节过度平滑、涂抹感、或者偶尔出现奇怪纹理。所以后文会讨论模型选型和放大倍率控制,这是把工具用好最关键的一步。

2. 技术选型复盘:为什么是 APISR + Vulkan + WinForms

把技术栈拆开说,这三块各自解决了什么问题,以及替代方案为什么被我否掉。

2.1 APISR 在动漫超分里比 Real-ESRGAN 赢在哪

Real-ESRGAN 是很强的通用图像修复模型,但对动漫场景,它有一个先天短板:它的退化模型主要模拟自然图像的真实退化,对动漫的线稿、色块、网点纹理理解不够深。用 Real-ESRGAN 放大老动画,线条容易"糊成一坨",色块交界处容易出现不均匀的过渡。

APISR 是专门为动漫设计的开源超分工具箱,它的核心改进在两点:训练数据覆盖了大量真实动画退化场景,包括压缩伪影、降采样、噪声;训练目标里有明确的"恢复线与色块"的约束。实际效果上,APISR 对线条边缘的保持、对色带的抑制,都明显比通用模型更适合老动画,尤其是 2x 放大档,主观观感非常自然。

2.2 Vulkan 后端:为什么不直接用 CUDA

超分模型最常见的推理框架是 PyTorch + CUDA。但问题很现实:你想让普通亲戚朋友用上这个工具,不可能让他先装 Python、再装几 GB 的 CUDA 和 PyTorch,还得有一块 N 卡。

用 Vulkan 做推理后端,意味着任何现代显卡都可以参与计算:N 卡、A 卡、核显都行。Vulkan 是图形驱动自带的,不需要额外安装运算框架,不需要用户配置环境。对个人工具来说,这是"能不能传给朋友用"的关键。

我在对比 ONNX Runtime 的多个执行提供程序时发现,Vulkan EP 的算子覆盖度和性能已经足够跑 APISR 这种以卷积为主的模型,而且它通过 C# 的 NuGet 包就能直接集成,不需要写任何底层代码,非常适合桌面工具场景。

2.3 WinForms 在现在的桌面工具开发里仍是最务实的选择

有人会问,为什么不用 WPF、Avalonia 或者直接做个网页界面?WinForms 的逻辑很简单:它是 Windows 下最成熟、依赖最少、打包体积最小的 UI 方案。

这个工具的核心逻辑是文件选择、参数配置、进度显示、日志输出,不需要复杂的动画和布局。WinForms 在这类场景下开发速度极快,发布时单文件发布加几个 DLL 就能跑,对最终用户完全透明。种过地的朋友都知道,锄头好不好使,关键是省力、顺手、不挑地,WinForms 就是这个角色的锄头。

3. 从模型到界面:一个可运行的桌面超分工具全流程

下面就是整个项目的主干流程。按这条路线走,你可以从零拼出一个能用的工具。

3.1 第一步:在Python环境里导出一个 ONNX 模型

APISR 的模型是 PyTorch 格式,桌面端不能用 PyTorch 直接跑,所以先导成 ONNX。这一步需要一个临时 Python 环境,但日常使用工具的人不需要这个环境,这点后面会解释。

导出过程大致如下:加载 APISR 模型权重,构造一个示例输入,用torch.onnx.export导出,指定 opset 版本。这里有两个经验值:

  1. opset 建议用 16 或 17。太高可能遇到 ONNX Runtime 各 EP 支持滞后,太低有些算子导出不了。
  2. 输入尺寸尽量用固定 H/W。Vulkan EP 对动态尺寸支持比较弱,固定 960x540 或者 640x360,可以避免很多运行时问题。 如果是整段视频,需要提前用 ffprobe 读取视频宽高,再对宽高做对齐。为了避免尺寸不满足模型输入倍数要求,把输入尺寸向上取到最近的 16 的倍数。

导出后用onnxsim做一次图精简,可以去掉一些冗余算子,这能明显降低在 Vulkan EP 上出问题的概率。我实际遇到的 PixelShuffle 算子兼容问题,就是在简化之后解决的。

3.2 第二步:在 C# 里接 ONNX Runtime 的 Vulkan 执行提供程序

C# 侧核心是Microsoft.ML.OnnxRuntime这个 NuGet 包。它自带一个Vulkan执行提供程序选项,需要还额外引用Microsoft.ML.OnnxRuntime.Vulkan运行时包。

初始化会话的代码骨架大致如下:

using Microsoft.ML.OnnxRuntime; var options = new SessionOptions(); options.AppendExecutionProvider_Vulkan(deviceId: 0); options.GraphOptimizationLevel = GraphOptimizationLevel.ORT_ENABLE_ALL; options.EnableMemoryPattern = false; using var session = new InferenceSession("apisr_2x.onnx", options);

值得注意的是EnableMemoryPattern = false。默认开启内存模式时,Vulkan EP 在部分动态尺寸输入上会出现内存分配异常。这个参数是我调试时偶然发现的关键开关,不关掉的话,第二次推理就会报错。

推理时输入是 NCHW 的 float 张量,值域是 0-1,通道顺序是 RGB。每个像素由 Bitmap 转成浮点数组,填充张量,调用session.Run,再把输出张量转回 Bitmap。核心代码:

using Microsoft.ML.OnnxRuntime.Tensors; var input = new DenseTensor<float>(new[] { 1, 3, inputHeight, inputWidth }); // 将每个像素的RGB值归一化后填入input using var inputs = new List<OnnxValue> { OnnxValue.CreateFromTensor(input) }; using var outputs = session.Run(inputs); var output = outputs[0].AsTensor<float>(); // 将output转为Bitmap

肉眼可见的问题是这个循环很朴素,逐帧分配张量会有不少 GC 压力。但实测下来,普通 720p 输入、2x 输出,每帧推理时间在 N 卡主流型号上大约 30-60 毫秒,GC 压力可以接受。如果以后要处理 4K 输出,就得换 IOBinding 复用内存,我在第 4 节再细说。

3.3 第三步:用 FFmpeg 抽帧与封装,拼出完整视频处理管线

视频处理不能直接喂给模型,需要先抽帧。我的方案是用 FFmpeg 把视频拆成 PNG 序列,逐帧处理,最后再拿 FFmpeg 封装回视频。

抽帧:

ffmpeg -i input.mkv -vsync 0 frame_%06d.png

这个命令会按原视频的时间戳抽取每一帧,而不是按默认的 25fps 去重采样。很多人在抽帧时没加-vsync 0,导致动画这种 24/23.976fps 的片源抽出来的帧数不对,最后视频时长会变。

音频单独抽取:

ffmpeg -i input.mkv -map 0:a? -c:a copy audio.mka

处理完所有帧后,再合成:

ffmpeg -framerate 23.976 -i frame_%06d.png -i audio.mka -c:v libx265 -crf 18 -preset slow -pix_fmt yuv420p10le -c:a copy output.mkv

这里额外建议用 10bit 的 x265 输出。动画是色带重灾区,8bit 输出时即使超分模型修掉了大部分色带,编码环节也可能重新压出色带。10bit 的改善非常直观,现代设备播放 10bit HEVC 几乎没有兼容性问题。

-framerate必须和原视频帧率一致,做法是先用 ffprobe 读取r_frame_rate字段,比如24000/1001,直接传给 FFmpeg。

3.4 第四步:WinForms 界面与批量任务设计

界面我没有做得很复杂,一个主窗口包含:

  • 视频文件拖放区域
  • 模型文件选择下拉框(2x 修复、2x 均衡、4x 等等)
  • 输出目录选择
  • 进度条和日志框
  • 开始/取消按钮

用BackgroundWorker或者Task.Run跑后台任务,UI 线程只做进度更新。事实证明,这种异步架构对长视频很重要,我最早图省事直接在 UI 线程里跑循环,结果一集动画处理到一半窗口就假死,只能在任务管理器里结束。后来改成后台线程,加CancellationTokenSource做取消,用户随时可以停止任务。

批量处理也不难:用户拖入多个文件,任务队列按顺序执行,每个文件独立抽帧、独立清理临时目录。WinForms 的ListBox配上任务状态枚举,就能做出一个足够好用的调度界面。

如果想把工具交给完全不懂技术的家人,建议做成单文件发布,把 ONNX 模型放到程序目录的 models 子文件夹,用户只需要选视频、点开始。但凡要用户自己配路径、装依赖,工具就会被打入冷宫。

4. 调通之后才真正开始的坑:Vulkan 推理的九个小教训

这个工具最折磨人的阶段不是写界面,而是把模型用 Vulkan 跑通。表面上看 ONNX Runtime 封装得很好,实际运行中踩了不少坑,我把最典型的几条排查链路完整记录下来。

4.1 第一次跑通是运气:绿屏与黑屏的排查链路

第一个典型问题是输出全绿或全黑。具体现象:第一帧输出正常,第二帧开始输出变成一团绿色,或者偶尔整帧黑屏。

排查过程是这样的。先怀疑张量初始化有误,打印输入 Tensor 的均值方差,正常。再怀疑模型输出有问题,换成 CPU EP 跑同一帧,输出正常。这时候问题就锁定了 Vulkan EP 与 CPU EP 的行为差异。

继续追,发现session.Run每次都会创建一个新的输出张量,而 Vulkan EP 内部对这个张量的内存复用策略有问题。解决方式有两个:

  1. 使用IOBinding绑定固定的输入输出缓冲区,由我们自己管理内存复用;
  2. 或者更简单:每次都走Run,但每次Run之后要把输出张量立即Dispose并且把inputs列表也Dispose。

我一开始用的就是第二种,因为没有在一个循环里正确释放OnnxValue,导致显存里的缓冲区被反复覆盖。把using的作用域严格限制在每次迭代内之后,绿屏问题消失。

还有一个黑屏问题与颜色通道顺序有关。ONNX 模型的输入通道是 RGB,但我从 System.Drawing.Bitmap 读像素时拿到的是 BGR。这个错位会让画面变成一块诡异的灰蓝色或者暗红色。排查时我打印了第一帧的 RGB 均值和输出均值,发现输出通道顺序异常,才意识到是通道顺序的问题。

4.2 显存与性能调优:复用、限流与缓存

第二个大坑是显存增长。跑完一集动画后,任务管理器看到显存占用不降。这说明每一帧创建的 GPU 缓冲区没有被释放干净。

解决方式是用IOBinding手动管理 I/O。绑定输入输出到固定的OrtValue后,每次推理都在同一块显存区域里执行,不再反复分配:

var ioBinding = session.CreateIoBinding(); // 绑定输入张量 ioBinding.BindInput("input", inputOrtValue); // 绑定输出张量,指定到设备 ioBinding.BindOutput("output", device); session.Run(ioBinding); var output = ioBinding.GetOutputValues()[0];

这样显存占用就稳定在一个固定值。注意BindOutput时如果不指定设备,输出会落在 CPU 上,反而引入一次不必要的拷贝。指定到 GPU 后用GetOutputValues拿结果,再手动Dispose,整体性能更好。

性能上还有一个容易忽略的点:Vulkan EP 第一次推理会做 shader 编译,耗时可能达到几十秒甚至分钟级。这是正常的,不是卡死。如果想让第二次启动变快,可以开启SessionOptions缓存。具体做法是把session配置成SetOptimizationLevel并保存缓存目录。但 ONNX Runtime 的 Vulkan 缓存机制在不同版本间不太稳定,我最终没有依赖它,而是在程序启动时先跑一次 1 帧的"预热推理",让用户知道这个启动等待是正常的。

至于要不要开多线程并行处理帧,我建议不要。Vulkan 推理已经占满 GPU,CPU 侧并行反而会因为张量拷贝和同步导致整体变慢。用串行循环加每 10 帧打印一次进度日志,已经足够稳定。

4.3 不同模型档位在动画上的玄学差异

APISR 提供了不同侧重的模型:有的更强调细节保留,有的更强调去伪影。这个选择直接影响观感。

我做的对比测试里,对于 DVD 画质的老动画,细节保留型模型在 2x 档效果最好:线条干净、背景纹理自然、人物肤色过渡均匀。如果上 4x,虽然分辨率提升明显,但画面会出现一种"过度锐化"的油亮感,尤其是人物的头发边缘,像是被描了一遍黑边。

对于本身噪声很大的录像带转数码片源,分子先做一次轻度的去噪会比直接超分更好。我后来在管线里加了可选的预处理:先用一个轻量去噪模型跑一遍,再超分。虽然耗时多一倍,但最终效果平静很多,人物的皮肤不再是"噪点肌肉"。

我的结论是:超分不是放大倍数越大越好,而是修复到目标分辨率的合适档位最好。家用 1080p 或 2K 显示设备,480p -> 2x 完全够用;只有面向 4K 投影仪,才需要 4x。

4.4 配套的音画处理细节

处理老番时还有一个容易翻车的点是封装参数。很多人只处理了视频流,忽略了原视频可能包含软字幕、多条音轨、章节信息。用-map 0保留所有流,再额外指定视频流替换,可以避免丢字幕丢音轨。

我实际用的是:

ffmpeg -i frame_%06d.png -i input.mkv -map 1 -map 0:v -c:v libx265 -crf 18 -preset slow -pix_fmt yuv420p10le -c:a copy -c:s copy output.mkv

这样所有原封装的音频轨、字幕轨、章节信息都保留,只替换视频流。这个细节对收藏老番很重要,因为好多老番的外挂字幕和原声轨,找不到替代品。

另一个细节是帧率。动画片源经常是 23.976 或 24 帧,如果抽帧时用了默认 25fps,最终视频会整体加速 4%。从 ffprobe 直接拿r_frame_rate作为输入参数是最可靠的。

5. 写给新手的实测参数与主观效果对照

工具写完,我拿了几类典型片源做了对比,这里给出主观效果评估和参数建议。

5.1 分片源的主观效果对照

片源类型推荐倍数效果评价
干净DVD,480p2x线条锐利,色带减少明显,效果非常自然
压缩严重网络片源2x + 去噪块状感减轻,边缘仍有轻微振铃,但整体可看
录像带转制2x,慎用4x噪点大量保留,建议先降噪,否则放大后噪点更密
720p较新动画2x提升有限,但不产生副作用,适合投大屏

主观评分上,对动漫观众最在意的线条边缘和色带控制,APISR 在 2x 档都得 9/10 左右;4x 档线条得 8 分但自然度只有 6 分。所以我推荐大部分人对老番只用 2x。

5.2 家庭场景下的推荐处理参数

我自己最终固定的参数组合:

  • 抽帧:-vsync 0,输出 PNG
  • 预处理:不启用通用锐化
  • 超分:2x,输入尺寸对齐到 16 的倍数
  • 编码:x265 10bit,CRF 18,preset slow

这个组合在保持画质和压缩率之间最平衡。如果你对体积不敏感,CRF 16 也可以,但通常 CRF 18 已经肉眼看不出差异。用 x265 preset slow 处理一集 24 分钟动画,在主流 CPU 上大约需要一到两小时,这完全可接受,毕竟不是实时处理。

5.3 这个工具适合谁用

如果你手里有大量老动画文件,并且愿意花一个下午处理一小批,这个工具会很香。如果你想要的是"实时增强播放",那应该去看播放器插件,而不是做批处理超分工具。

同样,如果只是为了在手机上临时看一集,没必要做超分,手机屏幕小,480p 也够看。超分修复的核心价值在长时间观看、投影设备、以及保存老动画副本这件事上,它是给自己留的"修复版",而不是解决临时观看需求的快捷方式。

6. 结尾:真正给孩子看之前,还要想清楚的事

工具做完,我给孩子放了一集修复后的老动画。孩子的评价是"比以前清楚多了",但对成年人来说,修复版的观感还是和老动画本身的审美习惯有出入。超分后的画面太干净,有些老动画特有的颗粒感和柔焦感没了,像是一张被 AI 磨皮的老照片。

我现在的建议是:处理时分开两个版本,一个修复版,一个原版保留。修复版给孩子看,原版留着自己怀念。毕竟老动画不只是"画质差",那种介于胶片和数字之间的旧日质感,也是记忆的一部分。

另外,超分修复只适合自己收藏和家庭观看。把修复后的完整动画分享到公共平台,会涉及版权问题,要谨慎对待。最终,这个工具对家庭场景的意义是:让经典故事以新一代孩子看得惯的画质继续流传,而不是把过去的东西改造成另一种东西。

最后分享一个小经验:批量处理前,先截取 200 帧样片试效果,确认模型档位和参数对了再跑全量。我第一次处理全集就是因为没先试样,处理完后发现线条太锐利,全部重新跑,白花几个小时。

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

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

立即咨询