ARTICLE DETAIL

资讯详情

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

C# WinForm部署PaddleOCR V3:基于ONNX Runtime的离线OCR实战

C# WinForm部署PaddleOCR V3:基于ONNX Runtime的离线OCR实战 简介这份C# WinForm部署PaddleOCR V3模型的完整源码工程面向需要在桌面应用中集成中文OCR识别功能的.NET开发者。资源基于VS2019与.NET Framework 4.7.2开发集成OpenCvSharp4.8.0以及Sdcb.PaddleInference、Sdcb.PaddleOCR等关键库涉及图像预处理、模型加载、文字识别等典型环节适合希望快速掌握PaddleOCR在C#端落地方法的读者。压缩包共73个文件总大小约236.74MB内容以dll运行库、cs源码、xml配置、pdiparams与pdmodel模型文件等为主同时附带资源文件和工程配置文件结构完整解压即可对照代码理解部署流程。目前已有532人学习如果你正需要在WinForm项目中嵌入OCR能力这份带模型和完整依赖的示例工程可以省去大量环境配置时间直接提供可运行的参考实现。1. C# WinForm 部署 PaddleOCR V3 到底难在哪为什么「照着 Python 抄」跑不起来很多团队早就用 PaddleOCR 在 Python 里把中文识别跑通等需求变成「把 OCR 能力放进 C# WinForm 程序客户机器上不能装 Python、不能装 conda」原本两行代码能启动的推理就变成了一堆黑匣子。C# WinForm 部署 PaddleOCR V3 模型的本质难题是把 PP-OCRv3 的三段推理迁进 .NET 进程里还要保证识别结果和 Python 环境基本一致。文章会给你一份可以直接照抄的源码骨架把官方推理模型转成 ONNX用 ONNX Runtime 在 C# 里完成 det、cls、rec 三个模型串联最后在 WinForms 按钮事件里显示识别文本和坐标。适合做上位机、离线工具、内部系统以及要打包成安装程序交付的场景。2. 先选跑道为什么我把 PP-OCRv3 推到 ONNX Runtime 而不是 Paddle Inference2.1 Paddle Inference 路线的三个隐性成本PaddleOCR 官方推荐的部署方式通常走 Paddle Inference但这条路在 C# WinForms 里并不友好。首先是依赖问题Paddle Inference 的 C 预测库体积大还要和 OpenBLAS、MKL、Visual Studio 运行库搅在一起装错一个版本就在启动时报内存错。其次官方封装的预测接口面向 C 和 PythonC# 要绕 P/Invoke 调用结构体定义、生命周期管理、内存释放都是血泪经验新手容易写一次崩一次。我一般会先走 ONNX Runtime 路线原因很直接NuGet 装包、C# 直接调用、DLL 自动跟随程序发布没有那么多玄学。PP-OCRv3 的推理模型可以通过 paddle2onnx 转成标准 ONNX再用Microsoft.ML.OnnxRuntime加载。模型还是同一套权重只是推理引擎换了识别效果几乎不受影响。WinForms 里只需要处理图像预处理、结果后处理这部分代码我们完全可控。2.2 准备 PP-OCRv3 三个模型并转成 ONNX 文件PP-OCRv3 在识别一条文本时实际跑了三个模型模型输入输出作用det1x3xHxW1x1xHxW 概率图找到文字所在区域cls1x3x48x1921x2 概率判断是否旋转 180 度rec1x3x48xW1xWxVocabSize解码出最终文字这三个模型要分别下载推理版注意不是下载训练模型。训练模型的目录里通常只有model.pdparams推理模型目录里才有inference.pdmodel和inference.pdiparams这种可部署文件。我常用的转换命令是这样# det 模型转换 paddle2onnx --model_dir ch_PP-OCRv3_det_infer \ --model_filename inference.pdmodel \ --params_filename inference.pdiparams \ --save_file ch_PP-OCRv3_det.onnx \ --opset_version 12 # cls 模型转换 paddle2onnx --model_dir ch_PP-OCRv3_cls_infer \ --model_filename inference.pdmodel \ --params_filename inference.pdiparams \ --save_file ch_PP-OCRv3_cls.onnx \ --opset_version 12 # rec 模型转换 paddle2onnx --model_dir ch_PP-OCRv3_rec_infer \ --model_filename inference.pdmodel \ --params_filename inference.pdiparams \ --save_file ch_PP-OCRv3_rec.onnx \ --opset_version 12这里的关键点是 det 和 rec 的输入宽高都是动态的转换后 ONNX 里会有Dynamic Axes不能强压成固定尺寸。opset 版本用 11 或 12 都行ONNX Runtime 当前稳定版本对这两个版本兼容性最好。如果你本机装了旧版 paddle2onnx转换后先用下面的代码打印模型输入输出名别急着写 C#。2.3 用 Netron 核对输入输出名别把 det 当 rec 用很多照着源码抄的人翻车是因为写的x输入名和实际模型对不上。同一个x在 det 和 rec 里都存在但输出名往往不一样。转换后用 Netron 打开 ONNX 文件先看 Input 和 Output 的节点名。也可以用 C# 快速打印var session new InferenceSession(models\ch_PP-OCRv3_det.onnx, new SessionOptions()); foreach (var item in session.InputMetadata) Console.WriteLine(Input: item.Key shape string.Join(,, item.Value.Dimensions)); foreach (var item in session.OutputMetadata) Console.WriteLine(Output: item.Key shape string.Join(,, item.Value.Dimensions)); session.Dispose();InputMetadata的 Key 就是你在NamedOnnxValue.CreateFromTensor里要填的输入名。Output 我一般直接用output.First()不写死名字因为很多 ONNX 模型输出层会有save_infer_model/scale_0.tmp_1这种长名写死容易翻车。这一步建议在写完整引擎前先跑一遍确认三个模型的输入形状和你后续代码里的维度一致。提示PP-OCRv3 的 det 输入宽高需要是 32 的倍数rec 的高度固定 48宽度按原图比例动态变化。这些尺寸约束在代码里要显式处理否则推理结果全是 0。3. WinForms 工程骨架从 NuGet 依赖到能跑通的模型加载3.1 创建项目并安装 ONNX Runtime 与 OpenCvSharp打开 Visual Studio新建一个 WinForms 项目。目标框架建议 .NET Framework 4.7.2 或 .NET 6/8。如果你还在 VS2015 上做 WinForms 项目先说句实话新版本的 ONNX Runtime NuGet 包对 VS2015 兼容性很差经常会卡在 MSBuild 版本过低无法还原强烈建议升到 VS2019 或 VS2022否则后面每一步都是坑。在 NuGet 包管理器里装两个包版本直接选最新稳定版即可ItemGroup PackageReference IncludeMicrosoft.ML.OnnxRuntime / PackageReference IncludeOpenCvSharp4.Windows / /ItemGroupOpenCvSharp4.Windows会自动带上 native 的OpenCvSharpExtern.dll和运行依赖省去手动拷贝一堆 DLL 的麻烦。图像解码、缩放、找文本轮廓都用 OpenCvSharp 做比 System.Drawing 快一个量级尤其在 WinForms 里要实时预览识别框时System.Drawing 的 GDI 缩放容易卡 UI。创建完项目后把三个 ONNX 文件和ppocr_keys_v1.txt字典文件放到models子目录如果想让源码干净就在项目属性里把这几个文件设为「复制到输出目录 始终复制」。这样生成的 exe 旁边就有完整的模型目录调试和打包路径一致。3.2 写一个 OcrEngine 类加载三个 ONNX Session核心引擎我习惯封装成一个OcrEngine类构造函数里一次性加载三个 Session对外只暴露Detect、Recognize、RecognizeFromFile这些方法。WinForms 按钮事件里不直接碰 ONNX Runtime这样界面层干净出问题也好定位。using System; using System.Collections.Generic; using System.IO; using System.Text; using Microsoft.ML.OnnxRuntime; using OpenCvSharp; namespace PaddleOcrV3Demo { public class OcrEngine : IDisposable { private readonly InferenceSession _detSession; private readonly InferenceSession _clsSession; private readonly InferenceSession _recSession; private readonly string[] _vocab; public OcrEngine(string detModelPath, string clsModelPath, string recModelPath, string vocabPath) { var opts new SessionOptions { GraphOptimizationLevel GraphOptimizationLevel.ORT_ENABLE_ALL, IntraOpNumThreads 4 }; _detSession new InferenceSession(detModelPath, opts); _clsSession new InferenceSession(clsModelPath, opts); _recSession new InferenceSession(recModelPath, opts); _vocab File.ReadAllLines(vocabPath, Encoding.UTF8); } public void Dispose() { _detSession?.Dispose(); _clsSession?.Dispose(); _recSession?.Dispose(); } } }这段代码有三个关键参数要留意。GraphOptimizationLevel设置为ORT_ENABLE_ALLONNX Runtime 会做图融合、常量折叠CPU 推理速度能差 10% 到 30%。IntraOpNumThreads我设为 4不是越大越好太大会在 UI 机器上把 CPU 占满导致整个 WinForms 界面拖不动。字典文件_vocab按行读入首行是 blank后面是真实字符这个顺序直接影响 4.2 节里的 CTC 解码。3.3 把 Mat 转成 ONNX Runtime 需要的 TensorONNX Runtime 不认Mat需要转成float[]再包成DenseTensorfloat。PP-OCRv3 的预处理和一般图像分类网络一样先转 RGB、再缩放、归一化到[0,1]、用 ImageNet 的 mean/std 做标准化最后按 NCHW 布局排内存。很多新手在这里只做了归一化忘了通道顺序调整最后识别出来的内容全是乱的。private static float[] MatToTensor(Mat bgr, int targetH, int targetW, float[] mean, float[] std) { Mat rgb new Mat(); Cv2.CvtColor(bgr, rgb, ColorConversionCodes.BGR2RGB); Cv2.Resize(rgb, rgb, new Size(targetW, targetH), 0, 0, InterpolationFlags.Linear); float[] data new float[3 * targetH * targetW]; for (int y 0; y targetH; y) { for (int x 0; x targetW; x) { Vec3b p rgb.AtVec3b(y, x); int i y * targetW x; data[i] (p[0] / 255f - mean[0]) / std[0]; data[i targetH * targetW] (p[1] / 255f - mean[1]) / std[1]; data[i 2 * targetH * targetW] (p[2] / 255f - mean[2]) / std[2]; } } rgb.Dispose(); return data; }这段代码看起来简单但有个容易踩的点Vec3b p在 RGB 模式下p[0]是红通道p[1]是绿p[2]是蓝和数据布局里第一个“通道平面”对应。我见过有人直接在 BGR Mat 上做归一化然后把p[0]当成红色填进 R 平面结果模型输出概率值完全异常。PaddleOCR 默认的 mean 是0.485f, 0.456f, 0.406fstd 是0.229f, 0.224f, 0.225fdet、cls、rec 三个模型可以共用同一套预处理参数。提示缩放建议用InterpolationFlags.Linear这是 PaddleOCR 在 Python 里默认的cv2.INTER_LINEAR。如果改成 Cubic某些边缘字的置信度会波动但文本内容不会大变。4. 把检测、方向分类、识别串成一条 OCR 流水线最小可运行源码4.1 检测从概率图到文本框det 模型输出的是一张概率图每个像素表示该位置是文字区域的概率。后处理要做的有两步阈值二值化然后找连通域。PP-OCRv3 使用 DBNet 结构直接把概率图二值化后找轮廓比传统 MSER 方法稳得多。public ListRect2f Detect(Mat bgr, float detThreshold, double minArea, int maxSideLen 960) { int h bgr.Rows, w bgr.Cols; float scale 1f; if (Math.Max(h, w) maxSideLen) scale (float)maxSideLen / Math.Max(h, w); int newW (int)(w * scale) / 32 * 32; int newH (int)(h * scale) / 32 * 32; newW Math.Max(32, newW); newH Math.Max(32, newH); float[] data MatToTensor(bgr, newH, newW, new float[] { 0.485f, 0.456f, 0.406f }, new float[] { 0.229f, 0.224f, 0.225f }); var input new DenseTensorfloat(data, new[] { 1, 3, newH, newW }); var inputs new ListNamedOnnxValue { NamedOnnxValue.CreateFromTensorfloat(x, input) }; using var output _detSession.Run(inputs); var prob output.First().AsTensorfloat(); Mat probMat new Mat(newH, newW, MatType.CV_32FC1); for (int y 0; y newH; y) for (int x 0; x newW; x) probMat.Atfloat(y, x) prob[0, 0, y, x]; Mat mask new Mat(); Cv2.Threshold(probMat, mask, detThreshold, 255, ThresholdTypes.Binary); mask.ConvertTo(mask, MatType.CV_8UC1); Cv2.FindContours(mask, out Point[][] contours, out HierarchyIndexes[] hierarchy, RetrievalModes.External, ContourApproximationModes.ApproxSimple); var boxes new ListRect2f(); foreach (var contour in contours) { if (Cv2.ContourArea(contour) minArea) continue; RotatedRect rr Cv2.MinAreaRect(contour); var box rr.BoundingRect2f(); box.X / scale; box.Y / scale; box.Width / scale; box.Height / scale; boxes.Add(box); } return boxes; }detThreshold我一般用 0.3这是 PaddleOCR 官方默认值。minArea按图片尺寸调整960 的图我用 8 到 16如果画面里有大量噪点就提到 30 以上。坐标最后要除以scale因为你检测用的是缩放后的图返回给界面预览时要用原图坐标。还有一个容易忽略的事newW和newH取 32 的倍数但缩放后如果原始尺寸小于 32会被强制提到 32这时scale已经不准了所以文本区域很小的截图会定位偏掉至少要保证原图长边不低于 64 再做检测。4.2 方向分类先判断文字是否倒置检测框出来之后每个框里的内容本身可能旋转了 180 度。cls 模型专门做两分类0表示正常1表示需要旋转 180 度。WinForms 里经常从扫描仪或相机拿图这步不做rec 识别率会直接从 90% 掉到 50% 以下。private bool NeedRotate180(Mat crop) { float[] data MatToTensor(crop, 48, 192, new float[] { 0.485f, 0.456f, 0.406f }, new float[] { 0.229f, 0.224f, 0.225f }); var input new DenseTensorfloat(data, new[] { 1, 3, 48, 192 }); var inputs new ListNamedOnnxValue { NamedOnnxValue.CreateFromTensorfloat(x, input) }; using var output _clsSession.Run(inputs); var scores output.First().AsTensorfloat(); return scores[0, 1] scores[0, 0]; }cls 的输入统一是48x192不跟你之前 det 的尺寸走。如果你的图片里文字方向固定是正常的可以跳过这个 Session 不加载能省一点启动时间和内存。但做通用 OCR 工具时我建议保留因为客户拍照时手机方向经常是反的。调用Recognize时如果NeedRotate180返回 true就用 OpenCvSharp 转正if (NeedRotate180(crop)) Cv2.Rotate(crop, crop, RotateFlags.Rotate180);4.3 识别Crop → rec 模型 → CTC 解码rec 模型把固定高度 48 的文字条转成特征序列输出形状是[1, seqLen, vocabSize]。每个时间步取概率最大的类再去掉 blank 和相邻重复就是最终识别结果。public string Recognize(Mat bgr, Rect2f box, out float confidence) { Mat crop new Mat(bgr, new Rect((int)box.X, (int)box.Y, (int)box.Width, (int)box.Height)); if (NeedRotate180(crop)) Cv2.Rotate(crop, crop, RotateFlags.Rotate180); int recH 48; int recW Math.Max((int)(crop.Width * (float)recH / crop.Height), 16); float[] data MatToTensor(crop, recH, recW, new float[] { 0.485f, 0.456f, 0.406f }, new float[] { 0.229f, 0.224f, 0.225f }); var input new DenseTensorfloat(data, new[] { 1, 3, recH, recW }); var inputs new ListNamedOnnxValue { NamedOnnxValue.CreateFromTensorfloat(x, input) }; using var output _recSession.Run(inputs); var pred output.First().AsTensorfloat(); string text DecodeCtc(pred, out confidence); return text; } private string DecodeCtc(Tensorfloat pred, out float confidence) { int seqLen pred.Dimensions[1]; int classNum pred.Dimensions[2]; StringBuilder sb new StringBuilder(); int lastId -1; float totalScore 0f; for (int t 0; t seqLen; t) { int maxId 0; float maxScore float.MinValue; for (int c 0; c classNum; c) { float score pred[0, t, c]; if (score maxScore) { maxScore score; maxId c; } } if (maxId ! 0 maxId ! lastId) sb.Append(_vocab[maxId - 1]); totalScore maxScore; lastId maxId; } confidence totalScore / Math.Max(seqLen, 1); return sb.ToString(); }CTC 解码时_vocab[maxId - 1]是因为_vocab[0]是 blank实际字符从下标 1 开始。lastId用来合并连续相同字符比如 “AA” 在模型输出里可能是 A、A、blank、A不加去重会变成 “AAA” 或 “A A”。置信度我取所有时间步最大概率的平均值方便在 WinForms 列表里按置信度排序把低分的框过滤掉或标红。4.4 在 WinForms 按钮事件里把流程串起来在窗体上放一个 TextBox 填图片路径、一个 PictureBox 预览、一个 ListBox 显示结果、一个 Button 触发识别。按钮事件里的代码不需要把上面所有逻辑再写一遍private async void btnRecognize_Click(object sender, EventArgs e) { if (_ocrEngine null) { string dir AppDomain.CurrentDomain.BaseDirectory; _ocrEngine new OcrEngine( Path.Combine(dir, models, ch_PP-OCRv3_det.onnx), Path.Combine(dir, models, ch_PP-OCRv3_cls.onnx), Path.Combine(dir, models, ch_PP-OCRv3_rec.onnx), Path.Combine(dir, models, ppocr_keys_v1.txt)); } btnRecognize.Enabled false; lblStatus.Text 正在识别…; listResults.Items.Clear(); try { using var mat Cv2.ImRead(txtImagePath.Text.Trim(), ImreadModes.Color); if (mat.Empty()) { lblStatus.Text 图片打开失败; return; } var result await Task.Run(() { var boxes _ocrEngine.Detect(mat, 0.3f, 8f); var items new Liststring(); foreach (var box in boxes) { string text _ocrEngine.Recognize(mat, box, out float conf); items.Add(${text} | {conf:P1} | X{box.X:F0} Y{box.Y:F0} W{box.Width:F0} H{box.Height:F0}); } return items; }); foreach (var item in result) listResults.Items.Add(item); lblStatus.Text $检测到 {result.Count} 个文本框; } catch (Exception ex) { lblStatus.Text 错误 ex.Message; } finally { btnRecognize.Enabled true; } }这里最值得强调的不是 OCR 本身而是Task.Run。PaddleOCR 在 CPU 上跑一张 960x960 的图通常要几百毫秒到一秒放在 UI 线程上直接导致 WinForms 窗体未响应拖动窗口、点击按钮全部无效。上面用async voidTask.Run把推理挪到后台线程结果通过await回到 UI 线程界面一直能刷新。WinForms 程序员最容易忽略的就是这一点等客户说「怎么一点就卡死」其实程序没死只是 UI 线程被 OCR 推理占死了。5. WinForms 部署 PaddleOCR V3 的避坑清单5 个我踩过的坑5.1 识别结果全是乱码字符现象程序能跑检测框也有但输出的文字像天书完全不是图片内容。原因字典文件里第一个字符被当成真实文本处理了或者预处理时 RGB 通道顺序错乱。解决先检查ppocr_keys_v1.txt首行是不是blank如果是DecodeCtc里就必须用_vocab[maxId - 1]然后检查MatToTensor里的Cv2.CvtColor确认已经 BGR 转 RGB。我见过有人两个错误同时犯输出还有模有样的乱码。5.2 检测框和文字对不上坐标偏得离谱现象框在图上位置不对要么整体偏左上要么缩了一圈。原因det 推理时对图像做了缩放但返回坐标时没有把缩放系数乘回去或者宽高取 32 倍数时直接截断了原图比例。解决像我 4.1 节一样在缩放前记录scale所有最终坐标除以scale。注意要除以缩放系数而不是乘法因为你是把大图变小再推理还原回大图时要放大坐标。取 32 倍数时用/ 32 * 32的整数操作不要用Math.Round四舍五入否则宽度和高度可能差几十个像素。5.3 发布到别的机器上提示找不到 onnxruntime 或 OpenCvSharp 的 DLL现象开发机跑得好好的复制整个 Release 目录到客户机器双击 exe 就报BadImageFormatException或DllNotFound。原因ONNX Runtime 的原生 DLL 在runtimes/win-x64/native目录下OpenCvSharp 的原生 DLL 是OpenCvSharpExtern.dll打包 WinForms 安装程序时没把它们按目录结构带过去。解决项目平台强制设为x64把runtimes目录整体复制到部署目录Inno Setup 打包时添加这一整层。如果你的客户机器是 32 位系统趁早放弃 x86 的念头PP-OCRv3 在 32 位进程里跑大图内存吃不消。5.4 第一次点按钮能识别第二次点就内存暴涨或直接崩现象同一个按钮点三次内存从 100MB 涨到 1GB最后提示 OOM。原因每次点击都new InferenceSession旧 Session 没释放非托管内存一直堆积或者using var mat Cv2.ImRead没有在循环里释放det 返回很多框时 Mat 没被 Dispose。解决OcrEngine做成窗体私有字段只创建一次Dispose在窗体关闭时调用。循环里创建的所有Mat、Cv2.Rotate产生的中间对象能放进using就放进using。ONNX Runtime 的 Session 是线程安全的同一个引擎可以被Task.Run里的多个任务并发调用不需要为每次识别重建。5.5 在客户机器上界面卡死后台 CPU 却满了现象客户点识别后程序界面能拖但是很卡任务管理器里看到 CPU 一直 100%。原因虽然推理在后台线程跑但IntraOpNumThreads设得太大ONNX Runtime 把机器所有逻辑核都拿去跑算子UI 线程拿不到 CPU 时间。解决设IntraOpNumThreads为Environment.ProcessorCount / 2或者物理核心数别写成默认值。同时给 WinForms 按钮在识别期间置灰避免用户连续点击造成任务堆积。客户机器如果是 4 核 8 线程设 4 就够了设 8 反而会因为线程切换让单次推理更慢。6. 交付前的事压测一张图和 Python 结果对齐把速度调到能交付6.1 用 SessionOptions 限制线程让 OCR 让位给其他业务如果你做的是上位机软件OCR 只是其中一个模块CPU 不能全给推理用。加载模型时的SessionOptions里已经可以控制线程数我通常会这样写var opts new SessionOptions { GraphOptimizationLevel GraphOptimizationLevel.ORT_ENABLE_ALL, IntraOpNumThreads Math.Max(2, Environment.ProcessorCount / 2), ExecutionMode ExecutionMode.ORT_SEQUENTIAL };ORT_SEQUENTIAL表示算子按顺序执行不做并行虽然极端情况下会慢一点但能让其他线程稳定运行。WinForms 里如果有实时图像采集最好把 OCR 线程优先级设为ThreadPriority.BelowNormal避免识别大图时把采集线程饿死。6.2 和 Python 结果对齐文本、置信度、坐标全对比模型转换后最怕的是 C# 侧预处理和后处理出了问题推理结果表现成「大部分能识别但个别字错」。交付前写一个临时方法对同一张测试图跑 10 次输出每段文本、置信度、检测框坐标和 Python 里用 PaddleOCR 跑出来的结果逐项对齐。var stopwatch Stopwatch.StartNew(); for (int i 0; i 10; i) { var boxes _ocrEngine.Detect(mat, 0.3f, 8f); foreach (var box in boxes) { string text _ocrEngine.Recognize(mat, box, out float conf); Console.WriteLine(${text}|{conf:F2}|{box.X:F0},{box.Y:F0},{box.Width:F0},{box.Height:F0}); } } stopwatch.Stop(); Console.WriteLine(平均耗时: stopwatch.ElapsedMilliseconds / 10.0 ms);允许 1 到 2 像素的坐标误差属于正常但文本内容应该一致置信度不应该出现断崖式差异。如果某个字在 Python 里对、在 C# 里错优先检查MatToTensor的 resize 和归一化而不是怀疑 ONNX 转换。PP-OCRv3 对图像缩放非常敏感Python 里默认对 rec 宽度做了min(width, 320)之类的限制C# 侧也要加同样限制recW Math.Min(recW, 320)否则超宽文字条会让 CTC 序列过长识别出多余字符。6.3 把模型文件、DLL 和字典一起纳入安装程序最后一步是 WinForms 打包成安装程序。无论用 Visual Studio Installer 还是 Inno Setup模型目录和 native DLL 都得按运行时路径放好。我的习惯是启动时做一次健壮性检查缺哪个文件就明确告诉用户缺什么而不是让 ONNX Runtime 抛一个看不懂的异常。private void CheckModels() { string dir AppDomain.CurrentDomain.BaseDirectory; string[] required { models\ch_PP-OCRv3_det.onnx, models\ch_PP-OCRv3_cls.onnx, models\ch_PP-OCRv3_rec.onnx, models\ppocr_keys_v1.txt }; foreach (var path in required) { if (!File.Exists(Path.Combine(dir, path))) MessageBox.Show(缺少模型文件: path, 部署错误, MessageBoxButtons.OK, MessageBoxIcon.Error); } }部署目录里如果出现了runtimes\win-x64\native\onnxruntime.dll确认它的平台是 x64不是 anycpu。OpenCvSharp 的 native DLL 会被 NuGet 自动拷贝但安装程序如果只打了 exe 和模型文件还是会漏。这条路径我在第一次打包时翻过车后来把所有依赖都放到同一层native目录然后用相对路径加载才彻底消掉这个问题。做这个 C# WinForm 部署 PaddleOCR V3 的工程我最深刻的教训是不要在 UI 线程上碰推理不要靠猜输入输出名不要省掉和 Python 结果对齐的步骤。只要这三条守住剩下的只是把代码从这篇文章里搬进你的项目改一改模型路径就能跑出一条可交付的离线 OCR 链路。希望帮到你。本文还有配套的精品资源点击获取
返回列表