简介:面向C#/.NET开发者的PaddleOCRSharp数字识别示例项目,基于PaddleOCR封装类库,只需几行代码即可调用高精度中英文OCR,支持数字识别、文本检测与表格识别,注释细致,适合深度学习初学者与业务集成开发者自学和二次开发。压缩包共87个文件、约103MB,包含27个dll动态库(如基于PaddleOCR修改的PaddleOCR.dll及OpenCV x64依赖)、5组pdmodel/pdiparams模型与参数文件、C#窗体源码、可直接运行的exe及配套配置;模型文件保证离线可用,无需联网。目前已有3331人学习,项目采用WinForms结构,提供可直接运行的窗体示例,从项目打开到识别结果展示路径清晰,配合详细注释和清晰的目录划分,读者可快速掌握OCR调用流程,同时也可作为中英文识别、数字识别场景下的工程参考。对于想要降低PaddleOCR使用门槛的团队,这套demo展示了NuGet即装即用的封装风格,能帮助在离线环境下快速验证识别效果,并便于后续向表格识别等方向扩展。 做工业上位机的朋友应该都有过这种经历:现场设备报错,屏幕上显示一串数字,客户要把这串数字录入系统,再跟后台数据核对。手工录入既慢又容易出错,人一多、设备一多,光靠眼睛盯根本不现实,所以OCR替代人工录入是个很自然的诉求。但C#这个生态里做深度学习OCR,可选方案不算多,我最早接触PaddleOCR是在Python项目里做文字检测,效果确实让人满意,可要搬到C#项目里用,中间隔着模型部署和底层调用这道鸿沟。后来找到了PaddleOCRSharp,一个把PaddleOCR推理能力封装成.NET可以直接调的库,我才算把这条链路打通。这篇文章就把我基于PaddleOCRSharp写数字识别demo的完整过程整理出来,包含依赖部署、核心代码、以及让识别准确率从“能跑”到“能看”的一系列调整,适合想在上位机或者WinForms项目里快速接入文字识别能力的朋友参考。
1. 在C#里做OCR,为什么挑上PaddleOCRSharp
1.1 从Tesseract到云端API,我试过一圈
早几年用C#做OCR,很多人第一反应是Tesseract。它免费、开源、有.NET封装,但真放到工业场景里,问题很明显:印刷体数字在干净背景上表现尚可,一旦遇到仪表盘反光、数字倾斜、亮度不均匀,识别结果就是一团乱码;而且它需要针对字体做训练,调起来非常费劲。我也试过Windows自带的OCR能力,Win10以上系统确实提供了接口,但调用方式偏底层,处理中文和数字时还经常受系统语言包影响,部署到客户机器上还要额外确认系统版本,完全不可控。
云端OCR是另一个思路,百度、阿里、腾讯的通用文字识别API效果都不错,尤其对数字和印刷体,准确率能到99%以上。但工业项目里有个硬约束:很多客户现场是内网隔离环境,不允许数据出网,连调用外部API的通道都没有。就算允许联网,按次计费的成本放到长期跑批场景里也是一笔不小的开销。所以最后我的结论很明确:需要一个离线、免费、在C#里能直接调用的深度学习OCR方案。
1.2 PaddleOCRSharp到底封装了什么
PaddleOCR是百度开源的OCR工具库,底层是深度学习模型加推理引擎,在python生态里已经很成熟了。PaddleOCRSharp做的就是把这个能力用C++封装成原生DLL,再通过P/Invoke暴露给.NET调用。从使用者的角度看,它其实包含三块能力:
- 文本检测:从整张图片里找出哪些区域有文字,这部分由检测模型(det)负责。
- 方向分类:判断文字区域是不是倒着的,比如旋转了180度的数字,要先纠正方向再识别。
- 文字识别:对已经抠出来的文字区域做逐字识别,这部分由识别模型(rec)负责。
这三步合在一起,就是一次完整的OCR调用。PaddleOCRSharp把这些细节都包好了,我们在C#里只需要创建引擎、传图片、拿结果。第一次用的时候,我最大的感受是:终于不用在C#里手动管理张量、通道和推理上下文了,写起来跟调普通类库一个手感。
数字识别其实是PaddleOCR能力里比较基础的一种,因为数字类别只有10个,识别模型本身对数字的支持就很好,所以做demo阶段的重点反而不在算法,而在部署链路和调用姿势。
2. 搭建跑通的第一个数字识别Demo:依赖与部署细节
2.1 依赖包和模型文件夹,一开始别漏掉
PaddleOCRSharp的引入方式有两种:一种是直接在NuGet里搜PaddleOCRSharp安装,另一种是去GitHub上下载包含完整运行库的发布包,自己放到项目里引用。我推荐第一次做demo直接用NuGet安装,省心,依赖关系会自动带进来。
安装命令很简单:
Install-Package PaddleOCRSharp但要注意一个关键点:NuGet包里装的是.NET封装层和部分原生库,识别模型文件通常不会自动下载,需要单独准备models目录。一般发布包里的models文件夹长这样:
| 目录/文件 | 作用 |
|---|---|
ch_PP-OCRv4_det_infer | 文本检测模型的推理目录 |
ch_PP-OCRv4_rec_infer | 文字识别模型的推理目录 |
ch_ppocr_mobile_v2.0_cls_infer | 方向分类模型的推理目录 |
paddle_inference.dll | Paddle推理引擎原生库 |
opencv_world*.dll | 图像处理所需的OpenCV原生库 |
模型目录里是.pdmodel和.pdiparams这类文件,这是Paddle推理格式,不是训练格式,千万别拿训练用的文件来替换。如果不清楚具体该放在哪个位置,最稳妥的做法是把整个models文件夹复制到程序输出目录,让det_infer、rec_infer的路径指向它的子目录。我见过不少人在这个环节报错,提示找不到初始化模型,最后基本都是路径没配对。
2.2 新建WinForms项目的两个关键设置
demo我用的是WinForms,因为C#上位机场景大多也是WinForms或者WPF,界面代码大家最熟。建项目时有两点一定要提前确认:
第一,平台目标必须设为x64。PaddleOCRSharp配套的原生库目前只有64位版本,如果项目默认是AnyCPU,在64位系统上通常也能跑,但一旦遇到32位进程兼容问题,就会冒出BadImageFormatException这类错误。我在一个老项目里踩过这个坑,项目原本是x86编译的,引入PaddleOCRSharp后一启动就崩,最后把整个解决方案平台改成x64才解决。如果你维护的是老系统,这个动作会影响其他依赖,提前和团队沟通好。
第二,目标框架建议.NET Framework 4.7.2以上,或者.NET 6以上。PaddleOCRSharp对较新的.NET版本支持得更好,用老版本Framework可能会遇到NuGet包还原不完全的问题。
2.3 能跑通的最小代码量
在form上加一个按钮,点一下弹对话框选图片,然后直接调用识别,核心代码就这么点:
using System; using System.Windows.Forms; using PaddleOCRSharp; public partial class MainForm : Form { private PaddleOCREngine _engine; public MainForm() { InitializeComponent(); _engine = new PaddleOCREngine(); } private void btnRecognize_Click(object sender, EventArgs e) { using (OpenFileDialog dlg = new OpenFileDialog()) { dlg.Filter = "图片文件|*.png;*.jpg;*.bmp"; if (dlg.ShowDialog() != DialogResult.OK) return; OCRResult result = _engine.DetectText(dlg.FileName); txtResult.Text = result.Text; } } }这段代码在模型文件默认放在根目录的情况下就能跑通,OCRResult.Text是所有识别文本拼起来的结果。我第一次跑通的时候心里是有点惊讶的,因为前前后后只写了十几行代码,深度学习OCR的门槛确实被封装得比想象中低很多。
不过demo归demo,真要放到项目里,引擎不能每次识别都重新创建,因为模型加载非常耗时,最好在窗体启动时创建一次,整个生命周期复用。上面的代码把引擎放成了窗体字段,就是基于这个考虑。
3. 核心识别流程拆解:从Bitmap到识别结果
3.1 初始化:模型路径和推理参数
如果使用默认构造函数,引擎会去当前目录找模型。更稳的写法是手动指定模型配置,这样程序部署到别的机器后,即使目录结构变了也能快速调整。我推荐使用OCRModelConfig和OCRParameter这两个类来管理:
var config = new OCRModelConfig { det_infer = @"models\ch_PP-OCRv4_det_infer", rec_infer = @"models\ch_PP-OCRv4_rec_infer", cls_infer = @"models\ch_ppocr_mobile_v2.0_cls_infer" }; var param = new OCRParameter { enable_mkldnn = true, text_score = 0.6f, use_gpu = false }; _engine = new PaddleOCREngine(config, param);这里几个参数单独解释一下。enable_mkldnn是开启Intel的推理加速库,实测在CPU机器上能明显减少推理时间,建议默认开;text_score是识别结果的可信度阈值,低于这个分数的文本会被过滤掉,数字识别场景里如果画面干净可以调到0.5,画面脏就调高到0.7左右;use_gpu则看部署机器,有NVIDIA显卡可以开,否则不要碰。
值得强调的是,这三个模型路径都不是指向单个模型文件,而是指向包含推理模型文件的目录。刚接触Paddle推理格式的人很容易搞混,以为是选择.pdmodel文件,实际应该是ch_PP-OCRv4_rec_infer这样的文件夹。选错的话,初始化的时候不会立刻报错,直到第一张图片检测时才抛异常,排查成本比较高。
3.2 检测和识别一次调用返回什么
DetectText方法是我用得最多的调用入口,它接收两种常见输入:文件路径和Bitmap对象。我后来把截图功能接进来后发现,WinForms程序里直接传Bitmap更方便,因为截图本来就是内存图像。
返回的OCRResult对象里,主要关注这几个属性:
Text:识别出来的全部文本拼接结果。TextBlocks:每个独立文本区域的集合,每块包含文字内容、置信度得分、位置框坐标。
举个例子,一张图片上有两排数字,用result.Text拿到的可能是"12345\n67890"或者"1234567890",具体取决于模型检测出的文字块顺序。而用TextBlocks可以拿到每个数字块的精确位置,这在做区域统计时非常有用,比如把屏幕里某几个固定位置的仪表读数挑出来。
我习惯先打印每个文本块的得分,判断是不是识别错了。代码这样写:
foreach (var block in result.TextBlocks) { Console.WriteLine($"文本:{block.Text},得分:{block.Score:F2}"); }得分高不代表一定对,但得分低基本就是画面质量不行,这个规律在后续调优里帮我省了很多事。
3.3 只留下数字:正则和数据清洗
PaddleOCR的识别模型默认带中英文字符集,所以识别结果里除了数字,可能混入字母、中文甚至特殊符号。数字识别demo的最终目的是拿到纯数字,这时候就要做一层清洗。
我一般用正则表达式解决:
using System.Text.RegularExpressions; string rawText = result.Text; string digitsOnly = Regex.Replace(rawText, @"[^0-9]", "");这样会把非数字字符全部去掉,得到一串连续数字。如果是带小数的数字,比如仪表显示12.34,保留小数点的正则需要这样写:
string numberText = Regex.Match(rawText, @"-?\d+\.?\d*").Value;这里多留了一个负号匹配,因为有些设备会显示负温度、负压力,提前把负数情况考虑进去,后面加需求时不会被卡住。还有一点:如果识别结果里同时存在多个数字区域,用Regex.Replace会把它们拼在一起,容易丢失边界。更合理的方式是先遍历TextBlocks,对每块单独清洗,再按业务规则拼接。
4. 把数字识别从能用变成好用:调优与避坑
4.1 识别区域裁剪和图像放大带来的提升
demo跑通之后,我第一次拿真实表计照片测,效果并不理想。问题出在照片里除了数字,还有大量背景干扰,比如表盘刻度、品牌logo、反光斑块。PaddleOCR虽然有检测模型,会先找出文字区域,但背景越复杂、数字越小,漏检率就越高。
我的处理办法是:把识别区域先裁剪出来,再做等比放大。比如只要仪表面板中间那一块读数区,就在代码里用Bitmap把这个区域截出来,然后缩放到原来的2到3倍再传给引擎。
using System.Drawing; Bitmap CropAndScale(Bitmap src, Rectangle roi, float scale) { Bitmap cropped = src.Clone(roi, src.PixelFormat); int newWidth = (int)(cropped.Width * scale); int newHeight = (int)(cropped.Height * scale); Bitmap scaled = new Bitmap(cropped, newWidth, newHeight); return scaled; }放大为什么有效?因为识别模型在训练时见过的最合适文字尺寸是有限范围的,小字直接喂进去,特征图上的响应太弱,放大3倍之后,数字笔画在模型内部的感受野里占据更合理的比例,识别成功率明显提高。我实测一组仪表照片,放大前准确率大概在85%,放大加裁剪后能到96%以上。这是个极其朴素但非常有效的技巧。
4.2 关于推理速度和内存占用
不少第一次用C#集成深度学习库的人会担心性能,其实PaddleOCRSharp在CPU上的表现比我预想的好。我拿一台i5-8500的工控机测试,单张1920x1080的图,检测加识别一次大约在300到500毫秒之间,如果只识别裁剪出的数字区域,150毫秒左右就能出结果。这个速度放到“人工点按钮看一眼”的半自动场景下完全够用,但要做到实时视频流识别,比如每秒25帧去读仪表,就不现实了。
想要压速度,可以从两个方向入手:一是缩小输入图像分辨率,二是尽可能提前裁剪。PaddleOCR的检测模型是对整张图做特征提取,图越大计算量越大,所以截成小区域是立竿见影的优化手段。另外,enable_mkldnn一定要打开,关闭状态下同样的图推理时间可能翻倍。
内存方面,引擎初始化后大约占用几百MB内存,这个数字在不同模型版本上会有波动。如果程序里还有其他大内存需求,建议把OCR引擎单独封装成一个后台任务,识别完成就释放图像对象,避免Bitmap残留导致内存涨上去。
4.3 何时需要考虑重新训练模型
做数字识别demo时,很多人一上来就担心模型认不出自己设备的数字字体。实际上PaddleOCR自带的通用识别模型已经覆盖了0到9、英文字母和常用中文,数字识别这种任务,预训练模型通常已经够用。只有遇到极度花哨的七段数码管、点阵屏、艺术字这类特殊字形,才需要考虑用标注数据去微调模型。
PaddleOCR提供了模型训练和微调工具,训练完成后会导出成推理模型,把导出的目录替换掉PaddleOCRSharp的rec_infer配置即可。注意替换时一定要保持目录内文件结构一致,通常就包含.pdmodel、.pdiparams和inference.pdiparams.info等几个文件。我自己在液晶屏字符识别项目里用过一次微调,大概准备了几百张标注图片,训练后准确率从90%左右提升到了99%,效果很明显。
但这个过程是有门槛的,需要准备数据集、配置训练环境、调训练参数。如果你只是做普通的仪表数字读取,我的建议是先别碰训练,把精力放在图像预处理和场景约束上,往往性价比更高。
最后再分享一个我自己常用的验证方法:拿一批真实场景图片,每张图先人工标出期望的数字结果,再用程序批量跑一遍识别,把输出结果和期望值做比对,统计准确率。这样每次改参数、加预处理,都能用数据说话,而不是“感觉好像准了点”。这个方法看着笨,但在我调PaddleOCRSharp的过程中,几乎所有的效果提升都是靠它一步步验证出来的。
本文还有配套的精品资源,点击获取