用 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 测试界面。下载后配置好模型就可以直接初始化、选择图片、识别、查看耗时和结果。效果项目做了什么这版主要实现了使用 OpenCV 5 DNN 加载 ONNX 模型支持 PP-OCRv5 / PP-OCRv6 模型切换C 封装 OCR 动态库C# WinForms 调用 DLL 测试支持图片选择、初始化、识别、结果展示支持在界面查看初始化成功或失败信息C# 端不再依赖 OpenCvSharp减少额外组件引用目前 OpenCV 5 DNN 暂时不考虑 GPU因此这版明确采用 CPU 推理。对于学习和跨环境部署来说这样反而更简单、稳定也更容易排查问题。为什么要做 OpenCV DNN 版本很多 OCR 示例要么是 Python 代码要么依赖较多推理框架。对于 C# 桌面项目、WinForms 工具、小型业务系统来说经常会遇到几个问题Python 不方便集成推理框架依赖复杂DLL 调用方式不清楚模型、字典、预处理、后处理流程分散新手不知道从哪里开始看所以这次专门做了一个完整工程。C 负责核心 OCR 推理C# 只负责界面、图片读取、调用 DLL 和展示结果。整体结构比较清楚方便学习也方便后续替换模型或集成到自己的系统里。支持的模型测试项目界面里增加了模型单选按钮可以直接选择PP-OCRv5 mobilePP-OCRv5 serverPP-OCRv6 smallPP-OCRv6 tiny所有模型统一放在inference文件夹下面项目启动和编译后会自动复制到输出目录使用起来比较直观。对于想对比不同模型速度和效果的朋友这个界面会很方便。C# 端更轻量原来的测试项目使用了 OpenCvSharp 读取图片。这次 OpenCV DNN 版的 C# 测试程序已经去掉 OpenCvSharp改为使用 .NET 自带的System.Drawing.BitmapLockBits连续 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# 调用、界面测试、耗时验证都在一起。后续计划后面还可以继续优化几个方向继续优化 OpenCV DNN 推理速度增加更多模型配置支持批量图片测试输出更详细的 det / rec 分段耗时整理更完整的新手教程对比 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 ListOCRResult 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 namesender/param /// param namee/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 namesender/param /// param namee/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.DeserializeObjectListOCRResult(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 namesender/param /// param namee/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?pwd9aed 提取码: 9aed