当前位置: 首页 > news >正文

用 OpenCV 5 DNN 跑 PP-OCR:一个适合新手学习的 C++ 动态库 + C# 可视化测试项目

最近在整理 OCR 项目时,我把原来基于 ONNX Runtime DirectML 的 PP-OCRSharp 项目,重新做了一版基于 OpenCV 5 DNN 推理的实现:

lw.OpenCVDNN.PPOCRSharp

这个项目的目标很简单:
让想学习 OCR 工程化落地的朋友,可以从一个完整、清晰、可运行的项目开始,而不是只看到零散代码片段。

它包含 C++ 动态库、OpenCV 5 DNN 推理、PP-OCR 检测识别流程,以及 C# WinForms 测试界面。下载后配置好模型,就可以直接初始化、选择图片、识别、查看耗时和结果。

效果

项目做了什么

这版主要实现了:

  1. 使用 OpenCV 5 DNN 加载 ONNX 模型

  2. 支持 PP-OCRv5 / PP-OCRv6 模型切换

  3. C++ 封装 OCR 动态库

  4. C# WinForms 调用 DLL 测试

  5. 支持图片选择、初始化、识别、结果展示

  6. 支持在界面查看初始化成功或失败信息

  7. C# 端不再依赖 OpenCvSharp,减少额外组件引用

目前 OpenCV 5 DNN 暂时不考虑 GPU,因此这版明确采用 CPU 推理。对于学习和跨环境部署来说,这样反而更简单、稳定,也更容易排查问题。

为什么要做 OpenCV DNN 版本

很多 OCR 示例要么是 Python 代码,要么依赖较多推理框架。对于 C# 桌面项目、WinForms 工具、小型业务系统来说,经常会遇到几个问题:

  • Python 不方便集成

  • 推理框架依赖复杂

  • DLL 调用方式不清楚

  • 模型、字典、预处理、后处理流程分散

  • 新手不知道从哪里开始看

所以这次专门做了一个完整工程。

C++ 负责核心 OCR 推理,C# 只负责界面、图片读取、调用 DLL 和展示结果。整体结构比较清楚,方便学习,也方便后续替换模型或集成到自己的系统里。

支持的模型

测试项目界面里增加了模型单选按钮,可以直接选择:

  • PP-OCRv5 mobile

  • PP-OCRv5 server

  • PP-OCRv6 small

  • PP-OCRv6 tiny

所有模型统一放在inference文件夹下面,项目启动和编译后会自动复制到输出目录,使用起来比较直观。

对于想对比不同模型速度和效果的朋友,这个界面会很方便。

C# 端更轻量

原来的测试项目使用了 OpenCvSharp 读取图片。
这次 OpenCV DNN 版的 C# 测试程序已经去掉 OpenCvSharp,改为使用 .NET 自带的:

  • System.Drawing.Bitmap

  • LockBits

  • 连续 BGR byte 数组

然后直接调用 C++ 动态库的ocr2接口。

这样做的好处是:

  • 少一个 NuGet 依赖

  • 输出目录更干净

  • 调用链更容易理解

  • 更适合新手学习 DLL 调用和图像内存传递

界面效果

测试程序保留了和原来 PP-OCRv5 测试项目类似的 WinForms 界面:

  • 选择图片

  • 初始化模型

  • 执行识别

  • 显示识别文本

  • 显示完整 JSON

  • 在图片上绘制检测框

  • 显示耗时

  • 显示初始化成功或失败信息

点击“初始化”后,右侧文本框会输出当前加载的模型路径、字典路径、设备信息以及初始化结果。

这样如果模型路径错误、字典缺失或 DLL 依赖不完整,问题也能更快定位。

项目结构

整体结构大致如下:

lw.OpenCVDNN.PPOCRSharp C++ OCR 动态库 OpenCV 5 DNN 推理 det / rec / cls preprocess / postprocess DLL 导出接口 lw.OpenCVDNN.PPOCRSharp.Test C# WinForms 测试程序 模型选择 图片选择 初始化信息输出 OCR 结果展示 inference PP-OCRv5 / PP-OCRv6 模型 字典文件

核心 DLL 接口保持简单:

init(...) ocr2(...) destroy(...)

C# 侧通过DllImport调用即可。

适合谁学习

