目录
[OCR 识别再提速:基于 OpenVINO 的 PPOCRSharp 来了](#OCR 识别再提速:基于 OpenVINO 的 PPOCRSharp 来了)
[OpenVINO 推理,速度提升明显](#OpenVINO 推理,速度提升明显)
[支持 PP-OCRv5 / PP-OCRv6 模型](#支持 PP-OCRv5 / PP-OCRv6 模型)
[C# 调用更方便](# 调用更方便)
[本地 OCR,更适合这些场景](#本地 OCR,更适合这些场景)
[为什么选择 OpenVINO 版本?](#为什么选择 OpenVINO 版本?)
效果

OCR 识别再提速:基于 OpenVINO 的 PPOCRSharp 来了
做 OCR 项目,很多人第一反应是:模型效果重要,速度也同样重要。
尤其是在发票识别、证件识别、截图文字提取、批量图片处理这些场景里,识别速度慢一点,用户体验就会差很多。之前我们已经做过基于 OpenCV DNN 的 OCR 推理版本,整体可用性不错,但在性能上还有继续优化的空间。
这一次,基于 OpenVINO 的 lw.OpenVINO.PPOCRSharp 版本来了。
OpenVINO 推理,速度提升明显
OpenVINO 是 Intel 推出的深度学习推理优化工具套件,针对 CPU 推理做了大量优化,包括算子融合、图优化、线程调度、指令集加速等。
在实际测试中,同样的 OCR 模型,使用 OpenVINO 推理后,速度相比 OpenCV DNN 有明显提升。
对于本地部署 OCR 来说,这一点非常关键:
-
不依赖云服务
-
不需要上传图片
-
保护数据隐私
-
响应速度更快
-
更适合桌面软件、本地工具、内网系统集成
支持 PP-OCRv5 / PP-OCRv6 模型
本项目基于 PaddleOCR 系列模型封装,支持常见的文本检测和文字识别流程。
目前测试程序中已集成多种模型选择:
-
PP-OCRv5 mobile
-
PP-OCRv5 server
-
PP-OCRv6 tiny
-
PP-OCRv6 small
-
PP-OCRv6 medium
可以根据实际业务场景选择不同模型。
如果追求速度,可以选择轻量模型;如果追求识别效果,可以选择更大的模型。
C# 调用更方便
lw.OpenVINO.PPOCRSharp 提供 DLL 接口,C# 程序可以直接调用。
测试程序中已经封装好:
-
模型初始化
-
图片选择
-
OCR 识别
-
JSON 结果返回
-
识别框绘制
-
耗时统计
对于 C# 开发者来说,可以更方便地集成到 WinForm、WPF、桌面工具、业务系统中。
不用重新写 C++ 推理逻辑,也不用研究复杂的模型部署细节,直接调用 DLL 即可。
本地 OCR,更适合这些场景
这个版本尤其适合:
-
身份证、驾驶证、营业执照识别
-
发票、单据、表格图片识别
-
批量图片文字提取
-
截图 OCR 工具
-
内网系统 OCR 集成
-
桌面端 OCR 软件开发
-
不能上传图片到云端的私有化项目
如果你的项目对隐私、安全、离线部署有要求,本地 OCR 会是更合适的方案。
为什么选择 OpenVINO 版本?
相比 OpenCV DNN 版本,OpenVINO 的优势主要体现在推理性能上。
简单来说:
OpenCV DNN 更通用,部署简单;
OpenVINO 更偏向高性能推理,尤其适合 CPU 本地部署。
在 OCR 这种需要频繁调用 det 和 rec 模型的任务中,推理速度提升会直接反映到整体识别耗时上。
实际体验就是:同样一张图,OpenVINO 版本更快。
项目特点
-
基于 C++ 封装 OCR 推理
-
支持 C# 调用
-
支持 OpenVINO 后端
-
支持 PP-OCRv5 / PP-OCRv6 模型
-
支持本地离线识别
-
返回 JSON 识别结果
-
测试程序可直接运行
-
适合二次开发和项目集成
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 OCRTest
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
const string DllName = "lw.OpenVINO.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;
}
elseif (rdov6medium.Checked)
{
det_model_dir = "inference/PP-OCRv6_medium_det.onnx";
rec_model_dir = "inference/PP-OCRv6_medium_rec.onnx";
rec_char_dict_path = "inference/PP-OCRv6_medium_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");
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.OpenVINO.PPOCRSharp.Test 链接: https://pan.baidu.com/s/1UzhCDIRDMV-Ykwpe__X4og 提取码: efya