

C# WinForms 文档图像矫正:去扭曲 / 去阴影 / 去模糊 / 二值化(.NET 8 + OpenCvSharp + ONNX Runtime DirectML)
一、背景
做文档 OCR 时,手机拍摄的纸质文档经常存在曲面扭曲、光照不均、阴影、模糊等问题,直接送进文字检测/识别模型会明显拉低准确率。
网上这类文档矫正项目大多是 Python + PyTorch 实现(如 UVDoc、GCDRNet、NAF-DPM 等),部署到 Windows 桌面端需要装 Python、配环境,不太适合直接交付给普通用户。
于是我把原 Python/C++ 版本的推理逻辑完整移植成了一个纯 C# 的 WinForms 桌面程序:双击 exe 即可用,支持 GPU 加速,零 Python 依赖。
配图:处理前后对比(如
images/demo3.jpg的处理结果)
二、功能特性
- 5 个可自由组合的处理步骤,按顺序流水线执行:
- 矫正展平(UVDoc)
- 去除阴影(GCDRNet:GCNet + DRNet)
- 去除模糊(NAF-DPM)
- 图像增强(双边滤波 + CLAHE + 锐化,纯 OpenCV)
- 二值化(U-Net CNN)
- DirectML GPU 加速,自动回退 CPU
- 图片拖放打开、滚轮缩放、鼠标拖拽平移、双击适应窗口
- 一键切换「原图 / 处理结果」对比
- 后台线程推理,界面不卡死,带进度条与分步耗时日志
- 自动记住上次打开/保存目录(配置写入
%AppData%\DocumentUndistort\settings.json) - 高 DPI 适配(PerMonitorV2),150% 缩放下布局与文字不糊不裁
三、技术栈
| 组件 | 版本 | 用途 |
|---|---|---|
| .NET / WinForms | 8.0 (net8.0-windows) | 桌面 UI |
| OpenCvSharp4 | 4.10.0 | 图像预处理、缩放、采样 |
| Microsoft.ML.OnnxRuntime.DirectML | 1.20.1 | ONNX 推理 + GPU 加速 |
需要 5 个 ONNX 模型:uvdoc.onnx、gcnet.onnx、drnet.onnx、nafdpm.onnx、unetcnn.onnx(模型下载地址见文末)。
四、项目结构
csharp/DocumentUndistort/
├─ Program.cs # 程序入口
├─ MainForm.cs # 业务逻辑
├─ MainForm.designer.cs # 经典设计器布局
├─ Theme.cs # 主题配色与字体
├─ AppSettings.cs # 上次目录记忆
├─ Core/
│ ├─ ModelLocator.cs # 模型/示例图查找
│ ├─ OnnxSessionFactory.cs # DirectML / CPU 会话创建
│ ├─ OnnxModel.cs # 推理基类
│ ├─ TensorHelper.cs # Mat ↔ CHW 张量互转
│ ├─ UnwrapPredictor.cs # UVDoc 展平
│ ├─ UnshadowPredictor.cs # GCNet + DRNet 去阴影
│ ├─ UnblurPredictor.cs # NAF-DPM 去模糊
│ ├─ BinaryPredictor.cs # U-Net 二值化
│ ├─ BilateralEnhancer.cs # 传统图像增强
│ └─ DocumentPipeline.cs # 处理流水线
└─ Controls/ # 自绘控件:圆角按钮、步骤卡片、进度条、图片画布
模型与示例图放在程序目录向上可找到的 models/、images/ 文件夹中,仓库内直接运行无需拷贝。
五、核心实现
1. ONNX 会话:DirectML 优先,失败自动回退 CPU
DirectML 对部分算子的支持并不完整,直接创建会话可能抛异常,因此必须做好兜底:
public static InferenceSession Create(string modelPath, bool useDirectML)
{
if (useDirectML)
{
try
{
return new InferenceSession(modelPath, CreateOptions(useDirectML: true));
}
catch
{
// DirectML 不可用时回退 CPU,保证程序仍可运行
}
}
return new InferenceSession(modelPath, CreateOptions(useDirectML: false));
}
private static SessionOptions CreateOptions(bool useDirectML)
{
var options = new SessionOptions
{
GraphOptimizationLevel = GraphOptimizationLevel.ORT_ENABLE_BASIC,
LogSeverityLevel = OrtLoggingLevel.ORT_LOGGING_LEVEL_ERROR
};
if (useDirectML)
{
options.AppendExecutionProvider_DML(0); // 0 = 第一块 GPU
}
return options;
}
2. Mat 与 CHW 张量的互转
ONNX 模型输入是 [1,3,H,W] 的 float 张量,需要按通道拆分像素。这里先用 OpenCV 转成 CV_32FC3,再 Split 出单通道连续内存,直接按通道拷贝,避免逐像素取值:
public static DenseTensor<float> MatToChw(Mat bgr, bool rgb, float scale)
{
using var f32 = new Mat();
bgr.ConvertTo(f32, MatType.CV_32FC3, scale); // 同时完成类型转换与归一化
return FloatMatToChw(f32, rgb);
}
public static unsafe DenseTensor<float> FloatMatToChw(Mat f32Bgr, bool rgb)
{
int height = f32Bgr.Rows, width = f32Bgr.Cols, area = height * width;
var tensor = new DenseTensor<float>(new[] { 1, 3, height, width });
var buffer = tensor.Buffer.Span;
Mat[] channels = f32Bgr.Split();
try
{
for (int c = 0; c < 3; c++)
{
int target = rgb ? 2 - c : c; // BGR -> RGB 时反转通道顺序
var source = new ReadOnlySpan<float>((void*)channels[c].Data, area);
source.CopyTo(buffer.Slice(target * area, area));
}
}
finally
{
foreach (var channel in channels) channel.Dispose();
}
return tensor;
}
推理结果同样按 CHW 还原成 CV_8UC3 图像即可。
3. 处理流水线
把每一步封装成独立 Predictor,按勾选顺序串起来,并统一上报进度与耗时:
public Mat Run(Mat source, IReadOnlyList<PipelineStep> steps, IProgress<PipelineProgress>? progress = null)
{
Mat current = source;
try
{
for (int i = 0; i < steps.Count; i++)
{
progress?.Report(new PipelineProgress(i, steps.Count, $"正在执行:{GetDisplayName(steps[i])}…"));
var sw = Stopwatch.StartNew();
Mat next = Execute(steps[i], current);
sw.Stop();
if (!ReferenceEquals(next, current)) current.Dispose();
current = next;
progress?.Report(new PipelineProgress(i + 1, steps.Count,
$"{GetDisplayName(steps[i])} 完成({sw.ElapsedMilliseconds} ms)"));
}
return current;
}
catch
{
current.Dispose();
throw;
}
}
模型实例懒加载并复用,切换运行设备时统一释放,避免重复加载大模型。
4. UVDoc 的网格采样
UVDoc 输出的是 [1,2,45,31] 的低分辨率网格坐标,需要:
- 双线性上采样到原图尺寸(
align_corners=True); - 把
[-1,1]的归一化坐标映射到像素坐标x = (gx + 1) * (W - 1) / 2; - 逐像素双线性采样生成展平图。
其它几个模型基本就是「预处理 → 推理 → 反归一化 → 裁剪」,移植时主要是核对通道顺序、归一化尺度与 padding 方式。
六、性能实测
测试图 demo3.jpg(658 × 901),四步全开(不含二值化):
| 步骤 | CPU | DirectML (GPU) |
|---|---|---|
| 矫正展平 | 273 ms | 1061 ms |
| 去除阴影 | 506 ms | 1048 ms |
| 去除模糊 | 1678 ms | 353 ms |
| 图像增强 | 107 ms | 108 ms |
可以看出小分辨率图片下 DirectML 的显存拷贝开销反而让部分模型变慢,大图或大模型(NAF-DPM)上 GPU 才有优势,所以程序里保留手动切换设备的能力。
七、运行方式
cd csharp/DocumentUndistort
dotnet build -c Release
dotnet run -c Release
注意:
- 目标平台为 x64,DirectML 不支持 AnyCPU;
- 模型文件需按
models/xxx.onnx放置; - 单独部署时把
models、images放到 exe 同级目录。
八、踩坑记录
DenseTensor不实现IDisposable:不能using var,只有IDisposableReadOnlyCollection<DisposableNamedOnnxValue>需要释放。- 150% DPI 下文字下半截被裁:用固定像素矩形绘制文字时,字体随 DPI 放大而矩形没放大。解决:所有尺寸按
DeviceDpi / 96缩放,并用TextRenderer.MeasureText实测高度后再绘制。 - 中文路径读图失败:
Cv2.ImRead对非 ASCII 路径不稳定,改为File.ReadAllBytes+Cv2.ImDecode。 - 进度回调跨线程:用
Progress<T>自动切回 UI 线程,不要直接操作控件。
九、参考
- 原 Python/C++ 项目:本文改造的项目(含 Python 与 C++ 两套推理实现 链接:https://github.com/hpc203/document-undistort-onnxrun)
- 模型来源:ModelScope「DocUnwrap」相关模型
- ONNX Runtime DirectML 官方文档
源码下载:
通过网盘分享的文件:DocumentUndistort.7z
链接: https://pan.baidu.com/s/104JQEU72hd1IglQcCbPaxQ?pwd=ku1c 提取码: ku1c
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/hixiong123/article/details/170243325