这个项目比较适合:

  • 想学习 OCR 工程落地的朋友

  • 想了解 PP-OCR C++ 推理流程的朋友

  • 想用 C# 调用 C++ OCR 动态库的朋友

  • 想研究 OpenCV DNN 加载 ONNX 模型的朋友

  • 想做桌面 OCR 工具、小工具、识别服务原型的朋友

它不是一个只展示算法的 Demo,而是更接近真实项目结构:模型、字典、DLL、C# 调用、界面测试、耗时验证都在一起。

后续计划

后面还可以继续优化几个方向:

  1. 继续优化 OpenCV DNN 推理速度

  2. 增加更多模型配置

  3. 支持批量图片测试

  4. 输出更详细的 det / rec 分段耗时

  5. 整理更完整的新手教程

  6. 对比 ONNX Runtime DirectML 和 OpenCV DNN 两种方案的速度差异

总结

lw.OpenCVDNN.PPOCRSharp是一个基于 OpenCV 5 DNN 的 PP-OCR C++ 动态库项目,并配套 C# WinForms 测试程序。

它的特点是:

  • 项目完整

  • 结构清楚

  • 方便新手学习

  • 支持 v5 / v6 模型切换

  • C# 端轻量化

  • 适合二次开发和集成测试

如果你正在学习 OCR、C++ DLL 封装、C# 调用本地库,或者想了解 OpenCV 5 DNN 如何跑 PP-OCR,这个项目会是一个不错的起点。

C#调用源码

