不挑显卡也能 GPU 加速:基于 DirectML 的 Windows OCR 推理方案
2026/7/21 17:37:34 网站建设 项目流程

目录

说明

效果

项目特点

适用场景

与其他方案对比

部署优势

性能建议

C#调用源码

下载


说明

做 OCR 项目时,很多 Windows 客户端都会遇到一个现实问题:

客户电脑里的显卡不一定是 NVIDIA。

有的是 NVIDIA,有的是 AMD,有的是 Intel 核显,也有的是普通办公电脑。如果只做 TensorRT,速度很快,但只能覆盖 NVIDIA GPU;如果只做 CPU,兼容性好,但速度又不够理想。

所以我们做了一个更适合 Windows 通用部署的版本:

lw.OnnxRuntime.PPOCRSharp_dml

它基于ONNX Runtime + DirectML,目标是让 PP-OCR 在 Windows 环境下可以更方便地调用 GPU 推理,同时兼容更多显卡。

效果

项目特点

  1. 支持 Windows 多品牌 GPU

DirectML 是微软提供的 GPU 推理后端,可以在 Windows 上调用多种 GPU:

  • NVIDIA

  • AMD

  • Intel 核显

  • Intel 独显

相比 CUDA / TensorRT,它不强绑定 NVIDIA,更适合普通 Windows 客户端软件部署。

  1. 基于 ONNX Runtime

项目使用 ONNX Runtime 加载 OCR 模型,模型格式采用 ONNX,部署流程清晰:

PaddleOCR 模型 -> ONNX 模型 -> ONNX Runtime DirectML 推理

不需要提前转换 TensorRT engine,也不需要针对不同显卡重新生成模型文件。

  1. C++ DLL 封装,C# 简单调用

底层推理封装在 C++ DLL 中,C# 界面通过 P/Invoke 调用:

init() ocr() destroy()

这样既保留了 C++ 推理性能,也方便 C# 桌面程序、业务系统、WinForms / WPF 项目集成。

  1. 支持 PP-OCR 多版本模型

测试程序中可以切换多组 OCR 模型,例如:

  • PP-OCRv5 mobile

  • PP-OCRv5 server

  • PP-OCRv6 tiny

  • PP-OCRv6 small

  • PP-OCRv6 medium

可以根据不同场景选择速度优先或精度优先。

  1. 支持 det / rec / cls 三阶段 OCR

完整支持:

  • 文本检测 det

  • 文本识别 rec

  • 方向分类 cls

实际项目中,如果图片方向固定,也可以关闭 cls,进一步减少耗时。

  1. 保留 GPU ID 设置

界面保留use_gpugpu_id,方便在多 GPU 环境下选择指定显卡。

  1. 适合通用 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"; } elseif (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"; } 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"; } 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"; } 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

引入地址

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询