hixiong123头像
C# WinForms 文档图像矫正:去扭曲 / 去阴影 / 去模糊 / 二值化封面图

C# WinForms 文档图像矫正:去扭曲 / 去阴影 / 去模糊 / 二值化

在这里插入图片描述

在这里插入图片描述

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 个可自由组合的处理步骤,按顺序流水线执行:
    1. 矫正展平(UVDoc)
    2. 去除阴影(GCDRNet:GCNet + DRNet)
    3. 去除模糊(NAF-DPM)
    4. 图像增强(双边滤波 + CLAHE + 锐化,纯 OpenCV)
    5. 二值化(U-Net CNN)
  • DirectML GPU 加速,自动回退 CPU
  • 图片拖放打开、滚轮缩放、鼠标拖拽平移、双击适应窗口
  • 一键切换「原图 / 处理结果」对比
  • 后台线程推理,界面不卡死,带进度条与分步耗时日志
  • 自动记住上次打开/保存目录(配置写入 %AppData%\DocumentUndistort\settings.json)
  • 高 DPI 适配(PerMonitorV2),150% 缩放下布局与文字不糊不裁

三、技术栈

组件版本用途
.NET / WinForms8.0 (net8.0-windows)桌面 UI
OpenCvSharp44.10.0图像预处理、缩放、采样
Microsoft.ML.OnnxRuntime.DirectML1.20.1ONNX 推理 + 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] 的低分辨率网格坐标,需要:

  1. 双线性上采样到原图尺寸(align_corners=True);
  2. 把 [-1,1] 的归一化坐标映射到像素坐标 x = (gx + 1) * (W - 1) / 2;
  3. 逐像素双线性采样生成展平图。

其它几个模型基本就是「预处理 → 推理 → 反归一化 → 裁剪」,移植时主要是核对通道顺序、归一化尺度与 padding 方式。

六、性能实测

测试图 demo3.jpg(658 × 901),四步全开(不含二值化):

步骤CPUDirectML (GPU)
矫正展平273 ms1061 ms
去除阴影506 ms1048 ms
去除模糊1678 ms353 ms
图像增强107 ms108 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 同级目录。

八、踩坑记录

  1. DenseTensor 不实现 IDisposable:不能 using var,只有 IDisposableReadOnlyCollection<DisposableNamedOnnxValue> 需要释放。
  2. 150% DPI 下文字下半截被裁:用固定像素矩形绘制文字时,字体随 DPI 放大而矩形没放大。解决:所有尺寸按 DeviceDpi / 96 缩放,并用 TextRenderer.MeasureText 实测高度后再绘制。
  3. 中文路径读图失败:Cv2.ImRead 对非 ASCII 路径不稳定,改为 File.ReadAllBytes + Cv2.ImDecode。
  4. 进度回调跨线程:用 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

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--