用手机拍显示器上的图片,拍完放大一看,经常会出现一层彩色条纹。画面本来是清楚的,却像蒙上了一张不规则的网,越看越难受。
这次我把 ESDNet 去摩尔纹模型做成了一款 Windows 桌面工具:C# WinForms 界面、C++ Vulkan GPU 推理,支持单张图片和文件夹批量处理。
程序打成了约 23 MB 的部署包,完整解压后就能打开使用。面向开发者,也准备了包含 C#、C++、模型资源的完整 VS2022 解决方案,方便继续修改界面、接入业务或研究 GPU 推理。
先看一下实际界面:

ESDNet Vulkan 去摩尔纹工具主界面
图 1:左侧原图、右侧结果,单图与批量处理入口都在同一个窗口中。
一、摩尔纹为什么难处理
屏幕翻拍是一个很典型的场景。屏幕的子像素排列与相机采样发生相互作用,照片里就可能出现原画面没有的条纹和彩色干扰。相关问题与 ESDNet 的研究背景,可以查看作者的官方项目介绍(https://github.com/CVMI-Lab/UHDM)。
实际处理时,难点在于:干扰纹理和图像本身的细节混在了一起。
如果只是把整张图模糊,彩色条纹可能变淡,但文字、窗框、头发等细节也会一起变糊。我们需要的是在抑制干扰的同时,尽量保留内容。因此,这个工具使用训练好的图像恢复模型来生成处理结果。
这类处理也存在取舍:模型可能同时平滑真实细纹理。是否适合自己的图片,最好先拿几张有代表性的样本试一下。
二、这个工具已经能做什么
当前版本把常用的图片处理流程做到了界面里:
| 功能 | 使用方式 |
|---|---|
| 单张去摩尔纹 | 选择图片,处理后左右查看原图与结果 |
| 文件夹批量 | 逐张处理,可包含子目录 |
| 保留目录结构 | 子目录中的图片保存到对应输出子目录 |
| PNG / JPEG | PNG 默认保存;JPEG 可以设置质量 |
| 原尺寸处理 | 最长边设为 0,输出保持输入尺寸 |
| 主动缩小大图 | 设置最长边,降低处理耗时与显存需求 |
| 继续处理 | 批量任务可以跳过同名已有结果 |
| 资源模式 | 提供桌面友好、均衡、全速三种模式 |
| 安全停止 | 等待当前 GPU 调用结束,保留已经保存的结果 |
界面使用 .NET Framework 4.8 WinForms ,控件定义放在标准的 MainForm.Designer.cs 中。需要修改布局时,可以继续用 VS2022 设计器调整。
看一下处理前后的变化
下面使用作者演示图片中的一个 640×480 原尺寸裁块。左右展示的是同一块图像,未通过缩小图片来掩盖条纹。

640×480 官方演示图裁块处理前后对比
图 2:左侧为输入,右侧为本工具的 Vulkan 推理结果。示例原图来自作者的 Hugging Face 演示 Space(https://huggingface.co/spaces/ECCV2022/Screen_Image_Demoireing)。
可以看到,建筑表面的部分彩色条纹和波纹减弱了,画面也有一定程度的平滑。这里展示的是实际输出,具体效果还要看摩尔纹的尺度、拍摄条件和画面内容。
三、拿到部署包后怎样使用
1. 完整解压并启动
部署版约 23 MB ,解压后约 27.5 MiB,已经包含程序、模型和所需的 Release VC++ 运行库。
完整解压到普通可写目录,双击:
EsdNet.WinForms.exe
也可以双击随包的 启动.cmd。
使用部署版无需安装 Visual Studio、Python、CUDA、OnnxRuntime 或 Vulkan SDK。电脑需要 64 位 Windows、.NET Framework 4.8 或兼容版本,以及支持 Vulkan 的显卡驱动。部署包没有包含 .NET Framework 安装器和显卡驱动;系统缺少时需要先安装。
请保留同目录的 DLL、assets 和 licenses。只复制 exe 会丢失运行所需的资源。
2. 处理单张图片
选择"单张图片"页签,按下面的顺序操作:
- 点击"选择图片",也可以先点击"载入官方示例"。
- 选择 Vulkan GPU。
- 设置输出目录和 PNG/JPEG 格式。
- 首次尝试可以把"最长边"保持为 0。
- 点击"开始去摩尔纹"。
处理完成后,窗口会显示结果与耗时。点击"打开输出目录"就能找到保存的图片。
3. 批量处理文件夹
切换到"文件夹批量",选择输入、输出目录,按需勾选"批量包含子目录"。
输出会保留子目录层级。例如:
输入图片/
page.jpg
子目录/
page.jpg
输出/
page.jpg.demoire.png
子目录/
page.jpg.demoire.png
文件名里保留原扩展名,可以区分同一目录下的 page.jpg 和 page.png。
批量勾选"跳过同名已有结果"后,再次执行时会跳过已经存在的目标文件。这个判断只检查文件是否存在;更换参数后,建议换一个输出目录,避免把之前的结果当作本次结果跳过。
4. 最长边和资源模式怎样选
"最长边 0"表示保持原尺寸。对于显存需求较高的大图,可以主动改为 1600 等值,软件会按比例缩小输入,输出也对应缩小后的尺寸。
程序不会在显存不足时悄悄缩图。遇到预算不足提示后,可以明确调整最长边再试。
资源模式可以按电脑的使用场景选择:
- 桌面友好:在图片之间留出更多休息时间,适合边处理边办公。
- 均衡:减少休息时间,兼顾持续处理效率。
- 全速:连续处理,适合集中跑任务。
这里的休息发生在一次 GPU 调用完成之后,单张图片计算时仍然可能产生较高负载。
所有相对路径都以 exe 所在目录 为基准,默认结果保存到 输出 目录。移动整个软件目录后,可以继续使用包内示例和默认相对路径。
四、从 ESDNet 到 Vulkan,中间做了哪些工作
1. 模型选择与转换
ESDNet 来自 ECCV 2022 的图像去摩尔纹研究,官方发布了模型与预训练权重。本项目选用 UHDM 权重的小模型,FP32 精度;上游网络与权重信息见官方仓库(https://github.com/CVMI-Lab/UHDM)。
开发时先加载官方权重,再导出用于核对的 ONNX,以及应用实际执行需要的图描述和打包权重。
运行时读取的是:
assets/models/
esdnet-uhdm.graph.json
esdnet-uhdm.weights.bin
开发用 ONNX 和原始 PyTorch checkpoint 留在源码工程中。部署版只携带实际运行所需的模型资源,因此体积更小。
权重加载时会检查 SHA256 和长度,避免解压不完整或模型文件不配套后继续计算。
2. Vulkan 在这里负责什么
Vulkan 是 Khronos 维护的图形与计算 API,提供跨平台、跨厂商的 GPU 编程能力,也允许应用显式控制 GPU 资源与工作提交。其定位可以参考 Khronos 的 Vulkan Guide(https://docs.vulkan.org/guide/latest/what_is_vulkan.html)。
在这个项目里,Vulkan 执行计算着色器,完成卷积、激活、缩放、像素重排和归约等网络运算。
C# 负责界面、图片读写、任务调度和尺寸计划;C++ 负责 Vulkan 设备、缓冲区、计算管线与命令提交。两部分通过 C ABI 和 P/Invoke 对接。
Vulkan 本身不会直接执行任意 ONNX。当前工程有一层针对 ESDNet 的图编译逻辑,把已经支持的模型运算映射到 Vulkan 计算内核。
3. 一次完整的推理流程
读取图片
↓
转换为 RGB,像素除以 255
↓
居中补边到 32 的倍数
↓
根据实际尺寸生成张量与执行计划
↓
准备 GPU 缓冲、计算管线与命令
↓
上传输入,提交 Vulkan 计算
↓
等待完成,读回最终输出
↓
裁回有效区域,量化为 8 位 RGB
↓
保存 PNG / JPEG
预处理采用与官方实现相同的 RGB 通道常量补边。比如一张 257×193 的图片,会补到 288×224 进入网络,处理结束再裁回 257×193。
因此,不需要为每一种图片大小提前生成一套固定计划。尺寸变化时,程序根据实际输入重新推导形状和缓冲地址;可处理的尺寸仍受显存、设备限制和程序上限约束。
4. 为什么采用整图执行
当前实现保留了模型的多尺度处理和全局注意力。整张图片作为同一次网络输入,可以保留这些全局计算的作用范围。
代价是大图需要较多工作区。当前版本采用整图方案,显存不足时由用户主动限制最长边。
执行层也会把超过单次派发数量限制的计算拆成多次派发。这是 GPU 调度层的拆分,整张图片仍属于同一次网络推理。
五、实际速度是多少
测试环境是 RTX 4060 Laptop GPU,8 GiB 显存,Release x64,FP32。
每种尺寸在同一个会话里连续处理三次,下表取第 2、3 次原生调用耗时的平均值:
| 输入尺寸 | 实际网络输入 | 原生调用平均耗时 | 逻辑工作区 |
|---|---|---|---|
| 257×193 | 288×224 | 34.78 ms | 51.68 MiB |
| 640×480 | 640×480 | 94.90 ms | 246.09 MiB |
| 1920×1080 | 1920×1088 | 674.07 ms | 1673.44 MiB |
这里的耗时包含输入上传、GPU 执行、等待与结果读回。 图片解码、预处理、后处理、保存、首次准备和资源模式休息需要另外计算。
以 1080p 为例,本次后两轮的预处理约 106~110 ms ,后处理约 19~25 ms,读写图片的耗时另计。估算整个文件夹任务时,应把这些环节一起考虑。
表中的"逻辑工作区"是执行计划计算出来的缓冲空间。进程或整卡显存还包括权重、传输缓冲和驱动开销。本次没有采集 GPU 利用率和整卡显存峰值,所以这里只报告已经记录的指标。
首次运行时,驱动可能需要创建和编译计算管线,本机曾测得约 15~16 秒 的准备开销;缓存命中后,小图准备约 0.18 秒。批量处理中复用会话,可以把初始化开销分摊到后续图片上。
这些数据来自本机的少量基线测试,适合了解当前版本的量级。其他显卡、驱动和图片内容需要实际测试。
输出有没有对照参考实现
有。开发阶段使用相同权重导出的 ONNX,通过 OnnxRuntime CPU 计算参考输出,再与 Vulkan 的未量化 FP32 输出对照。
| 输入尺寸 | 最大绝对误差 |
|---|---|
| 64×64 | 约 1.01×10⁻⁶ |
| 257×193 | 约 2.09×10⁻⁶ |
| 640×480 | 约 3.40×10⁻⁶ |
| 1920×1080 | 约 1.93×10⁻⁵ |
这些数值用于检查 Vulkan 实现与参考计算的一致性。图像恢复质量则需要结合真实样本、对应真值或人工观察另行评估。
六、源码工程怎样编译
源码版提供一个完整的 VS2022 解决方案:
ESDNet.Vulkan.WinForms.VS2022/
ESDNet.Vulkan.WinForms.VS2022.sln
Build.cmd
Run.cmd
WinForms/ C# 界面与业务逻辑
native/Lw.Vulkan/ C++ Vulkan 核心
shared/ 公共基础代码
Assets/ 模型与示例
Tools/ 转换、验证、打包工具
Docs/ 使用说明与测试记录
Licenses/ 第三方许可
VS2022 需要安装".NET 桌面开发""使用 C++ 的桌面开发",以及 .NET Framework 4.8 SDK/目标包、MSVC v143 和 Windows SDK。
打开解决方案,选择 Release | x64 ,把 EsdNet.WinForms 设为启动项目,然后生成并运行。也可以使用根目录的 Build.cmd 和 Run.cmd。
项目已经包含 Vulkan 编译依赖和冻结的 SPIR-V,按现有源码编译无需额外安装 Vulkan SDK;如果继续修改着色器,则需要对应的编译工具与更新步骤。
目前已经验证了 Debug、Release 两种 x64 配置,并把源码包解压到新的中文目录,从零编译和运行过示例。
做二次开发时,核心调用在哪里
在 WinForms 工程内部,可以从 EsdEngine 和 ImageJobs 入手。下面是使用现有内部类型的简化调用示例:
// 在 WinForms 事件中交给后台线程,避免阻塞界面。
await Task.Run(() =>
{
var paths = new AppPaths();
var device = NativeApi.DefaultDevice(NativeApi.Devices());
string inputPath = paths.Resolve(@"assets\samples\screen-moire.png");
string outputPath = ImageJobs.UniquePath(
paths.Resolve(@"输出\sample.demoire.png"));
using (var engine = new EsdEngine(
paths.Resolve(@"assets\models"), device))
using (var input = ImageCodec.Load(inputPath, maxSide: 0))
using (var result = engine.Process(input, CancellationToken.None))
{
ImageCodec.Save(result, outputPath, jpegQuality: 95);
}
});
批量场景应把引擎放到循环外复用。工程里的 ImageJobs 已经处理了会话复用、子目录遍历、输出命名、进度与取消,可以直接沿着这层业务逻辑继续改。
七、资源包与当前边界
本项目准备了两种资源:
| 资源 | 适合用途 | 大小 |
|---|---|---|
| Windows x64 部署包 | 解压后直接使用单图、批量功能 | 约 23 MB |
| VS2022 完整源码包 | 修改界面、研究实现、接入其他业务 | 约 71 MiB |
部署包内附使用说明、静态文件 SHA256 清单和第三方许可;源码包包含模型转换脚本和数值验证记录。
当前版本使用 ESDNet-UHDM 小模型、FP32 整图推理,已在本机 NVIDIA GPU 上验证。AMD、Intel 等设备还需要补充实机测试。图片恢复可能平滑细节,大尺寸也可能遇到显存限制。
输出为 8 位 RGB,透明图片先合成到白底,TIFF 处理首帧;EXIF、ICC 等原图信息不会随输出保留。
源码里的尺寸计划器针对当前 ESDNet 图实现。后续接入其他网络时,需要核对算子、布局和形状处理;视频场景还需要额外考虑帧间一致性。
对我来说,这次开发比较有价值的一点,是把一个图像恢复模型连同界面、GPU 执行、批处理和部署一起做完整了。普通用户可以直接拿图片试效果,C# 开发者也能从一个可编译、可运行的工程开始,继续做自己的图像工具。
欢迎结合自己的屏幕翻拍样本试用,也欢迎交流处理效果和二次开发中的实现思路。