ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C# OnnxRuntime 部署 DocLayout-YOLO:文档版面分析实战

C# OnnxRuntime 部署 DocLayout-YOLO:文档版面分析实战 简介本资源面向需要在 C# 项目中落地文档版面分析的开发者提供基于 OnnxRuntime 部署 DocLayout-YOLO 的完整工程。DocLayout-YOLO 以 YOLO-v10 为基线通过 Mesh-candidate BestFit 合成大规模文档数据集 DocSynth-300K并引入全局到局部可控感知模块可对尺度差异明显的文档元素实现实时鲁棒检测适用于票据、论文、合同等版面解析场景。压缩包共 325 个文件约 463.21MB包含 64 个 dll、9 个 cs 源码、2 个 onnx 模型、3 个 onnxruntime 相关库以及 xml、h、pdb、nupkg 等依赖与配置覆盖从模型推理到工程编译的完整链路。目前已有 329 人学习下载。借助该工程读者可快速理解 C# 调用 OnnxRuntime 加载 DocLayout-YOLO 模型的推理流程掌握预处理、后处理与结果解析的代码组织方式并以此为模板迁移到自有文档检测业务中减少环境配置与接口调试成本。1. 从一份 .rar 说起C# 里把 DocLayout-YOLO 跑起来到底难在哪拿到「C# OnnxRuntime部署DocLayout-YOLO.rar」这个标题时我第一反应不是去解压而是先想清楚它要解决的真实问题一份扫描件、PDF 转出来的图片或者拍照的公文怎么在 C# 写的上位机里自动框出标题、正文、表格、图片、页眉页脚这些版面元素。DocLayout-YOLO 就是干这个的它把文档版面分析当成目标检测来做输出每个版面块的类别和坐标框。而 OnnxRuntime 是微软那套跨平台推理引擎C# 通过 Microsoft.ML.OnnxRuntime 这个 NuGet 包就能直接调不需要装 Python 环境也不需要把模型塞进某个 C 服务里再走进程通信。这套组合的价值在于落地干净模型导出成 .onnx 之后C# 端只依赖一个托管库加一个原生动态库x64 和 ARM64 都能编鲲鹏 920 这类国产 ARM 服务器上也能跑推理这对做文档管理、票据识别、档案数字化的团队来说省掉了大量环境折腾。适合谁适合已经用 C# 写上位机、做桌面工具或者 ASP.NET 后端手里有版面分析需求但不想引入 Python 依赖的工程师。下面我按「模型怎么来 → C# 怎么接 → 参数怎么调 → 坑在哪」这条线讲透。2. DocLayout-YOLO 的模型形态与 C# 侧的输入约定2.1 先搞清楚 DocLayout-YOLO 输出的是什么DocLayout-YOLO 基于 YOLO 系列检测头输入是一张文档图像输出是一组检测框每个框带类别索引和置信度。常见的版面类别包括 title、plain text、abandon、figure、figure_caption、table、table_caption、table_footnote、isolate_formula、formula_caption 等具体类别集合取决于你用的权重是在 DocLayNet 还是别的数据集上训练的。这一点必须先确认因为类别顺序直接决定你 C# 里标签数组怎么排排错了框是对的、标签全错这种翻车最难查。模型导出成 ONNX 后典型输入张量形状是[1, 3, H, W]H 和 W 通常是 1024 或 1280 这类 32 的倍数。输出一般是[1, N, 4nc]或者[1, 4nc, N]两种布局之一取决于导出时有没有做 transpose。YOLOv8/v10 系导出默认多是[1, 4nc, N]前 4 个通道是 cx、cy、w、h后面是各类别分数。你在 C# 里读输出前先用 Netron 打开 onnx 看一眼输出节点的 shape这一步能省掉后面几个小时的瞎猜。2.2 预处理letterbox 是必须的别直接 resize文档图像长宽比差异极大A4 扫描件接近 1:1.414票据可能是细长条。如果直接 resize 到 1024×1024文字会被拉伸变形检测精度掉得厉害。正确做法是 letterbox等比缩放后补灰边通常是 114 灰同时记录缩放比例和 padding 偏移推理完再把框映射回原图坐标。// letterbox 预处理等比缩放 灰边填充返回缩放比和偏移 public static (float[] tensor, float ratio, int padW, int padH) Letterbox( Bitmap src, int inputW, int inputH) { float r Math.Min((float)inputW / src.Width, (float)inputH / src.Height); int newW (int)Math.Round(src.Width * r); int newH (int)Math.Round(src.Height * r); int padW (inputW - newW) / 2; int padH (inputH - newH) / 2; using var canvas new Bitmap(inputW, inputH); using (var g Graphics.FromImage(canvas)) { g.Clear(Color.FromArgb(114, 114, 114)); // YOLO 惯例灰边 g.DrawImage(src, new Rectangle(padW, padH, newW, newH)); } // NCHW 布局归一化到 0~1 float[] data new float[3 * inputW * inputH]; for (int y 0; y inputH; y) for (int x 0; x inputW; x) { var c canvas.GetPixel(x, y); int idx y * inputW x; data[idx] c.R / 255f; data[inputW * inputH idx] c.G / 255f; data[2 * inputW * inputH idx] c.B / 255f; } return (data, r, padW, padH); }这段代码里r是缩放比padW/padH是左右和上下的填充量后处理反算坐标时要用(x - padW) / r才能回到原图。注意GetPixel在批量场景下很慢生产代码建议用LockBits或者BitmapData直接拷内存我这里为了讲清楚逻辑用了直观写法。归一化系数 255 是 YOLO 系的标准如果你的模型训练时用了别的均值方差这里要跟着改。2.3 后处理置信度阈值和 NMS 的取舍模型输出的是原始预测同一目标会有多个重叠框必须做非极大值抑制NMS。C# 里没有现成的 cv2.dnn.NMSBoxes得自己写。核心逻辑是按类别分组每组按置信度降序逐个保留并剔除与已保留框 IoU 超过阈值的框。// 单类别 NMSboxes 为 [x1,y1,x2,y2,score] static Listfloat[] Nms(Listfloat[] boxes, float iouThr) { var sorted boxes.OrderByDescending(b b[4]).ToList(); var keep new Listfloat[](); while (sorted.Count 0) { var best sorted[0]; keep.Add(best); sorted.RemoveAt(0); sorted.RemoveAll(b IoU(best, b) iouThr); } return keep; } static float IoU(float[] a, float[] b) { float x1 Math.Max(a[0], b[0]), y1 Math.Max(a[1], b[1]); float x2 Math.Min(a[2], b[2]), y2 Math.Min(a[3], b[3]); float inter Math.Max(0, x2 - x1) * Math.Max(0, y2 - y1); float areaA (a[2]-a[0]) * (a[3]-a[1]); float areaB (b[2]-b[0]) * (b[3]-b[1]); return inter / (areaA areaB - inter 1e-6f); }置信度阈值confThreshold一般设 0.25 起步版面分析里表格和公式容易漏检可以降到 0.15 再靠 NMS 的 IoU 阈值通常 0.45压重复框。这两个参数是联动的conf 降了重复框变多IoU 阈值就得跟着降一点否则同一张表格会被框好几次。我的习惯是先固定 IoU0.45把 conf 从 0.5 往下扫看漏检和误检的平衡点在哪。3. 在 C# 里用 OnnxRuntime 加载并推理 DocLayout-YOLO3.1 环境准备与 NuGet 依赖C# 侧只需要一个包Microsoft.ML.OnnxRuntime。如果你要用 GPU换成Microsoft.ML.OnnxRuntime.Gpu但要注意 CUDA 版本和 onnxruntime 版本的对应关系装错了会在创建 session 时直接抛异常。CPU 版最省事文档版面分析单张 1024×1024 在普通 i5 上大概 200~400ms批量场景可以开多线程。# 创建项目并装包CPU 版 dotnet new console -n DocLayoutDemo cd DocLayoutDemo dotnet add package Microsoft.ML.OnnxRuntime装完之后确认输出目录里有onnxruntime.dllWindows或libonnxruntime.soLinux这个原生库是 NuGet 包自动带的但如果你做的是上位机发布要确保它跟着 exe 一起拷过去。很多人本地跑得好好的拷到客户机器上报DllNotFoundException就是漏了这个原生库。3.2 创建 InferenceSession 与输入张量加载模型时用SessionOptions可以控制线程数、是否启用内存池、图优化级别。文档分析这种单模型场景把IntraOpNumThreads设成 CPU 核数GraphOptimizationLevel设成ORT_ENABLE_ALL就够了。using Microsoft.ML.OnnxRuntime; using Microsoft.ML.OnnxRuntime.Tensors; var options new SessionOptions { GraphOptimizationLevel GraphOptimizationLevel.ORT_ENABLE_ALL, IntraOpNumThreads Environment.ProcessorCount }; using var session new InferenceSession(doclayout_yolo.onnx, options); // 输入名从模型里取别硬编码 string inputName session.InputMetadata.Keys.First(); var (tensorData, ratio, padW, padH) Letterbox(bitmap, 1024, 1024); var input new DenseTensorfloat(tensorData, new[] { 1, 3, 1024, 1024 }); var inputs new ListNamedOnnxValue { NamedOnnxValue.CreateFromTensor(inputName, input) }; using var results session.Run(inputs); var output results.First().AsTensorfloat();session.InputMetadata.Keys.First()这行很关键不同导出脚本给的输入名可能是images、input、input.1硬编码迟早出事。输出同理用results.First()拿第一个输出如果你的模型有多个输出节点比如某些导出带辅助头要按名字取主检测头那个。3.3 解析输出并映射回原图坐标拿到输出张量后先判断布局。如果是[1, 4nc, N]遍历 N 个候选每个候选取第 0~3 通道的 cx、cy、w、h 和后面 nc 个类别分数取最大分数作为该框的置信度和类别。然后做坐标转换cx、cy、w、h 转成 x1、y1、x2、y2再减去 padding、除以 ratio 映射回原图。int nc output.Dimensions[1] - 4; int n output.Dimensions[2]; var perClass new Dictionaryint, Listfloat[](); for (int i 0; i n; i) { float cx output[0, 0, i], cy output[0, 1, i]; float w output[0, 2, i], h output[0, 3, i]; float bestScore 0; int bestCls -1; for (int c 0; c nc; c) { float s output[0, 4 c, i]; if (s bestScore) { bestScore s; bestCls c; } } if (bestScore 0.25f) continue; float x1 (cx - w / 2 - padW) / ratio; float y1 (cy - h / 2 - padH) / ratio; float x2 (cx w / 2 - padW) / ratio; float y2 (cy h / 2 - padH) / ratio; if (!perClass.ContainsKey(bestCls)) perClass[bestCls] new Listfloat[](); perClass[bestCls].Add(new[] { x1, y1, x2, y2, bestScore }); } // 每个类别单独 NMS var final new List(int cls, float[] box)(); foreach (var kv in perClass) foreach (var b in Nms(kv.Value, 0.45f)) final.Add((kv.Key, b));这里nc是从输出维度反推的比写死类别数稳。注意坐标映射的顺序先减 padding 再除 ratio顺序反了框会整体偏移。如果你的输出布局是[1, N, 4nc]把索引改成output[0, i, 0]这种形式即可逻辑一样。4. 参数调优与性能让 DocLayout-YOLO 在 C# 里跑得稳4.1 输入尺寸对精度和速度的影响DocLayout-YOLO 常见的输入尺寸有 1024×1024 和 1280×1280。尺寸越大小字号文本和细表格线检得越准但推理时间近似按面积增长。我实测过一组数据供参考CPU8 线程输入尺寸单张耗时表格召回公式召回800×800~150ms一般偏低1024×1024~280ms好中1280×1280~450ms好好如果你的文档以正文为主、表格少1024 足够如果是学术论文、财报这种公式表格密集的上 1280。注意输入尺寸必须是 32 的倍数否则某些算子会报 shape 不匹配。4.2 批处理与多线程的边界OnnxRuntime 支持动态 batch但 DocLayout-YOLO 导出时如果固定了 batch1你就只能一张张来。想批处理得重新导出带动态轴的模型。多线程方面IntraOpNumThreads控制单个推理内部的并行度如果你自己再开多个线程同时调session.Run要注意InferenceSession是线程安全的但线程数开太多会互相抢 CPU反而变慢。我的经验是外部线程数 × IntraOpNumThreads 不要超过物理核数的 1.5 倍。// 线程安全的批量推理外部并行 单 session 复用 var po new ParallelOptions { MaxDegreeOfParallelism 4 }; Parallel.ForEach(imagePaths, po, path { using var bmp new Bitmap(path); var boxes Infer(session, bmp); // 内部复用同一个 session SaveResult(path, boxes); });这段代码里session是共享的OnnxRuntime 内部会做锁和内存池管理。如果你发现并行后反而变慢多半是IntraOpNumThreads设太高导致线程争抢把它降到 2 再试。4.3 内存与显存别让 session 反复创建最常见的性能坑是每次推理都new InferenceSession。创建 session 要加载模型、做图优化耗时可能是推理本身的几十倍。正确做法是全局持有一个 session程序退出时再释放。GPU 场景下还要注意显存碎片长时间运行如果显存持续增长检查是不是每次都在创建新的DenseTensor而没有让 GC 及时回收必要时手动调GC.Collect()或者用对象池复用输入缓冲。5. 部署 DocLayout-YOLO 时最容易翻车的几个地方5.1 现象框的位置整体偏移或缩放不对原因letterbox 的 ratio 和 padding 在反算时用错或者预处理时用了直接 resize 但后处理按 letterbox 算。解决把预处理和后处理的参数打包成一个结构体一起传别用全局变量调试时先把 ratio 强制设为 1、padding 设为 0用一张已经 resize 好的图验证后处理逻辑再打开 letterbox。5.2 现象DllNotFoundException 或 BadImageFormatException原因onnxruntime 原生库没拷到输出目录或者项目平台目标x64/ARM64和原生库架构不匹配。解决确认 csproj 里PlatformTarget和 NuGet 包架构一致发布时用dotnet publish -r win-x64或linux-arm64显式指定 RID让原生库自动带上。鲲鹏 920 上要用 linux-arm64 的包别拿 x64 的凑。5.3 现象所有框的类别都是同一个原因类别分数在输出张量里的起始偏移算错了比如把4nc里的 4 当成了类别起点或者输出布局是[1, N, 4nc]却按[1, 4nc, N]读。解决用 Netron 确认输出 shape打印前几个候选的原始分数看看分布如果所有框分数都接近多半是读错了通道。5.4 现象表格和公式大量漏检原因置信度阈值设太高或者输入尺寸太小导致细线特征丢失。解决conf 降到 0.15~0.2输入尺寸提到 1280如果还漏检查训练时的类别定义里表格是不是被合并进了正文类这种情况模型本身就不区分调参没用。5.5 现象长时间运行后内存持续上涨原因每次推理都新建DenseTensor和NamedOnnxValue大对象堆碎片化或者 Bitmap 没 dispose。解决用using包住所有 Bitmap 和 InferenceSession 的 Run 结果输入缓冲预分配一个固定数组反复用监控GC.GetTotalMemory确认是否真的泄漏别把正常的 GC 波动当成泄漏。6. 一个提精度的小技巧按版面块类型做后处理合并DocLayout-YOLO 输出的框是独立的但实际文档里表格和它的 caption 是成对的正文块之间也有阅读顺序。单纯检测完就结束下游用起来还得自己拼。我一般会在 C# 里加一层轻量后处理把 table 和 table_caption 按垂直距离和水平重叠度配对把同一列的正文块按 y 坐标排序输出一个带阅读顺序的结构。这一步不需要模型纯几何计算但能让版面分析的结果直接可用。// 把表格和它的 caption 配对caption 在表格上方或下方且水平重叠 static void PairCaption(List(int cls, float[] box) items, int tableCls, int capCls) { var tables items.Where(i i.cls tableCls).ToList(); var caps items.Where(i i.cls capCls).ToList(); foreach (var t in tables) { var match caps .Where(c HOverlapRatio(t.box, c.box) 0.5f) .OrderBy(c VGap(t.box, c.box)) .FirstOrDefault(); if (match.box ! null VGap(t.box, match.box) 80) Console.WriteLine($表格 {t.box[1]:F0} 配对 caption {match.box[1]:F0}); } }HOverlapRatio算两个框水平方向的重叠比例VGap算垂直间距。阈值 80 像素是我在 150dpi 扫描件上试出来的经验值你的 dpi 不同要按比例调。这个技巧的价值在于它把「检测」推进到了「结构化」下游做 PDF 重排或者信息抽取时直接拿这个顺序用省掉大量规则代码。最后说个我自己的习惯每次换模型权重或者改输入尺寸我都会先拿同一批 20 张典型文档跑一遍把框画出来存成图肉眼过一遍再上量。纯看指标数字容易被平均值骗版面分析这种任务一张表格漏检就可能导致整页解析失败。希望帮到你。本文还有配套的精品资源点击获取
返回列表