用 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

相关推荐
王莎莎-MinerU20 小时前
面向大模型工作流的文档解析:从OCR到MinerU的深度技术指南
网络·ocr
ai_coder_ai1 天前
使用ocr实现自动化脚本
运维·自动化·ocr
番石榴AI1 天前
JiaJiaOCR-2.2.0:面向Java ocr的开源库
java·ocr
企业知识库布道者1 天前
从 OCR 到文档结构理解:MinerU-Popo 对 RAG 文档解析链路的补全
人工智能·ocr·私有化部署·知识库·rag·企业知识库
DevOpenClub1 天前
用 OCR、PDF 转文本和摘要接口构建 RAG 文档入库 Agent
数据库·pdf·ocr
动能小子ohhh2 天前
DocForge平台的设计与开发--文件上传接口的实现
开发语言·人工智能·python·langchain·ocr·fastapi
_李小白2 天前
【android opencv学习笔记】Day 32:直线检测之霍夫变换
android·opencv·学习
程序员正茂2 天前
EasyAR使用OpenCV下USB摄像头作为自定义相机
opencv·unity·easyar
_李小白2 天前
【android opencv学习笔记】Day 31:提取轮廓之Canny算法
android·opencv·学习