最近在整理 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 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# 调用、界面测试、耗时验证都在一起。
后续计划
后面还可以继续优化几个方向:
-
继续优化 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 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