目录
1. 引言
2. 优势
3. 效果
4. OCR 模型选择与准备
5. WinForm 界面实现
6. 下载
1. 引言
本方案旨在利用 DirectML 的硬件加速能力,在 Windows 系统上部署和运行一个轻量级、高精度的 OCR 模型。
2. 优势
其核心优势在于:
- 跨硬件兼容:DirectML 支持集成显卡、独立显卡(NVIDIA/AMD/Intel)以及 CPU,确保在不同 Windows 设备上都能获得加速。
- 低延迟推理:通过 DirectX 12 底层接口直接与硬件通信,减少中间层开销,实现端到端的高效推理。
- 易于集成:提供 C++ 和 WinRT API,可方便地集成到桌面应用、UWP 应用或服务中。
- 模型格式支持:支持 ONNX 模型格式,便于使用主流深度学习框架(如 PyTorch, TensorFlow)训练并导出的模型。
3. 效果
4. OCR 模型选择与准备
- PaddleOCR 轻量级模型:百度开源的 OCR 工具包,提供了优秀的轻量级中英文识别模型。
5. WinForm 界面实现
为了将 OCR 推理功能集成到 Windows 桌面应用中,我们可以使用 WinForm 创建一个简单的图形界面。以下是一个基本的 WinForm 窗体实现,包含图像选择、OCR 识别和结果显示功能。
namespace OnnxRuntimeForm { public partial class Form1 : Form { private IntPtr engine = IntPtr.Zero; private Bitmap currentImage = null; private string imgPath = null; private List<OcrResult> ltOCRResult = new List<OcrResult>(); private StringBuilder OCRResultInfo = new StringBuilder(); private StringBuilder OCRResultAllInfo = new StringBuilder(); private Pen pen = new Pen(Brushes.Red, 2f); private const string BaseModelDir = @"inference"; public class OcrResult { public List<List<double>> box { get; set; } public double score { get; set; } public string text { get; set; } } [DllImport("OnnxRuntimeDll.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)] static extern int init(out IntPtr engine, [MarshalAs(UnmanagedType.U1)] 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.U1)] bool use_dilation, [MarshalAs(UnmanagedType.U1)] bool cls, [MarshalAs(UnmanagedType.U1)] 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("OnnxRuntimeDll.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)] static extern int ocr(IntPtr engine, int rows, int cols, int channels, IntPtr data, StringBuilder msg, out IntPtr ocr_result, out int ocr_result_len); [DllImport("OnnxRuntimeDll.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)] static extern int destroy(IntPtr engine, StringBuilder msg); public Form1() { InitializeComponent(); } private void Form1_Load(object sender, EventArgs e) { rdoV6Tiny.Checked = true; chkGPU.Checked = false; chkCls.Checked = false; UpdateModelPaths(); txtUnclipRatio.Text = "1.8"; LoadModel(); } private void rdoModel_CheckedChanged(object sender, EventArgs e) { if (((RadioButton)sender).Checked) { UpdateModelPaths(); } } private void UpdateModelPaths() { string det = ""; string rec = ""; string dict = ""; bool v6 = false; if (rdoV5Mobile.Checked) { det = Path.Combine(BaseModelDir, "PP-OCRv5_mobile_det_onnx.onnx"); rec = Path.Combine(BaseModelDir, "PP-OCRv5_mobile_rec_onnx.onnx"); dict = Path.Combine(BaseModelDir, "ppocrv5_dict.txt"); } else if (rdoV5Server.Checked) { det = Path.Combine(BaseModelDir, "PP-OCRv5_server_det_infer.onnx"); rec = Path.Combine(BaseModelDir, "PP-OCRv5_server_rec_infer.onnx"); dict = Path.Combine(BaseModelDir, "ppocrv5_dict.txt"); } else if (rdoV6Tiny.Checked) { det = Path.Combine(BaseModelDir, "PP-OCRv6_tiny_det.onnx"); rec = Path.Combine(BaseModelDir, "PP-OCRv6_tiny_rec.onnx"); dict = Path.Combine(BaseModelDir, "PP-OCRv6_tiny_rec_dict.txt"); v6 = true; } else if (rdoV6Small.Checked) { det = Path.Combine(BaseModelDir, "PP-OCRv6_small_det.onnx"); rec = Path.Combine(BaseModelDir, "PP-OCRv6_small_rec.onnx"); dict = Path.Combine(BaseModelDir, "PP-OCRv6_small_rec_dict.txt"); v6 = true; } else if (rdoV6Medium.Checked) { det = Path.Combine(BaseModelDir, "PP-OCRv6_medium_det.onnx"); rec = Path.Combine(BaseModelDir, "PP-OCRv6_medium_rec.onnx"); dict = Path.Combine(BaseModelDir, "PP-OCRv6_medium_rec_dict.txt"); v6 = true; } txtDetModel.Text = det; txtRecModel.Text = rec; txtDict.Text = dict; if (v6) { chkCls.Checked = false; chkCls.Enabled = false; } else { chkCls.Enabled = true; } } private void btnInit_Click(object sender, EventArgs e) { richTextBox1.Clear(); if (engine != IntPtr.Zero) { UnloadModel(); } LoadModel(); } private void btnSelect_Click(object sender, EventArgs e) { using (OpenFileDialog ofd = new OpenFileDialog()) { ofd.Filter = "图片|*.bmp;*.jpg;*.jpeg;*.tiff;*.tif;*.png;*.gif|所有文件|*.*"; if (ofd.ShowDialog() == DialogResult.OK) { imgPath = ofd.FileName; currentImage?.Dispose(); currentImage = new Bitmap(imgPath); pictureBox1.Image = currentImage; richTextBoxResult.Clear(); } } } private void btnOCR_Click(object sender, EventArgs e) { if (engine == IntPtr.Zero) { MessageBox.Show("请先初始化模型!"); return; } if (imgPath == null || currentImage == null) { MessageBox.Show("请先选择图片!"); return; } btnSelect.Enabled = false; btnOCR.Enabled = false; richTextBoxResult.Clear(); OCRResultInfo.Clear(); OCRResultAllInfo.Clear(); var msgTemp = new StringBuilder(1024); var stopwatch = System.Diagnostics.Stopwatch.StartNew(); 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(engine, 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 = ParseOCRResults(ocr_result); ltOCRResult.Sort((a, b) => { double ax1 = a.box[0][0], ay1 = a.box[0][1], ax2 = a.box[0][0], ay2 = a.box[0][1]; double bx1 = b.box[0][0], by1 = b.box[0][1], bx2 = b.box[0][0], by2 = b.box[0][1]; foreach (var p in a.box) { ax1 = Math.Min(ax1, p[0]); ay1 = Math.Min(ay1, p[1]); ax2 = Math.Max(ax2, p[0]); ay2 = Math.Max(ay2, p[1]); } foreach (var p in b.box) { bx1 = Math.Min(bx1, p[0]); by1 = Math.Min(by1, p[1]); bx2 = Math.Max(bx2, p[0]); by2 = Math.Max(by2, p[1]); } double y_overlap = Math.Max(0, Math.Min(ay2, by2) - Math.Max(ay1, by1)); double h = Math.Min(ay2 - ay1, by2 - by1); if (h > 0 && y_overlap / h > 0.5) return ax1.CompareTo(bx1); return ay1.CompareTo(by1); }); OCRResultAllInfo.Append(FormatJson(ocr_result)); if (ltOCRResult.Count == 0) { OCRResultInfo.AppendLine("[未识别到文字]"); OCRResultAllInfo.AppendLine("[未识别到文字]"); } using (Graphics graphics = Graphics.FromImage(currentImage)) { foreach (OcrResult item in ltOCRResult) { OCRResultInfo.AppendLine(item.text); if (item.box != null && item.box.Count >= 4) { Point[] points = item.box.Select(p => new Point((int)p[0], (int)p[1])).ToArray(); graphics.DrawPolygon(pen, points); } } } pictureBox1.Image = null; pictureBox1.Image = currentImage; UpdateRichText(); } else { if (strPtr != IntPtr.Zero) Marshal.FreeCoTaskMem(strPtr); MessageBox.Show("识别失败:" + msgTemp.ToString()); } btnSelect.Enabled = true; btnOCR.Enabled = true; } private void chkAllInfo_CheckedChanged(object sender, EventArgs e) { UpdateRichText(); } private void UpdateRichText() { richTextBoxResult.Text = chkAllInfo.Checked ? OCRResultAllInfo.ToString() : OCRResultInfo.ToString(); } private double ParseDouble(TextBox tb, double defaultValue) { if (double.TryParse(tb.Text.Trim(), out double v)) return v; return defaultValue; } private int ParseInt(TextBox tb, int defaultValue) { if (int.TryParse(tb.Text.Trim(), out int v)) return v; return defaultValue; } private void LoadModel() { var msgTemp = new StringBuilder(1024); bool use_gpu = chkGPU.Checked; int gpu_id = 0; string det_model_dir = txtDetModel.Text.Trim(); int limit_side_len = ParseInt(txtLimitSideLen, 960); double det_db_thresh = ParseDouble(txtDbThresh, 0.3); double det_db_box_thresh = ParseDouble(txtBoxThresh, 0.6); double det_db_unclip_ratio = ParseDouble(txtUnclipRatio, 1.6); bool use_dilation = true; bool use_angle_cls = chkCls.Checked && chkCls.Enabled; bool cls = use_angle_cls; string cls_model_dir = use_angle_cls ? Path.Combine(BaseModelDir, "PP-OCRv5_mobile_cls_onnx.onnx") : ""; double cls_thresh = 0.9; double cls_batch_num = 1; string rec_model_dir = txtRecModel.Text.Trim(); string rec_char_dict_path = txtDict.Text.Trim(); int rec_batch_num = ParseInt(txtRecBatchNum, 8); int rec_img_h = 48; int rec_img_w = 960; int predictor_num = 4; AppendStatus("正在初始化模型..."); AppendStatus("det: " + det_model_dir); AppendStatus("rec: " + rec_model_dir); AppendStatus("dict: " + rec_char_dict_path); AppendStatus("device: " + (use_gpu ? "GPU" : "CPU")); AppendStatus($"db_thresh={det_db_thresh}, box_thresh={det_db_box_thresh}, unclip={det_db_unclip_ratio}"); int res = init(out engine, 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 { AppendStatus("模型加载失败: " + msgTemp.ToString()); MessageBox.Show("模型加载失败:" + msgTemp.ToString()); engine = IntPtr.Zero; } } private void UnloadModel() { if (engine != IntPtr.Zero) { var msgTemp = new StringBuilder(1024); destroy(engine, msgTemp); AppendStatus("释放成功: " + msgTemp.ToString()); engine = IntPtr.Zero; } } 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 List<OcrResult> ParseOCRResults(string json) { try { var serializer = new JavaScriptSerializer(); return serializer.Deserialize<List<OcrResult>>(json) ?? new List<OcrResult>(); } catch (Exception ex) { MessageBox.Show("JSON 解析失败:" + ex.Message); return new List<OcrResult>(); } } private string FormatJson(string json) { try { var serializer = new JavaScriptSerializer(); var obj = serializer.Deserialize<object>(json); return serializer.Serialize(obj); } catch { return json; } } private void Form1_FormClosing(object sender, FormClosingEventArgs e) { UnloadModel(); currentImage?.Dispose(); pen?.Dispose(); } } }6. 下载
通过网盘分享的文件:OnnxRuntime
链接: https://pan.baidu.com/s/1OnmaLUQs-TIQBWzk83j4Jg 提取码: fx8n