目录
说明
做 OCR 项目时,很多 Windows 客户端都会遇到一个现实问题:
客户电脑里的显卡不一定是 NVIDIA。
有的是 NVIDIA,有的是 AMD,有的是 Intel 核显,也有的是普通办公电脑。如果只做 TensorRT,速度很快,但只能覆盖 NVIDIA GPU;如果只做 CPU,兼容性好,但速度又不够理想。
所以我们做了一个更适合 Windows 通用部署的版本:
lw.OnnxRuntime.PPOCRSharp_dml
它基于 ONNX Runtime + DirectML,目标是让 PP-OCR 在 Windows 环境下可以更方便地调用 GPU 推理,同时兼容更多显卡。
效果

项目特点
- 支持 Windows 多品牌 GPU
DirectML 是微软提供的 GPU 推理后端,可以在 Windows 上调用多种 GPU:
-
NVIDIA
-
AMD
-
Intel 核显
-
Intel 独显
相比 CUDA / TensorRT,它不强绑定 NVIDIA,更适合普通 Windows 客户端软件部署。
- 基于 ONNX Runtime
项目使用 ONNX Runtime 加载 OCR 模型,模型格式采用 ONNX,部署流程清晰:
PaddleOCR 模型 -> ONNX 模型 -> ONNX Runtime DirectML 推理
不需要提前转换 TensorRT engine,也不需要针对不同显卡重新生成模型文件。
- C++ DLL 封装,C# 简单调用
底层推理封装在 C++ DLL 中,C# 界面通过 P/Invoke 调用:
init()
ocr()
destroy()
这样既保留了 C++ 推理性能,也方便 C# 桌面程序、业务系统、WinForms / WPF 项目集成。
- 支持 PP-OCR 多版本模型
测试程序中可以切换多组 OCR 模型,例如:
-
PP-OCRv5 mobile
-
PP-OCRv5 server
-
PP-OCRv6 tiny
-
PP-OCRv6 small
-
PP-OCRv6 medium
可以根据不同场景选择速度优先或精度优先。
- 支持 det / rec / cls 三阶段 OCR
完整支持:
-
文本检测 det
-
文本识别 rec
-
方向分类 cls
实际项目中,如果图片方向固定,也可以关闭 cls,进一步减少耗时。
- 保留 GPU ID 设置
界面保留 use_gpu 和 gpu_id,方便在多 GPU 环境下选择指定显卡。
- 适合通用 Windows 客户端部署
相比 TensorRT,DML 版本不需要生成 engine 文件,也不需要绑定 CUDA、TensorRT 版本,对客户电脑环境更友好。
适用场景
lw.OnnxRuntime.PPOCRSharp_dml 适合:
-
Windows 桌面 OCR 软件
-
C# 项目调用 OCR DLL
-
客户电脑显卡品牌不固定
-
希望 GPU 加速但不想绑定 NVIDIA
-
票据识别、表单识别、截图 OCR
-
工业视觉文字识别
-
普通办公电脑 OCR 部署
与其他方案对比
一句话来说:
-
OpenCV DNN:部署最简单
-
OpenVINO:Intel 平台更强
-
TensorRT:NVIDIA GPU 上最快
-
DirectML:Windows 多品牌 GPU 兼容性最好
DML 版本的定位非常明确:
不是追求单一平台极限速度,而是追求 Windows 客户端 GPU 加速的通用兼容。
部署优势
对于软件开发者来说,DML 版本最大的价值是降低部署门槛。
客户不需要安装 CUDA,不需要安装 TensorRT,也不需要提前转换 engine。只要显卡和系统支持 DirectML,就有机会启用 GPU 推理。
这对面向多个客户、多个硬件环境的软件尤其重要。
性能建议
为了获得更稳定的速度,建议:
-
优先使用较新的显卡驱动
-
Windows 电源模式设置为高性能
-
第一次推理作为预热,不计入测速
-
多 predictor 并发数量不要盲目调大
-
根据显存和图片中文字数量调整 batch
-
如果客户是 NVIDIA 并追求极限速度,可考虑 TensorRT 版本
C#调用源码
using Newtonsoft.Json;
using OpenCvSharp;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
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.OnnxRuntime.PPOCRSharp_dml.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 = -1;
try
{
using (Mat img = Cv2.ImRead(imgPath, ImreadModes.Color))
{
res = ocr2(OCREngine, img.Rows, img.Cols, img.Channels(), img.Data, msgTemp, out strPtr, out ocr_result_len);
}
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);
}
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));
using (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);
}
}
if (checkBox1.Checked)
{
richTextBox1.Text = OCRResultAllInfo.ToString();
}
else
{
richTextBox1.Text = OCRResultInfo.ToString();
}
pictureBox1.Image = null;
pictureBox1.Image = bmp;
}
else
{
MessageBox.Show("识别失败," + msgTemp.ToString());
}
}
finally
{
stopwatch.Stop();
if (strPtr != IntPtr.Zero)
{
Marshal.FreeCoTaskMem(strPtr);
}
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;
chkuse_gpu.Checked = true;
txtgpu_id.Text = "1";
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 chkcls_CheckedChanged(object sender, EventArgs e)
{
if (OCREngine == IntPtr.Zero)
{
return;
}
richTextBox1.Text = "";
Console.WriteLine("cls设置已改变,正在重新初始化模型...");
UnloadModel();
LoadModel();
}
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);
Console.WriteLine("释放成功:" + msgTemp.ToString());
OCREngine = IntPtr.Zero;
}
}
private void btnInit_Click(object sender, EventArgs e)
{
if (OCREngine != IntPtr.Zero)
{
StringBuilder msgTemp = new StringBuilder(128);
destroy(OCREngine, msgTemp);
Console.WriteLine("释放成功:" + msgTemp.ToString());
OCREngine = IntPtr.Zero;
LoadModel();
}
else
{
LoadModel();
}
}
void LoadModel()
{
StringBuilder msgTemp = new StringBuilder(128);
bool use_gpu = true;
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 = "inference/PP-OCRv5_mobile_cls_onnx.onnx";
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;
//赋值
if (chkuse_gpu.Checked == true)
{
use_gpu = true;
}
else
{
use_gpu = false;
}
gpu_id = Convert.ToInt32(txtgpu_id.Text.ToString());
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";
}
else if (rdoserver.Checked)
{
det_model_dir = "inference/PP-OCRv5_server_det_infer.onnx";
rec_model_dir = "inference/PP-OCRv5_server_rec_infer.onnx";
cls_model_dir = "inference/PP-OCRv5_server_cls_infer.onnx";
}
else if (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";
}
else if (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";
}
else
{
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";
}
Console.WriteLine("det model: " + det_model_dir);
if (cls && !File.Exists(Path.Combine(Application.StartupPath, cls_model_dir.Replace('/', Path.DirectorySeparatorChar))))
{
Console.WriteLine("cls model missing, disabled: " + cls_model_dir);
cls = false;
use_angle_cls = false;
}
Console.WriteLine("cls model: " + (cls ? cls_model_dir : "disabled"));
Console.WriteLine("rec model: " + rec_model_dir);
Console.WriteLine("rec dict : " + rec_char_dict_path);
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)
{
Console.WriteLine("模型加载成功!");
}
else
{
string msg = msgTemp.ToString();
Console.WriteLine("模型加载失败," + msg);
}
}
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.OnnxRuntime.PPOCRSharp_dml_Test 链接: https://pan.baidu.com/s/1xSzlQg7Nvnt8KYk1UVq3aQ 提取码: n4yt