ARTICLE DETAIL

资讯详情

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

C#上位机部署PaddleOCR V4:基于ONNX Runtime的完整实战源码

C#上位机部署PaddleOCR V4:基于ONNX Runtime的完整实战源码 简介本资源是一套基于C#实现PaddleOCR v4模型本地部署的完整示例工程面向.NET桌面应用开发者、工业视觉软件工程师及需在Windows平台集成OCR能力的技术人员解决传统Python OCR服务难以嵌入C#业务系统的问题。压缩包为ZIP格式大小239.15MB包含可直接编译运行的VS2019解决方案.sln、C#核心调用代码、模型权重文件、测试图像及配置说明主要文件类型涵盖.cs源码、.dll依赖库、.pdmodel模型文件与.config配置项结构清晰便于二次开发与环境迁移。已有1087人学习下载配套CSDN博文详解部署流程与常见异常处理并提供B站实机演示视频直观展示图像预处理、文本检测与识别全流程效果。读者可直接复用该工程框架快速构建票据识别、文档扫描、产线字符校验等工业级OCR应用。1. 项目背景与方案选型1.1 为什么要在C#上位机里部署PaddleOCR V4这段时间我在给一套工业视觉上位机做功能迭代需求并不复杂产品表面有一串中文批号加数字序列号相机拍完程序要能自动读出内容送到MES系统。试过几个商用OCR SDK识别率还行但一谈到授权费用和“.dll能不能离线部署”就开始尴尬。最后我选择了PaddleOCR V4模型理由很直接中英文混排的识别效果属于第一梯队模型开源可商用而且可以完全离线跑不需要把图片传到任何云端服务。在C#里部署PaddleOCR V4模型我的核心诉求很明确——要有能直接跑的源码例子而不是只给一个“思路”。因为C#上位机开发里真正占时间的从来不是写业务逻辑而是把深度学习模型接入到WinForms/WPF/工控机环境中去这一层的坑远比想象中多。比如DLL加载失败、模型路径找不到、图像格式不匹配、GPU不能激活、线程安全问题等等任何一步卡住都会让人怀疑人生。这篇文章就把我整理的C#部署PaddleOCR V4模型例子源码拿出来分享从环境准备、模型导出、代码实现到问题排查全部走一遍。1.2 主流的四种C#部署路线对比在C#环境里调用PaddleOCR V4不是只有一条路我知道的至少有四种方案各有各的适用场景这里先做个对比方便你根据项目情况选择部署路线原理优点缺点PaddleSharp对飞桨C推理库的C#封装使用Sdcb.PaddleOcr库集成简单直接加载飞桨原始模型中文社区资料多需要额外安装飞桨运行库版本绑定强高版本更新频繁飞桨原生C API P/Invoke自己写P/Invoke调用paddle_inference_c.dll性能接近最优可控性强代码量大需要理解C接口维护成本高不太适合普通业务导出ONNX模型 OnnxRuntime使用paddle2onnx导出ONNX再调用Microsoft.ML.OnnxRuntime跨平台不依赖飞桨原生库NuGet引用即用需要额外处理模型导出部分算子可能不兼容Python OCR服务 HTTP桥接C#作为客户端调用本机或远程Python API开发最快速模型更新方便部署时需要Python环境隔离差有通信开销如果你只是做原型验证直接用PaddleSharp是最快的。但如果你跟我一样最终要把这套能力放进工控机上位机里需要考虑安装包体积、离线环境、第三方依赖冲突那我强烈建议走ONNX Runtime这条路线。1.3 本例采用的方案说明本文提供的源码例子走的是“PaddleOCR V4模型导出ONNX C# OnnxRuntime推理”路线。为什么放弃PaddleSharp我在实际项目里使用PaddleSharp时踩过不少坑比如更新到新版后原生的paddle_inference.dll版本变了导致运行时报错又比如某些工控机上缺少对应的Visual C运行库。而ONNX Runtime配合NuGet包绝大多数情况下能做到“装完包就能跑”对C#开发者更友好也更容易做x64/x86的独立部署。另外工业自动化环境往往不能联网ONNX模型文件是单文件拷到目标机器上就能用。检测、识别、方向分类三个模型可以拆成独立文件后续想升级某一个模型只需要替换对应的.onnx文件不需要重新编译整个C#程序。这个特性在项目维护阶段特别有价值。2. 环境准备与模型文件2.1 开发环境与依赖包我的开发环境参数如下你可以根据实际情况微调Visual Studio 2022也可用2019但建议用2022.NET 6.0.NET 8也完全兼容NuGet包Microsoft.ML.OnnxRuntime 1.17.3OpenCvSharp4 4.8.0.20230708OpenCvSharp4.runtime.win 4.8.0.20230708目标平台x64这里要特别提醒一点OpenCvSharp4和OnnxRuntime都有原生DLL而C#项目默认的“Any CPU”在加载原生库时容易出问题。所以请在项目属性页里把“平台目标”改成“x64”并且把“首选32位”选项的勾去掉。如果目标机器是32位系统那又是另一套折腾法但现阶段工控机基本都64位了。2.2 PaddleOCR V4模型结构说明PaddleOCR V4完整识别一个文本区域实际是三个模型流水线协作分别是文本检测、方向分类、文本识别。文本检测模型ch_PP-OCRv4_det_infer负责在整张图中定位出所有文字区域输出的是区域文本框的坐标集合。方向分类模型ch_ppocr_mobile_v2.0_cls_infer负责判断检测出来的文字块是否被旋转了90度、180度或270度并做姿态纠正。文本识别模型ch_PP-OCRv4_rec_infer负责把定位并摆正后的文字块识别成字符串。三层流水线是OCR任务的标准范式也是PaddleOCR精度高的基础。官方发布的是PaddlePaddle格式的模型目录里包含inference.pdmodel和inference.pdiparams两个核心文件。直接让C#去加载这种格式不太方便所以我们用官方提供的paddle2onnx工具把它转换成ONNX格式。2.3 用paddle2onnx导出ONNX模型如果你本机安装了Python环境和PaddlePaddle导出过程很简单。先安装paddle2onnxpip install paddle2onnx然后分别导出检测、方向分类、识别模型# 导出检测模型 paddle2onnx \ --model_dir ch_PP-OCRv4_det_infer \ --model_filename inference.pdmodel \ --params_filename inference.pdiparams \ --save_file ch_PP-OCRv4_det.onnx \ --opset_version 12 # 导出方向分类模型 paddle2onnx \ --model_dir ch_ppocr_mobile_v2.0_cls_infer \ --model_filename inference.pdmodel \ --params_filename inference.pdiparams \ --save_file ch_ppocr_mobile_v2.0_cls.onnx \ --opset_version 12 # 导出识别模型 paddle2onnx \ --model_dir ch_PP-OCRv4_rec_infer \ --model_filename inference.pdmodel \ --params_filename inference.pdiparams \ --save_file ch_PP-OCRv4_rec.onnx \ --opset_version 12导出的三个onnx文件就是本文C#代码要加载的模型。如果你不想折腾Python也可以直接从相关社区下载别人已经转换好的ONNX模型但下载后记得验证一下输入输出节点名称否则代码里写错Tensor名会导致运行时报错。3. 核心源码实现3.1 项目文件结构在Visual Studio中新建一个.NET 6控制台项目然后添加上述NuGet包。为了让你看得清楚我先贴一下我整理的源码文件结构OcrPaddleV4Demo/ ├── Program.cs // 主流程读取图片并输出识别结果 ├── OcrEngine.cs // OCR引擎封装包含检测方向分类识别 ├── ImagePreprocess.cs // 图像预处理公共方法 ├── TextDetector.cs // 文本检测推理与后处理 ├── TextRecognizer.cs // 文本识别推理与解码 ├── models/ │ ├── ch_PP-OCRv4_det.onnx │ ├── ch_ppocr_mobile_v2.0_cls.onnx │ └── ch_PP-OCRv4_rec.onnx └── test.jpg // 测试图片这个结构是刻意拆开的方便你后续替换模型或者接入WinForms/WPF界面。实际项目里OCR逻辑最好单独放一个类库不要在窗口事件里写推理代码。3.2 图像预处理PaddleOCR V4的检测模型和识别模型输入要求不完全一样但公共步骤都是图像解码、缩放、归一化、转TensorNCHW布局。下面的代码使用OpenCvSharp加载图片统一转换为RGB格式using OpenCvSharp; public static class ImagePreprocess { public static Mat LoadAsBitmap(string imagePath) { Mat src Cv2.ImRead(imagePath, ImreadModes.Color); if (src.Empty()) { throw new Exception(加载图片失败请检查路径); } // OpenCV默认BGRPaddleOCR训练时用RGB需要转换 Mat rgb new Mat(); Cv2.CvtColor(src, rgb, ColorConversionCodes.BGR2RGB); return rgb; } public static float[] MatToTensor(Mat image, int targetH, int targetW) { Mat resized new Mat(); Cv2.Resize(image, resized, new Size(targetW, targetH)); // HWC - CHWbyte - float归一化到0-1 int channels 3; float[] tensor new float[channels * targetH * targetW]; unsafe { byte* ptr (byte*)resized.Data; int pixelSize channels; for (int h 0; h targetH; h) { for (int w 0; w targetW; w) { int pixelIndex h * targetW w; for (int c 0; c channels; c) { int byteIndex pixelIndex * pixelSize c; // PaddleOCR输入为RGB归一化 tensor[c * targetH * targetW pixelIndex] ptr[byteIndex] / 255f; } } } } return tensor; } }需要注意这里的归一化我直接除以255没有做更复杂的mean/std变换。PaddleOCR官方在导出ONNX时模型输入层已经内置了normalize操作所以在C#侧直接用0-1归一化即可。这点跟不少YOLO模型不太一样不要画蛇添足。3.3 文本检测推理与后处理文本检测模型输出的是从0到1的score map表示每个像素是文字中心的概率。后处理需要完成几个步骤二值化、连通域分析、轮廓查找、坐标排序、仿射变换。代码片段如下public class TextDetector { private InferenceSession _session; public TextDetector(string modelPath) { _session new InferenceSession(modelPath); } public ListRect Detect(Mat inputImage, int limitSideLen 960) { // 等比例缩放长边限制为960且高宽调整为32的倍数 int h inputImage.Rows; int w inputImage.Cols; double ratio 1.0; if (Math.Max(h, w) limitSideLen) { ratio (double)limitSideLen / Math.Max(h, w); } int resizeH (int)(Math.Ceiling(h * ratio / 32) * 32); int resizeW (int)(Math.Ceiling(w * ratio / 32) * 32); float[] input ImagePreprocess.MatToTensor(inputImage, resizeH, resizeW); // 构造Tensor维度为1x3xresizeHxresizeW DenseTensorfloat inputTensor new DenseTensorfloat(input, new[] { 1, 3, resizeH, resizeW }); var inputs new ListNamedOnnxValue { NamedOnnxValue.CreateFromTensor(x, inputTensor) }; using var results _session.Run(inputs); var output results.First().AsTensorfloat(); // 输出维度 1x1xHxW需要转换回原始图像尺寸 int outH output.Dimensions[2]; int outW output.Dimensions[3]; // ... 二值化 连通域 最小外接矩形代码略 return boxes; } }上面代码中“x”是检测模型的输入节点名称。如果你使用的ONNX文件不是官方导出的请先用工具查看输入节点的实际名称常见的有“x”、“input”等。运行时如果用错名称会直接抛InvalidOperationException。3.4 文本识别推理与解码识别模型输入固定高度32宽度建议是8或16的倍数。拿到检测框后我们需要从原图中裁剪出文字区域然后Resize到高度32、宽度按比例计算。识别模型的输出是CTC字符概率序列我们需要做“去重合并”解码。这里给出简化版实现public class TextRecognizer { private InferenceSession _session; private Liststring _labels; public TextRecognizer(string modelPath, string labelPath) { _session new InferenceSession(modelPath); _labels File.ReadAllLines(labelPath, Encoding.UTF8).ToList(); // PaddleOCR的字典第一行通常是空字符占位 } public string Recognize(Mat cropped) { int w cropped.Cols; int h cropped.Rows; float targetH 32f; float targetW (float)Math.Ceiling(w * targetH / h); targetW (float)Math.Ceiling(targetW / 8) * 8; float[] input ImagePreprocess.MatToTensor(cropped, (int)targetH, (int)targetW); DenseTensorfloat inputTensor new DenseTensorfloat(input, new[] { 1, 3, 32, (int)targetW }); var inputs new ListNamedOnnxValue { NamedOnnxValue.CreateFromTensor(x, inputTensor) }; using var results _session.Run(inputs); var output results.First().AsTensorfloat(); // 输出为 [1, 序列长度, 字典长度] return CtcDecode(output); } private string CtcDecode(Tensorfloat output) { // 实现CTC贪心解码取每个时间步最大概率字符并合并重复 StringBuilder sb new StringBuilder(); int seqLen output.Dimensions[1]; int dictLen output.Dimensions[2]; int prevLabel -1; for (int t 0; t seqLen; t) { int maxIdx 0; float maxVal float.MinValue; for (int c 0; c dictLen; c) { if (output[0, t, c] maxVal) { maxVal output[0, t, c]; maxIdx c; } } // 如果索引为0表示空字符跳过 if (maxIdx 0) { prevLabel -1; continue; } if (maxIdx ! prevLabel) { sb.Append(_labels[maxIdx]); } prevLabel maxIdx; } return sb.ToString(); } }CTC解码这里有个小坑如果两个相同字符中间没有空字符连续相同字符会被合并成一个这是CTC算法的正常行为。但如果你的业务场景里明确要求区分“11”和“1”OCR结果可能不符合预期。工业场景中我建议在识别后再加上一个正则校验至少能提前拦截不合理的输出。3.5 主流程整合源码下面是一个完整可运行的控制台程序主流程。它读取样例图片先做文本检测再对每个检测框做方向分类和文本识别最后输出识别结果列表。using OpenCvSharp; using System.Text; class Program { static void Main(string[] args) { Console.OutputEncoding Encoding.UTF8; string modelDir models; var detector new TextDetector(Path.Combine(modelDir, ch_PP-OCRv4_det.onnx)); var recognizer new TextRecognizer(Path.Combine(modelDir, ch_PP-OCRv4_rec.onnx), ppocr_keys_v1.txt); var classifier new TextClassifier(Path.Combine(modelDir, ch_ppocr_mobile_v2.0_cls.onnx)); Mat image ImagePreprocess.LoadAsBitmap(test.jpg); var boxes detector.Detect(image); foreach (var box in boxes) { // 裁剪检测框区域 Mat cropped new Mat(image, box); // 先做方向分类必要时旋转 int angle classifier.Classify(cropped); if (angle 90) { Cv2.Rotate(cropped, cropped, RotateFlags.Rotate90Counterclockwise); } else if (angle 270) { Cv2.Rotate(cropped, cropped, RotateFlags.Rotate90Clockwise); } string text recognizer.Recognize(cropped); Console.WriteLine($[检测框] x:{box.X}, y:{box.Y}, w:{box.Width}, h:{box.Height}, 文本:{text}); } } }这个例子只是把核心逻辑串起来。真正落地到WinForms上位机时建议再加一个识别结果置信度判断低于阈值的文本不要输出避免干扰业务。4. 性能调优与常见问题排查4.1 CPU推理并发与CPU线程设置OnnxRuntime在CPU上默认会尝试使用所有物理核但工控机上往往同时运行视觉定位、PLC通信、数据库写入等任务OCR推理如果占满所有CPU核心会导致整个上位机卡顿。建议在使用InferenceSession时设置线程数var options new SessionOptions(); options.AppendExecutionProvider_CPU(); options.AddSessionConfigEntry(session.intra_op.num_threads, 2); options.AddSessionConfigEntry(session.inter_op.num_threads, 1); _session new InferenceSession(modelPath, options);这里我通常设置intra-op为2inter-op为1识别单张图片的延迟不会增加太多但系统其它任务有了喘息空间。如果你的识别场景是长时间高并发建议用分页加载模型的方式每页模型常驻内存避免频繁创建和销毁会话。4.2 为什么Deploy到工控机上总是报“无法加载DLL”如果把项目从开发机拷到工控机上运行时常遇到DllNotFoundException或者System.DllNotFoundException: Unable to load DLL onnxruntime.dll。这是最常见的问题九成原因是NuGet包里的原生DLL没有正确复制到输出目录。解决方法是在项目文件中添加PropertyGroup CopyLocalLockFileAssembliestrue/CopyLocalLockFileAssemblies /PropertyGroup另外OnnxRuntime原生库必须放在与exe同目录或其子目录下。如果你引用了OpenCvSharp同时也可能出现OpenCvSharpExtern.dll加载失败。检查一下生成目录里是否有OpenCvSharpExtern.dll没有的话需要在NuGet管理器中确认OpenCvSharp4.runtime.win已经安装。还有一点容易被忽略如果你的上位机同时安装了Halcon开发环境在系统Path中可能会找到Halcon自带的某些同名DLL例如opencl.dll或msvcp140.dll这种冲突会导致程序在启动时就崩。我的经验是不要把ONNX Runtime放在全局PATH里强制把它放在exe目录下减少被其它组件干扰的概率。4.3 “无法加载一个或多个请求的类型。有关更多信息请检索LoaderExceptions属性”的解决办法这个错误经常出现在用PaddleSharp或者插件式架构加载程序集时。它本质是.NET程序集加载失败而不是OCR模型问题。可能的原因有引用的底层Native库版本不匹配、程序集目标Framework不一致、依赖的某个DLL不存在。我排查这类问题有三板斧第一在所有加载入口增加AppDomain.CurrentDomain.AssemblyResolve事件在事件里打印加载失败的DLL名称和路径信息。第二检查NuGet包版本是否和当前TargetFramework一致比如.NET 6项目引用了一个只支持.NET Framework的旧库就会引发类似错误。第三把整个输出目录拷到干净测试机上运行排除开发机环境因素干扰。如果你是使用PaddleSharp才报这个错我还有个小建议不要直接引用最新版尽量锁定一个经过时间验证的稳定版本同时确认它依赖的Sdcb.PaddleInference、Sdcb.PaddleOCR、Sdcb.PaddleNLP等包版本一致否则升级其中一个很容易带崩整体。4.4 识别效果不理想时怎么调优很多人以为部署成功就万事大吉但实际识别效果大概率达不到测试图片的精度。我遇到过最多的问题是检测框过碎一句话被切成了七八个小块。这时可以调整检测模型的后处理阈值把det_db_thresh从默认的0.3调低到0.2把det_db_box_thresh从0.6调低到0.5就会减少文本框被切断的情况。还有一个非常影响精度的地方是图像分辨率。PP-OCR系列模型训练时输入长边960如果你的输入图片本身只有640x480小目标文字很容易漏检。应该先在C#侧对图像做增强处理比如放大2倍再送入检测模型识别率提升非常明显。当然放大图片会增加推理耗时需要在速度和精度之间取舍。方向分类模型也值得留意。很多屏幕截图或者拍照图片里的文字会有180度翻转如果不使用方向分类后面的识别模型会把“698”识别成“896”。在pipeline里保留方向分类是一个低成本高收益的做法。5. 源码运行时容易踩的坑汇总5.1 模型路径与工作目录控制台程序运行时工作目录可能不是exe所在目录尤其在Visual Studio里按F5调试时工作目录默认是bin\Debug\net6.0。如果你把模型放在项目根目录的models文件夹下运行就会找不到文件。我的做法是写一个公共方法从exe所在目录向上找模型目录public static string FindModelDir() { string baseDir AppDomain.CurrentDomain.BaseDirectory; string modelDir Path.Combine(baseDir, models); if (!Directory.Exists(modelDir)) { // 从项目目录向上找两级的models目录 string projectRoot Directory.GetParent(Directory.GetCurrentDirectory())?.Parent?.Parent?.FullName; modelDir Path.Combine(projectRoot, models); } return modelDir; }发布时请使用“发布”功能确保models目录被复制到输出目录。建议在.csproj中增加ItemGroup None Includemodels\**\* CopyToOutputDirectoryPreserveNewest / /ItemGroup5.2 大批量图片识别的内存释放OnnxRuntime的InferenceSession.Run会返回一个IDisposableResults对象使用using语句可以释放资源。但如果你把OcrEngine作为单例常驻并且每张图片都执行一次完整的pipelineMat对象如果没有及时Dispose很容易造成内存持续增长。工业相机通常每秒拍10-30张图跑一天下来内存可能涨到几个GB。我的习惯是每处理完一帧显式调用Cv2.ImShow相关窗口时用using包裹推理用的Mat变量也不例外。对于识别结果不再需要的result集合早日清空不要把它们放进静态集合里。5.3 GPU推理的坑OnnxRuntime支持CUDA但GPU推理需要安装CUDA、cuDNN并且版本必须与OnnxRuntime匹配。CPU环境能跑的代码到GPU环境不一定能跑起来这几乎是新手必踩的坑。如果你只是靠机械臂视觉上位机采集图像CPU推理加上合理的线程调度完全够用而且部署简单得多。如果非要上GPU建议先在开发机上用官方脚本确认CUDA版本再引入Microsoft.ML.OnnxRuntime.Gpu包并且设置SessionOptions.AppendExecutionProvider_CUDA(0)。注意这个包会让exe体积增加很多并且部署机器上必须安装对应的NVIDIA驱动。6. 后续扩展与个人经验总结6.1 扩展方向从单图识别到服务化如果你只是在自己的上位机里偶尔识别几张小图上面的代码就够了。但如果要做成多工位同时识别的服务我建议把OCR能力独立成一个常驻进程对外提供gRPC或HTTP接口。C#调用本地Python服务也行但最好还是直接用ONNX Runtime写一个C#后台服务避免Python环境带来的运维负担。我实际做过的方案是一个Windows服务内部用Channel实现请求队列收到图片请求后排队识别识别结果回调给不同工作站。这样避免多个进程同时加载模型的资源浪费也便于统一升级模型和统计识别成功率。6.2 模型更新与版本管理PaddleOCR V4后续还有V4系列不同大小的模型比如mobile版、server版。mobile版速度更快server版精度更高。从实践来看如果没有非常苛刻的速度要求直接用server版更省心。不管用哪个版本模型文件建议放在专门的模型仓库目录里并记录好版本号。我每次测试新模型都会写一个简单的识别率对比脚本跑同一批测试图对比输出结果和耗时而不是拍脑袋替换模型。6.3 个人实际项目中的一点体会最后再分享一个我个人的小技巧。很多人部署OCR的时候第一反应是追求最高精度疯狂调参但忽略了一个事实在流水线上真正影响整体效率的往往是检测漏检和误检。我的建议是先跑通最简pipeline保证程序稳定运行三五天不崩再慢慢优化识别准确率。如果一开始就急着塞入旋转框检测、长文本拼接、置信度过滤这些高级功能出问题时反而不知道是哪一步引入的。另外工控机上跑模型日志一定要完整。我习惯在每次推理结束后记录输入图片名字、检测框数量、每个框识别结果、耗时、置信度这些信息至少保存一周日志。这样一旦现场说“识别不对”我可以快速定位是图像采集的问题还是模型本身的识别失败而不是盲目去改参数。C#部署PaddleOCR V4模型这个方向参考资料并不少但能直接拿来用的完整例子源码确实不多。希望这篇基于我自己实操整理的源码例子能帮你少走弯路。如果没有意外你照着上面的思路跑通第一个识别demo大概一个下午就能完成。剩下的时间就是慢慢在真实业务里去打磨那些细枝末节了。本文还有配套的精品资源点击获取
返回列表