using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Diagnostics; using System.Drawing; using System.Drawing.Imaging; using System.IO; using System.Runtime.InteropServices; using System.Text; using System.Windows.Forms; namespace OCRV5Test { public partial class Form1 : Form { public Form1() { InitializeComponent(); } const string DllName = "lw.OpenCVDNN.PPOCRSharp.dll"; //初始化 [DllImport(DllName, EntryPoint = "init", CallingConvention = CallingConvention.Cdecl)] public extern static int init(ref IntPtr engine , [MarshalAs(UnmanagedType.I1)] bool use_gpu , int gpu_id , string det_model_dir , int limit_side_len , double det_db_thresh , double det_db_box_thresh , double det_db_unclip_ratio , [MarshalAs(UnmanagedType.I1)] bool use_dilation , [MarshalAs(UnmanagedType.I1)] bool cls , [MarshalAs(UnmanagedType.I1)] bool use_angle_cls , string cls_model_dir , double cls_thresh , double cls_batch_num , string rec_model_dir , string rec_char_dict_path , int rec_batch_num , int rec_img_h , int rec_img_w , int predictor_num , StringBuilder msg); //识别 [DllImport(DllName, EntryPoint = "ocr", CallingConvention = CallingConvention.Cdecl)] public extern static int ocr(IntPtr engine, IntPtr image, StringBuilder msg, out IntPtr ocr_result, out int ocr_result_len); //识别,按图像内存传入,避免托管层依赖 C++ Mat ABI [DllImport(DllName, EntryPoint = "ocr2", CallingConvention = CallingConvention.Cdecl)] public extern static int ocr2(IntPtr engine, int rows, int cols, int channels, IntPtr data, StringBuilder msg, out IntPtr ocr_result, out int ocr_result_len); //释放 [DllImport(DllName, EntryPoint = "destroy", CallingConvention = CallingConvention.Cdecl)] public extern static int destroy(IntPtr engine, StringBuilder msg); static IntPtr OCREngine; private Bitmap bmp; private String imgPath = null; private List<OCRResult> ltOCRResult; private string fileFilter = "*.*|*.bmp;*.jpg;*.jpeg;*.tiff;*.tif;*.png"; private StringBuilder OCRResultInfo = new StringBuilder(); private StringBuilder OCRResultAllInfo = new StringBuilder(); Pen pen = new Pen(Brushes.Red, 2f); /// <summary> /// 选择图片 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void button1_Click(object sender, EventArgs e) { OpenFileDialog ofd = new OpenFileDialog(); ofd.Filter = fileFilter; if (ofd.ShowDialog() == DialogResult.OK) { imgPath = ofd.FileName; bmp?.Dispose(); bmp = new Bitmap(imgPath); pictureBox1.Image = bmp; richTextBox1.Clear(); button2_Click(null, null); } } /// <summary> /// 识别 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void button2_Click(object sender, EventArgs e) { if (OCREngine == IntPtr.Zero) { MessageBox.Show("请先初始化!!!"); return; } if (imgPath == null) { MessageBox.Show("请先选择图片!!!"); return; } button1.Enabled = false; button2.Enabled = false; richTextBox1.Clear(); OCRResultInfo.Clear(); OCRResultAllInfo.Clear(); StringBuilder msgTemp = new StringBuilder(128); Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); IntPtr strPtr = IntPtr.Zero; int ocr_result_len = 0; string ocr_result = string.Empty; int res; byte[] bgrData; using (Bitmap bgrBitmap = LoadBgrBitmap(imgPath, out bgrData)) { GCHandle handle = GCHandle.Alloc(bgrData, GCHandleType.Pinned); try { res = ocr2(OCREngine, bgrBitmap.Height, bgrBitmap.Width, 3, handle.AddrOfPinnedObject(), msgTemp, out strPtr, out ocr_result_len); } finally { handle.Free(); } } if (strPtr != IntPtr.Zero && ocr_result_len > 0) { byte[] buffer = new byte[ocr_result_len]; Marshal.Copy(strPtr, buffer, 0, ocr_result_len); ocr_result = Encoding.UTF8.GetString(buffer); Marshal.FreeCoTaskMem(strPtr); strPtr = IntPtr.Zero; } stopwatch.Stop(); double totalTime = stopwatch.Elapsed.TotalMilliseconds; OCRResultAllInfo.AppendLine($"耗时: {totalTime:F2}ms"); OCRResultAllInfo.AppendLine("---------------------------"); OCRResultInfo.AppendLine($"耗时: {totalTime:F2}ms"); OCRResultInfo.AppendLine("---------------------------"); if (res == 0) { ltOCRResult = Newtonsoft.Json.JsonConvert.DeserializeObject<List<OCRResult>>(ocr_result); OCRResultAllInfo.Append(JsonConvert.SerializeObject(ltOCRResult, Newtonsoft.Json.Formatting.Indented)); Graphics graphics = Graphics.FromImage(bmp); foreach (OCRResult item in ltOCRResult) { OCRResultInfo.AppendLine(item.text); System.Drawing.Point[] pt = new System.Drawing.Point[] { new System.Drawing.Point(item.x1, item.y1) , new System.Drawing.Point(item.x2, item.y2) , new System.Drawing.Point(item.x3, item.y3) , new System.Drawing.Point(item.x4, item.y4) }; graphics.DrawPolygon(pen, pt); } graphics.Dispose(); if (checkBox1.Checked) { richTextBox1.Text = OCRResultAllInfo.ToString(); } else { richTextBox1.Text = OCRResultInfo.ToString(); } pictureBox1.Image = null; pictureBox1.Image = bmp; } else { if (strPtr != IntPtr.Zero) { Marshal.FreeCoTaskMem(strPtr); } MessageBox.Show("识别失败," + msgTemp.ToString()); } button1.Enabled = true; button2.Enabled = true; } /// <summary> /// 初始化 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void Form1_Load(object sender, EventArgs e) { rdov6tiny.Checked = true; chkcls.Checked = false; LoadDefaultImage(); } private void checkBox1_CheckedChanged(object sender, EventArgs e) { richTextBox1.Clear(); if (checkBox1.Checked) { richTextBox1.Text = OCRResultAllInfo.ToString(); } else { richTextBox1.Text = OCRResultInfo.ToString(); } } private void radioButton1_CheckedChanged(object sender, EventArgs e) { RadioButton rb = sender as RadioButton; } private void Form1_FormClosing(object sender, FormClosingEventArgs e) { UnloadModel(); } private void btnDestroy_Click(object sender, EventArgs e) { UnloadModel(); } void UnloadModel() { if (OCREngine != IntPtr.Zero) { StringBuilder msgTemp = new StringBuilder(128); destroy(OCREngine, msgTemp); AppendStatus("释放成功: " + msgTemp.ToString()); OCREngine = IntPtr.Zero; } } private void btnInit_Click(object sender, EventArgs e) { richTextBox1.Text = ""; if (OCREngine != IntPtr.Zero) { StringBuilder msgTemp = new StringBuilder(128); destroy(OCREngine, msgTemp); AppendStatus("释放成功: " + msgTemp.ToString()); OCREngine = IntPtr.Zero; LoadModel(); } else { LoadModel(); } } void LoadModel() { StringBuilder msgTemp = new StringBuilder(128); bool use_gpu = false; int gpu_id = 0; string det_model_dir = ""; int limit_side_len = 960; double det_db_thresh = 0.3; double det_db_box_thresh = 0.6; double det_db_unclip_ratio = 1.2; bool use_dilation = false; bool cls = true; bool use_angle_cls = true; string cls_model_dir = ""; double cls_thresh = 0.9; int cls_batch_num = 1; string rec_model_dir = ""; string rec_char_dict_path = "inference/ppocrv5_dict.txt"; int rec_batch_num = 8; int rec_img_h = 48; int rec_img_w = 320; int predictor_num = 4; det_db_thresh = Convert.ToDouble(txtdet_db_thresh.Text.ToString()); det_db_box_thresh = Convert.ToDouble(txtdet_db_box_thresh.Text.ToString()); det_db_unclip_ratio = Convert.ToDouble(txtdet_db_unclip_ratio.Text.ToString()); if (chkcls.Checked == true) { cls = true; } else { cls = false; } cls_batch_num = Convert.ToInt32(txtcls_batch_num.Text.ToString()); rec_batch_num = Convert.ToInt32(txtrec_batch_num.Text.ToString()); predictor_num = Convert.ToInt32(txtpredictor_num.Text.ToString()); if (rdomobile.Checked) { det_model_dir = "inference/PP-OCRv5_mobile_det_onnx.onnx"; rec_model_dir = "inference/PP-OCRv5_mobile_rec_onnx.onnx"; cls_model_dir = "inference/PP-OCRv5_mobile_cls_onnx.onnx"; rec_char_dict_path = "inference/ppocrv5_dict.txt"; } elseif (rdov6small.Checked) { det_model_dir = "inference/PP-OCRv6_small_det.onnx"; rec_model_dir = "inference/PP-OCRv6_small_rec.onnx"; rec_char_dict_path = "inference/PP-OCRv6_small_rec_dict.txt"; cls = false; use_angle_cls = false; } elseif (rdov6tiny.Checked) { det_model_dir = "inference/PP-OCRv6_tiny_det.onnx"; rec_model_dir = "inference/PP-OCRv6_tiny_rec.onnx"; rec_char_dict_path = "inference/PP-OCRv6_tiny_rec_dict.txt"; cls = false; use_angle_cls = false; } else { det_model_dir = "inference/PP-OCRv5_server_det_infer.onnx"; rec_model_dir = "inference/PP-OCRv5_server_rec_infer.onnx"; rec_char_dict_path = "inference/ppocrv5_dict.txt"; cls = false; use_angle_cls = false; } AppendStatus("正在初始化模型..."); AppendStatus("det: " + det_model_dir); AppendStatus("rec: " + rec_model_dir); AppendStatus("dict: " + rec_char_dict_path); AppendStatus("device: CPU(OpenCV DNN)"); int res = init(ref OCREngine , use_gpu , gpu_id , det_model_dir , limit_side_len , det_db_thresh , det_db_box_thresh , det_db_unclip_ratio , use_dilation , cls , use_angle_cls , cls_model_dir , cls_thresh , cls_batch_num , rec_model_dir , rec_char_dict_path , rec_batch_num , rec_img_h , rec_img_w , predictor_num , msgTemp); if (res == 0) { AppendStatus("模型加载成功: " + msgTemp.ToString()); } else { string msg = msgTemp.ToString(); AppendStatus("模型加载失败: " + msg); MessageBox.Show("模型加载失败," + msg); } } private void AppendStatus(string text) { richTextBox1.AppendText($"[{DateTime.Now:HH:mm:ss}] {text}{Environment.NewLine}"); } private Bitmap LoadBgrBitmap(string path, out byte[] bgrData) { using (Bitmap source = new Bitmap(path)) { Bitmap bitmap = new Bitmap(source.Width, source.Height, PixelFormat.Format24bppRgb); using (Graphics g = Graphics.FromImage(bitmap)) { g.DrawImage(source, 0, 0, source.Width, source.Height); } Rectangle rect = new Rectangle(0, 0, bitmap.Width, bitmap.Height); BitmapData data = bitmap.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb); try { int rowBytes = bitmap.Width * 3; bgrData = new byte[rowBytes * bitmap.Height]; for (int y = 0; y < bitmap.Height; y++) { IntPtr src = IntPtr.Add(data.Scan0, y * data.Stride); Marshal.Copy(src, bgrData, y * rowBytes, rowBytes); } } finally { bitmap.UnlockBits(data); } return bitmap; } } private void LoadDefaultImage() { string defaultImagePath = Path.Combine(Application.StartupPath, "3.jpg"); if (!File.Exists(defaultImagePath)) { defaultImagePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "3.jpg"); } if (!File.Exists(defaultImagePath)) { return; } imgPath = defaultImagePath; bmp?.Dispose(); bmp = new Bitmap(imgPath); pictureBox1.Image = bmp; } } }

下载

通过网盘分享的文件:lw.OpenCVDNN.PPOCRSharp.Test.rar 链接: https://pan.baidu.com/s/1xd7NzdkYKvoCy1VApOxbJg?pwd=9aed 提取码: 9aed

http://www.jsqmd.com/news/993991/

相关文章:

  • VRCX:重新定义VRChat社交管理的智能伴侣
  • LeetCode CodeTop 82.删除排序链表中的重复元素Ⅱ
  • 2026年 重庆磷酸二氢钾/磷酸氢二钾/磷酸二氢钠/磷酸氢二钠/磷酸三钠厂家推荐:稳定品质与精准应用的化工源头之选 - 品牌发掘
  • Apache SeaTunnel 5 月月报:87 个 PR 合入,多维度升级功能、优化性能与修复 Bug
  • 别再手动重复造轮子了!用C#/Python为PowerMill打造你的专属自动化工具库
  • 全面解析行为验证码技术:从滑动拼图到文字点选的实战解决方案
  • P89LPC93x单片机UART、I2C、SPI、ADC外设深度解析与实战配置
  • 美团APP店铺与评论数据自动化采集工具(含签名生成、多接口协同与反反爬适配)
  • XXL-Job调度中心‘隐身’记:如何在不暴露Admin页面的情况下,让它在你的SpringCloud微服务里默默干活
  • STM32F103VC实测可用的CH19264E液晶屏8080并口驱动工程包
  • 用PyTorch复现论文:自动驾驶模型真的怕‘贴纸’攻击吗?实测5种对抗样本生成方法
  • 卫生间漏水到楼下怎么查找漏水点?2026吕梁24小时上门维修电话TOP7机构推荐,免费勘察+精准定位,专业师傅处理屋顶墙体洗手间暗管漏水 - 一休咨询
  • LayoutParser:5步搞定深度学习文档布局分析的完整指南
  • 卫生间漏水到楼下怎么查找漏水点?2026兰州24小时上门维修电话TOP7机构推荐,免费勘察+精准定位,专业师傅处理屋顶墙体洗手间暗管漏水 - 一休咨询
  • 如何快速部署GB28181视频监控平台:3步完成容器化配置
  • MPC8313E DDR与以太网接口时序设计实战解析
  • Windows下Python直连SAP RFC所需的nwrfc750官方SDK完整包(含DLL、头文件、示例与文档)
  • 具身智能数据产业链揭秘:从采集员到独角兽,数据复售模式能走多远?
  • 天津红桥防水补漏哪家靠谱?2026正规修缮公司排名实测(全区通用) - 苏易房屋修缮
  • 手把手教你搭建工业级Multi-Agent RAG系统,附完整代码与部署教程
  • 2026年广州注册公司代办服务推荐榜:一般、小规模纳税人、无地址注册、变更服务、异常处理一站式优质之选! - 信息热点
  • LeetCode CodeTop 88.合并两个有序数组
  • 天津河西防水补漏哪家靠谱?2026正规修缮公司排名实测(全区通用) - 苏易房屋修缮
  • 深入浅出跳表(SkipList):原理、实现与代码实战
  • 深度解析:Penpot云原生设计平台的微服务架构与性能优化实战指南
  • 如何高效使用downkyi哔哩下载姬:B站8K超高清视频下载终极指南
  • 2026北京朝阳区宝格丽首饰回收:这些细节决定回收价 - 逸程
  • 神经符号AI破局关键:一阶逻辑如何让AI既聪明又“讲理”?
  • 2026重庆奢侈品首饰回收实测盘点|正规渠道甄选与高价出货全攻略 - 薛定谔的梨花猫
  • CUDA从入门到精通(十四):Thrust库实战之并行算法重构