用 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

相关推荐
兵慌码乱10 天前
基于 MediaPipe 与 PySide2 的手势交互音乐控制系统实现:轻量化视觉交互全流程解析
python·opencv·计算机视觉·人机交互·手势识别·mediapipe·pyside2
梦想三三14 天前
OpenCV银行卡数字识别项目(图像预处理与字符分割)
人工智能·opencv·计算机视觉
Sour14 天前
PDF翻译卡住不动怎么办?扫描件、OCR 和大文件排查清单
前端·pdf·ocr
武子康14 天前
调查研究-180 roboflow/supervision:计算机视觉工程里的“胶水层“,为什么值得关注?
人工智能·opencv·计算机视觉·chatgpt·llm·向量化
旗讯数字14 天前
旗讯 OCR 工业手写识别解决方案|破解车间纸质表单录入难题,加速生产数字化转型
大数据·ocr
XTIOT66614 天前
多形态护照 OCR 读取器传输机制、识别算法与行业落地技术对比
大数据·人工智能·嵌入式硬件·物联网·ocr
天天代码码天天14 天前
用 TensorRT 加速 PP-OCR:一套 C++ DLL + C# 调用的高性能 OCR 推理方案
c++·c#·ocr
m沐沐14 天前
【计算机视觉】OpenCV 模板匹配银行卡数字识别---下
人工智能·python·opencv·计算机视觉·pycharm·numpy
fie888914 天前
SSR / MSR 图像增强
人工智能·opencv·计算机视觉
sali-tec14 天前
C# 基于OpenCv的视觉工作流-章85-包胶不良检测
图像处理·人工智能·opencv·算法·计算机视觉