基于C#与ONNX Runtime的工业OCR离线识别实现

发布时间:2026/9/9 1:38:39
基于C#与ONNX Runtime的工业OCR离线识别实现 简介这是一份面向C#开发者的OCR识别服务封装资源基于Onnx Runtime与PaddleOCR模型实现适用于需要离线部署文字识别能力的桌面或服务端项目。资源包内含可直接运行的完整Demo已配置好依赖与模型开发者无需重新训练即可集成调用。包体共2000个文件约331MB以XML配置、DLL运行库、TXT说明、C#源码及工程文件为主体同时包含6个ONNX模型、NuGet依赖包与符号文件结构清晰方便按需查阅与二次开发。目前已有383人学习下载。对于希望快速落地OCR功能或学习Onnx模型推理流程的C#工程师而言该资源提供了完整的服务封装示例和可验证的本地运行环境能有效减少环境搭建与模型转换的时间成本适合作为实际项目参考或技术入门模板。1. 方案选型为什么非要用 ONNX 部署 OCR1.1 需求背景从一次现场改造说起做 C# 上位机开发的朋友应该都有这种经历客户现场要求识别产品上的喷码字符、读取标签上的序列号、或者把仪表盘读数自动录进系统。这类需求以前要么买商业 OCR SDK要么调云端接口但在工业现场往往有网络隔离、数据不能出内网、识别速度还得快等硬性要求。我这次遇到的项目就是典型的扫码 字符识别场景最后决定用 Onnx 把 PaddleOCR 的模型部署到本地封装成一个独立的 OCRService用 C# 直接调用全程离线机器上不用装任何第三方的 Python 环境或运行库。项目文件名之所以存成C# Onnx.OCRService.rar其实就是把这套可复用的服务类、模型文件和使用文档打包后续接到任何工控机上都能快速发布。1.2 为什么不用 Tesseract 或云端 APIOCR 方案选型比很多人想的要复杂不是随便拉个开源库就行。我把常见方案摆在一起对比过方案优点缺点适合场景Tesseract开源免费、英文和标准印刷体识别不错工业现场喷码、刻字、反光背景效果一般中文场景调优成本高轻度识别、文字清晰的文档商业 OCR SDK识别率高、支持多场景收费贵、部分需要加密狗或联网授权预算充足的企业级项目云端 OCR API接入简单、模型迭代快依赖网络、数据出局、延迟不稳定非生产环境、数据不敏感PaddleOCR ONNX Runtime效果接近商业级、离线运行、部署简单、License 宽松需要自己处理模型导出、预处理、后处理细节工业上位机、内网隔离现场我最终选了最后一种。PaddleOCR 的检测加识别两个模型导出后总共也就十几 MBC# 侧通过Microsoft.ML.OnnxRuntime这个 NuGet 包直接加载推理不需要额外装深度学习框架。这里顺便回答一个很多新手在问的问题为什么要转 onnx 模型因为 onnx 本质上是一个格式转换层把 Paddle、PyTorch 训练好的模型统一成开放格式后就能脱离原框架被 C#、Java、C 这些语言在 CPU、GPU、边缘设备上直接调用。模型本身的知识和权重不丢但部署门槛低得多。1.3 整体架构OCRService 怎么拆OCR 不能只做一个调用一下模型的裸功能。在实际项目里我需要图片读取、预处理、推理、后处理、结果归一化、日志记录、多线程排队还要对外提供同步和异步都支持的接口。所以我单独建了一个类库项目命名为 OCRService把所有流程封装进去上位机主程序只需要引用之后调用RecognizeAsync()就行。这样做的最大好处是项目从 WinForms 迁移到 WPF或者从单机程序扩展成 TCP 服务OCR 相关代码都不用动。服务类内部是经典的流水线设计外层还有连接池和取消机制具体细节在后面展开。2. 模型准备从 PaddleOCR 到 onnx 文件2.1 检测模型和识别模型的分工PaddleOCR 的完整识别流程是先检测后识别两步检测模型DB 算法负责在图片里框出文字区域识别模型CRNN 结构负责把框出的区域转成字符串。所以导出 onnx 时需要把这两个模型分别导出推理时先跑检测模型得到文本框坐标再对每个框裁图跑识别模型。刚开始接触这个流程的朋友容易以为一个模型就能搞定实际分开处理反而更灵活——比如只检测不识别或者只识别已知裁剪好的区域现场调试时可以单独替换其中一个模型。2.2 导出步骤和几个重点提醒PaddleOCR 官方提供了PaddleOCR/tools/export_model.py导出命令大致如下python tools/export_model.py -c configs/det/ch_PP-OCRv4_det_cml.yml -o Global.pretrained_model./ch_PP-OCRv4_det_train/best_accuracy Global.save_inference_dir./inference/det python tools/export_model.py -c configs/rec/ch_PP-OCRv4_rec_distill.yml -o Global.pretrained_model./ch_PP-OCRv4_rec_train/best_accuracy Global.save_inference_dir./inference/rec导出成功后的目录是 Paddle 的 inference 结构还不是 onnx。要拿到 onnx 可以用paddle2onnx工具paddle2onnx --model_dir ./inference/det --model_filename inference.pdmodel --params_filename inference.pdiparams --save_file ./onnx/det.onnx --opset_version 11 paddle2onnx --model_dir ./inference/rec --model_filename inference.pdmodel --params_filename inference.pdiparams --save_file ./onnx/rec.onnx --opset_version 11这里有三个点必须注意固定输入尺寸导出时建议把检测模型的输入固定到 960x960 或者 640x640识别模型固定为高度 32、宽度 320 左右否则 C# 端动态尺寸处理会非常痛苦尤其是要自己写 resize 逻辑时。opset 版本ONNX Runtime 不同版本支持的 opset 范围不一样导出时用 11 或 12 兼容性最好。我遇到过用 opset 15 导出的模型在客户老版本运行库上直接跑不起来排查了半天才发现是算子版本不兼容。导出后必须验证用 Python 加载 onnx 跑一遍同样的输入和 Paddle 原模型对比输出确定没有导出错误再进 C# 流程。省这一步后面问题会加倍。2.3 int8 量化值不值得做热词里出现了 .onnx 量化 int8我也实际试过。用 onnxruntime 的量化工具把识别模型量化成 int8模型体积从 10MB 左右降到 3MB 左右CPU 推理速度大约提升 20-30%。但代价是识别率会掉一点尤其是喷码这种笔画不清晰、背景有噪点的情况出错率明显升高。提示工业场景对识别率要求严格时建议先跑 float32 模型确认整体效果再单独拿现场图片测试 int8 模型。如果准确率下降在可接受范围内再切换千万别一上来就量化。宁可慢 20ms也不要错一个字符导致返工。3. C# 调用 ONNX 的完整实现3.1 NuGet 包和工程配置在 Visual Studio 里建一个 .NET 6 或 .NET 8 的类库项目安装两个包PackageReference IncludeMicrosoft.ML.OnnxRuntime Version1.17.1 / PackageReference IncludeSystem.Drawing.Common Version7.0.0 /代码里新建两个推理会话private InferenceSession _detSession; private InferenceSession _recSession; public OcrService(string detModelPath, string recModelPath) { var sessionOptions new SessionOptions { GraphOptimizationLevel GraphOptimizationLevel.ORT_ENABLE_ALL, ExecutionMode ExecutionMode.ORT_SEQUENTIAL }; _detSession new InferenceSession(detModelPath, sessionOptions); _recSession new InferenceSession(recModelPath, sessionOptions); }如果目标机器有 NVIDIA 显卡且装了 CUDA 环境可以把包换成Microsoft.ML.OnnxRuntime.Gpu然后在 SessionOptions 里加一行AppendExecutionProvider_CUDA(0)。不过 OCR 模型普遍不大CPU 单线程推理已经够快GPU 主要在多路并发时才有明显优势普通项目不必为了 GPU 增加部署复杂度。关于跨平台部署onnxruntime 对 Linux 和 ARM 工控机支持也很好和在 Windows 上引用 NuGet 包不太一样需要单独装原生运行库但 C# 端调用代码基本不用改。3.2 图像预处理Bitmap 转 Tensor模型输入要求是归一化后的 float 数组顺序为 C H W通道、高度、宽度。图片预处理我封装成单独方法private Tensorfloat PreprocessImage(Bitmap image, int targetWidth, int targetHeight) { using var resized new Bitmap(image, targetWidth, targetHeight); var data new float[3 * targetHeight * targetWidth]; int index 0; for (int c 0; c 3; c) { for (int y 0; y targetHeight; y) { for (int x 0; x targetWidth; x) { var color resized.GetPixel(x, y); float value c 0 ? color.R : c 1 ? color.G : color.B; data[index] (value / 255f - 0.5f) / 0.5f; } } } return new DenseTensorfloat(data, new[] { 1, 3, targetHeight, targetWidth }); }这里有两个容易踩的坑一是 PaddleOCR 模型的预处理是(x / 255 - mean) / stdmean 和 std 都取 0.5也就是先归一化到 [-1, 1] 再进模型如果只除 255 而不减均值除方差识别结果会非常差。二是GetPixel循环在图片大时很慢高分辨率图片建议用LockBits把像素直接转成字节数组再按通道取值识别速度能快不少我后面会再提一次。3.3 推理、后处理与文本输出检测模型输出的结果是 score map 和文本检测框相关信息。代码整体模板化var inputs new ListNamedOnnxValue { NamedOnnxValue.CreateFromTensor(x, inputTensor) }; using var results _detSession.Run(inputs); var output results.First().AsTensorfloat(); // 对输出做阈值过滤、轮廓查找得到矩形框列表 var boxes ExtractTextBoxes(output, threshold: 0.3f);识别模型则需要把每个检测框裁出来的小块图片做同样预处理然后拿到 CTC 输出按字符表解码成字符串。解码是新手很容易出错的地方public string Decode(int[] preds) { var sb new StringBuilder(); int last -1; foreach (var p in preds) { if (p ! last p ! 0) sb.Append(_alphabet[p]); last p; } return sb.ToString(); }代码里 0 是 CTC 的 blank 符号表示空位。如果解码逻辑里漏掉p ! 0这个条件会出现连续重复字符漏掉p ! last则会出现同一个字符被重复输出两遍。这两个细节决定识别结果是否准确别小看这几行代码。3.4 OCRService 的对外接口设计对外暴露的方法要同时考虑同步和异步并且支持取消public TaskOcrResult RecognizeAsync(Bitmap image, CancellationToken token default) public OcrResult Recognize(Bitmap image)OcrResult 建议这样定义public class OcrResult { public string Text { get; set; } public float Confidence { get; set; } public ListRectangle Boxes { get; set; } public long ElapsedMilliseconds { get; set; } }这样上层不管是用扫码枪触发一帧识别还是在循环采集中周期性识别都能拿到统一的返回结构。我还会在服务内部加上简单的 3 次重试和异常隔离逻辑单次推理失败不会导致整个服务崩掉这在现场长时间运行时非常重要。4. 上位机集成扫码枪触发、TCP 通讯与 UI 卡顿4.1 扫码枪触发事件怎么与 OCR 联动热词里 c# 扫码枪触发事件 是上位机开发的高频需求。扫码枪一般模拟键盘输入在 WinForms 或 WPF 的 KeyDown 里拦截回车或特殊前缀字符private void txtScanner_KeyDown(object sender, KeyEventArgs e) { if (e.KeyCode Keys.Enter) { var text txtScanner.Text.Trim(); txtScanner.Clear(); if (string.IsNullOrEmpty(text)) return; _ TriggerOcrAsync(text); } }在实际项目中扫码枪扫到的条码往往是产品批次信息OCR 识别出来的字符需要和这个批次绑定。做法是把扫码文本和相机抓拍的图片文件名一起放进任务队列OCR 完成后把两段信息合并写入本地数据库或发送到 MES 系统。这种串联很自然但要注意扫码枪的输入处理和 OCR 异步处理都不能阻塞 UI 线程。4.2 循环数据采集与 UI 刷新卡顿的解法c# 循环数据采集和 ui 刷新卡顿 这个热搜背后是经典问题采集线程跑完就立刻往 UI 控件上塞数据或者在 UI 线程里直接做推理窗体就会卡死。我的做法是引入一个生产者/消费者队列采集线程只负责把图片丢进ChannelBitmap消费端用独立后台任务取图片跑 OCR识别完成后再通过async/await回到 UI 线程更新界面private readonly ChannelBitmap _channel Channel.CreateUnboundedBitmap(); // 生产者相机回调里 await _channel.Writer.WriteAsync(frame);// 消费者后台循环 while (await _channel.Reader.WaitToReadAsync()) { while (_channel.Reader.TryRead(out var frame)) { using (frame) { var result _ocr.Recognize(frame); OnResult?.Invoke(result); } } }这样即使单张图片 OCR 耗时 50ms 或 100msUI 依然流畅而且队列天然解决了偶发丢帧和突发流量堆积的问题。如果你项目里用的还是BackgroundWorker或者裸Thread可以试试这种基于 Channel 的写法代码简洁不少。4.3 Socket 方式把 OCR 能力暴露给其他设备热词里 c# socket、c# 工业级网口通讯助手 说明很多现场不只有一台工控机。其他设备如果也需要 OCR 能力而 OCRService 所在机器不方便做共享内存调用最简单的方案是开一个 TCP 监听端口接收图片字节流返回识别结果 JSONprivate void StartServer(int port) { _listener new TcpListener(IPAddress.Any, port); _listener.Start(); _ AcceptLoopAsync(); } private async Task HandleClientAsync(TcpClient client) { using (client) using (var stream client.GetStream()) { // 读取 4 字节长度前缀接着读 JPG 图片字节 // 调用 _ocr.Recognize 后返回 JSON } }协议建议用简单的4 字节长度 图片 JPG 字节方式返回 JSON 里包含文本、置信度和耗时这样即使对面是 Python、Java 或者 LabVIEW 写的客户端接入成本也几乎为零。我在现场实际接过一个 labview 做上位机、C# 做识别服务的工位整个联调只花了一个下午。5. 踩坑记录与问题排查5.1 ONNX Runtime DLL 加载失败最常见的报错是System.DllNotFoundException: Failed to load onnxruntime.dll。一般有四个原因x64/x86 不一致模型导出和程序生成位数必须统一项目强制 x64。NuGet 包版本冲突项目里同时引用 Runtime 和 Runtime.Gpu 会出问题一次只装一个。发布目录缺少原生 DLLonnxruntime 的runtimes/win-x64/native/onnxruntime.dll没有复制到输出目录。模型 opset 版本太高对应前面说的导出建议opset 11 最稳妥。我的习惯是统一 x64、统一 NuGet 包版本号每次发布到现场前检查输出目录里的onnxruntime.dll文件是否存在再开始联调。5.2 识别结果为空或者乱码识别结果为空多半是检测模型的阈值没调好文本区域被过滤掉了。检测后处理里的阈值从 0.2 到 0.5 做一个网格搜索找到现场图片亮度、反光情况下最合适的值。我之前在一个贴标反光的工位把阈值从 0.3 调到 0.25识别率立刻从 82% 涨到了 94%。如果是乱码重点检查字符表。PaddleOCR 提供的ppocr_keys_v1.txt要和导出时保持一致不能自己换个字典文件。C# 端加载字典时还要注意编码格式中文环境下必须以 UTF-8 读取否则后面到生僻字时会出现乱码甚至索引越界。5.3 性能优化与小经验总结onnxruntime 的同一个 InferenceSession 被多线程并发调用时内部会有锁竞争多路并发能力提升有限。想要真正并发我的做法是创建多个 Session 实例组成连接池按需取用。另一个优化点是图片预处理改用LockBits我实测单张耗时从 90ms 降到了 55ms效果非常明显。所有运行时分配的大数组也尽量做成缓存对象复用减少 GC 压力。注意如果用了System.Drawing.CommonWindows 上没有问题但要部署到 Linux 容器时需要额外装依赖低配 ARM 板卡上跑也要特别注意内存占用。生产环境建议还是以 Windows 工控机为主等模型和流程稳定后再考虑边缘设备的迁移。最后再分享一个实际体会我在做这个项目时踩过最深的坑不是模型效果不好而是能跑的 Demo和生产可维护的模块之间的差距。一开始我只是把识别代码塞在一个窗口的后台事件里后来改成独立的 OCRService 类库把接口、日志、并发、异常处理都统一掉之后再接入扫码枪、TCP、数据库就顺畅了很多。如果你的现场项目也准备上 OCR建议先拿 50 张现场图片把检测阈值、预处理、接口设计这些基本功打牢再扩展多路并发和设备接入后面会省掉大量返工的时间。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询