欢迎光临
我们一直在努力

用 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 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

    赞(0)
    未经允许不得转载:171主机测评 » 用 OpenCV 5 DNN 跑 PP-OCR:一个适合新手学习的 C++ 动态库 + C# 可视化测试项目
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址