Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .codegraph/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# CodeGraph data files
# These are local to each machine and should not be committed

# Database
*.db
*.db-wal
*.db-shm

# Cache
cache/

# Logs
*.log

# Hook markers
.dirty
Binary file added RapidOCRConsole/Assets/1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
130 changes: 97 additions & 33 deletions RapidOCRConsole/Program.cs
Original file line number Diff line number Diff line change
@@ -1,46 +1,110 @@

using Emgu.CV;
using RapidOCRLib;
using System.Diagnostics;
using System.Drawing.Imaging;
using System.Threading.Tasks;

var path = Path.Combine(AppContext.BaseDirectory, "models");
//Initialize model.
OcrLite ocrEngin = new OcrLite()
var winformModels = Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "RapidOCRWinform", "models");
winformModels = Path.GetFullPath(winformModels);
var detModel = Path.Combine(winformModels, "PP-OCRv6_det_medium.onnx");
var clsModel = Path.Combine(winformModels, "ch_ppocr_mobile_v2.0_cls_infer.onnx");
var recModel = Path.Combine(winformModels, "PP-OCRv6_rec_medium.onnx");
var dictFile = Path.Combine(winformModels, "ppocrv6_dict.txt");
var imgFile = Path.Combine(AppContext.BaseDirectory, "Assets", "demo.png");

const int padding = 50;
const float boxScoreThresh = 0.5f;
const float boxThresh = 0.3f;
const float unClipRatio = 1.6f;
const bool mostAngle = false;

TextWriter cout = Console.Out;
void Log(string msg) { cout.WriteLine(msg); cout.Flush(); }

int optimalThr = (int)(Environment.ProcessorCount * 0.7);
var configs = new (int ThreadNum, int MaxSideLen, bool DoAngle, bool UseGpu, string Label)[]
{
DetPath = Path.Combine(path, "ch_PP-OCRv5_mobile_det.onnx"),
ClsPath = Path.Combine(path, "ch_ppocr_mobile_v2.0_cls_infer.onnx"),
RecPath = Path.Combine(path, "ch_PP-OCRv5_rec_mobile_infer.onnx"),
KeyDicPath = Path.Combine(path, "ppocrv5_dict.txt"),
(ThreadNum: optimalThr, MaxSideLen: 1024, DoAngle: true, UseGpu: false, Label: $"A[CPU]: thr={optimalThr} | max=1024 | doAngle=T (Baseline)"),
(ThreadNum: optimalThr, MaxSideLen: 800, DoAngle: false, UseGpu: false, Label: $"D[CPU]: thr={optimalThr} | max=800 | doAngle=F (最优CPU配置)"),
(ThreadNum: optimalThr, MaxSideLen: 800, DoAngle: false, UseGpu: true, Label: $"G[GPU]: max=800 | doAngle=F (与D同参数,GPU加速对比)"),
};
const int warmupRuns = 1;
const int testRuns = 2;

Log($"Process64={Environment.Is64BitProcess}, ProcessorCount={Environment.ProcessorCount}, OS={Environment.OSVersion}");
Log("Ready.");

await ocrEngin.InitModels();
//get the image will be ocr.
var demoWillOCRFile = Path.Combine(AppContext.BaseDirectory, "Assets", "demo.png");
//ocr detect
var result = ocrEngin.Detect(demoWillOCRFile, 50);
if (result != null)
var results = new List<(string Label, double AvgTotal, double Db, double Ang, double Rec, int Blocks, string Engine)>();
foreach (var cfg in configs)
{
Console.WriteLine(result.ToString());
var img = result.BoxImg.ToBitmap();
var desFile = Path.Combine(AppContext.BaseDirectory!, "Assets", Path.GetFileNameWithoutExtension(Path.GetRandomFileName()) + ".png")!;
#pragma warning disable CA1416
img?.Save(desFile!);
#pragma warning restore CA1416
if (File.Exists(desFile))
Log("");
Log("=========================================================================");
Log($"CONFIG {cfg.Label}");
Log("=========================================================================");
var ocr = new OcrLite
{
ProcessStartInfo psi = new ProcessStartInfo
{
FileName = desFile,
UseShellExecute = true
};
DetPath = detModel,
ClsPath = clsModel,
RecPath = recModel,
KeyDicPath = dictFile,
ThreadNum = cfg.ThreadNum,
};
await ocr.InitModels(ocr.DetPath, ocr.ClsPath, ocr.RecPath, ocr.KeyDicPath, cfg.ThreadNum, cfg.UseGpu, gpuDeviceId: 0);
// 初始化后立即取一次引擎状态(Detect 会把 EngineProvider 写入结果)
Log($"InitModels done. UseGpu={cfg.UseGpu}, Begin warmup ({warmupRuns}).");

Process.Start(psi);
// 预热
for (int w = 0; w < warmupRuns; w++)
{
var r0 = ocr.Detect(imgFile, padding, cfg.MaxSideLen, boxScoreThresh, boxThresh, unClipRatio, cfg.DoAngle, mostAngle);
if (r0 != null) Log($" Warmup DetectTime={r0.DetectTime:F0}ms (Det={r0.DbNetTime:F0}ms Ang={r0.AngleNetTime:F0}ms Rec={r0.CrnnNetTime:F0}ms)");
}
Console.WriteLine("显示strResult");
Console.WriteLine(result.StrRes);
Log("Warmup done. Begin tests.");

List<double> totals = new(), dbs = new(), angs = new(), recs = new();
int blocks = 0;
string engineLine = "unknown";
for (int i = 0; i < testRuns; i++)
{
var r = ocr.Detect(imgFile, padding, cfg.MaxSideLen, boxScoreThresh, boxThresh, unClipRatio, cfg.DoAngle, mostAngle);
if (r == null) continue;
engineLine = r.EngineProvider ?? "n/a";
Log($" Run {i + 1}/{testRuns}: total={r.DetectTime:F0}ms Det={r.DbNetTime:F0}ms Ang={r.AngleNetTime:F0}ms Rec={r.CrnnNetTime:F0}ms Blocks={r.TextBlocks.Count} [{engineLine}]");
totals.Add(r.DetectTime);
dbs.Add(r.DbNetTime);
angs.Add(r.AngleNetTime);
recs.Add(r.CrnnNetTime);
blocks = r.TextBlocks.Count;
}
results.Add((cfg.Label, totals.Average(), dbs.Average(), angs.Average(), recs.Average(), blocks, engineLine));
}

Log("");
Log("#########################################################################");
Log(" SUMMARY REPORT (demo.png, PP-OCRv6_medium, CPU vs GPU)");
Log("#########################################################################");
Log(string.Format("{0,-58} | {1,8} | {2,8} | {3,8} | {4,8} | {5,7} | {6,8} | {7}",
"Config", "Total(ms)", "Det(ms)", "Ang(ms)", "Rec(ms)", "Blocks", "SpeedUp", "Engine"));
Log("-------------------------------------------------------------------------");
double baselineTotal = results[0].AvgTotal;
foreach (var r in results)
{
double speedup = baselineTotal > 0 ? baselineTotal / r.AvgTotal : double.NaN;
Log(string.Format("{0,-58} | {1,8:F0} | {2,8:F0} | {3,8:F0} | {4,8:F0} | {5,7} | x{6:F2} | {7}",
r.Label, r.AvgTotal, r.Db, r.Ang, r.Rec, r.Blocks, speedup, r.Engine));
}
Log("#########################################################################");

Console.ReadKey();
// 如果 GPU 方案仍显示为 CPU,给用户排查清单
var gpuResult = results.FirstOrDefault(x => x.Label.StartsWith("G["));
if (!string.IsNullOrEmpty(gpuResult.Engine) && !gpuResult.Engine.Contains("DML("))
{
Log("");
Log("! GPU 方案没有真正启用 DirectML,原因已在 Engine 列打印。按下面步骤排查:");
Log(" 1) 确认 Windows 版本 ≥ 10.0.1903(Win+R → winver)");
Log(" 2) 更新显卡驱动到最新版(NVIDIA/AMD/Intel 官网都有)");
Log(" 3) 本项目已 PlatformTarget=x64,确认编译输出为 x64 而非 32 位");
Log(" 4) 确认 RapidOCRLib.csproj 引用了 Microsoft.ML.OnnxRuntime.DirectML 1.24.4");
}
else if (!string.IsNullOrEmpty(gpuResult.Engine) && gpuResult.Engine.Contains("DML("))
{
Log("");
Log($">> DirectML GPU 启用成功!与最优 CPU 方案 D 对比:{baselineTotal / results[1].AvgTotal:F2}x(CPU内部) → {baselineTotal / gpuResult.AvgTotal:F2}x(含GPU)");
}
5 changes: 4 additions & 1 deletion RapidOCRConsole/RapidOCRConsole.csproj
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<!-- CUDA / cuDNN 原生库仅提供 x64 版本 -->
<PlatformTarget>x64</PlatformTarget>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>

<ItemGroup>
Expand Down
19 changes: 10 additions & 9 deletions RapidOCRLib/AngleNet.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Emgu.CV;
using Emgu.CV;
using Emgu.CV.CvEnum;
using Emgu.CV.Structure;
using Microsoft.ML.OnnxRuntime;
Expand All @@ -22,28 +22,29 @@ class AngleNet
private InferenceSession angleNet;
private List<string> inputNames;

public string ProviderInfo { get; private set; } = "NotInitialized";

public AngleNet() { }

~AngleNet()
{
angleNet.Dispose();
angleNet?.Dispose();
}

public async Task InitModel(string path, int numThread)
public async Task InitModel(string path, int numThread, bool useGpu = false, int gpuDeviceId = 0)
{
try
{
SessionOptions op = new SessionOptions();
op.GraphOptimizationLevel = GraphOptimizationLevel.ORT_ENABLE_EXTENDED;
op.InterOpNumThreads = numThread;
op.IntraOpNumThreads = numThread;
SessionOptions op = OcrUtils.CreateSessionOptions(OcrUtils.NetKind.Classification, numThread, useGpu, gpuDeviceId, out var providerInfo);
ProviderInfo = providerInfo;
Console.WriteLine($"[AngleNet] Loading model with provider: {providerInfo}");
angleNet = new InferenceSession(path, op);
inputNames = angleNet.InputMetadata.Keys.ToList();

// 从 ONNX 模型 metadata 读取输入尺寸,兼容 v2 (192x48) 和 v5 (160x80)
var dims = angleNet.InputMetadata.First().Value.Dimensions;
_dstHeight = dims[2];
_dstWidth = dims[3];
_dstHeight = dims.Length > 2 && dims[2] > 0 ? dims[2] : 48;
_dstWidth = dims.Length > 3 && dims[3] > 0 ? dims[3] : 192;

await Task.CompletedTask;
}
Expand Down
48 changes: 39 additions & 9 deletions RapidOCRLib/CrnnNet.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Emgu.CV;
using Emgu.CV;
using Microsoft.ML.OnnxRuntime;
using Microsoft.ML.OnnxRuntime.Tensors;
using RapidOCRLib.Models;
Expand All @@ -23,24 +23,25 @@ class CrnnNet
private List<string> keys;
private List<string> inputNames;

public string ProviderInfo { get; private set; } = "NotInitialized";

public CrnnNet() { }

~CrnnNet()
{
crnnNet?.Dispose();
}

public async Task InitModel(string path, string keysPath, int numThread)
public async Task InitModel(string path, string keysPath, int numThread, bool useGpu = false, int gpuDeviceId = 0)
{
try
{
SessionOptions op = new SessionOptions();
op.GraphOptimizationLevel = GraphOptimizationLevel.ORT_ENABLE_EXTENDED;
op.InterOpNumThreads = numThread;
op.IntraOpNumThreads = numThread;
SessionOptions op = OcrUtils.CreateSessionOptions(OcrUtils.NetKind.Recognition, numThread, useGpu, gpuDeviceId, out var providerInfo);
ProviderInfo = providerInfo;
Console.WriteLine($"[CrnnNet] Loading model with provider: {providerInfo}");
crnnNet = new InferenceSession(path, op);
inputNames = crnnNet.InputMetadata.Keys.ToList();
keys = InitKeys(keysPath);
keys = InitKeys(crnnNet, keysPath);
await Task.CompletedTask;
}
catch (Exception ex)
Expand All @@ -49,7 +50,36 @@ public async Task InitModel(string path, string keysPath, int numThread)
throw;
}
}
private List<string> InitKeys(string path)

private List<string> InitKeys(InferenceSession session, string keysPath)
{
// PP-OCRv5/v6 rec models embed the character dictionary in ONNX metadata.
if (session.ModelMetadata.CustomMetadataMap.TryGetValue("character", out string characters))
{
List<string> keys = new List<string>();
keys.Add("#");
string[] lines = characters.Split('\n');
int count = lines.Length;
if (count > 0 && lines[count - 1].Length == 0)
{
count--;
}
for (int i = 0; i < count; i++)
{
keys.Add(lines[i]);
}
keys.Add(" ");
Console.WriteLine($"keys Size = {keys.Count}");
return keys;
}
if (string.IsNullOrWhiteSpace(keysPath))
{
throw new Exception("The rec model does not embed a character dictionary and no key dictory file was provided.");
}
return InitKeysFromFile(keysPath);
}

private List<string> InitKeysFromFile(string path)
{
StreamReader sr = new StreamReader(path, Encoding.UTF8);
List<string> keys = new List<string>();
Expand Down Expand Up @@ -149,4 +179,4 @@ private TextLine ScoreToTextLine(float[] srcData, int h, int w)
}

}
}
}
15 changes: 8 additions & 7 deletions RapidOCRLib/DbNet.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Clipper2Lib;
using Clipper2Lib;
using Emgu.CV;
using Emgu.CV.CvEnum;
using Emgu.CV.Structure;
Expand All @@ -23,21 +23,22 @@ class DbNet

private List<string> inputNames;

public string ProviderInfo { get; private set; } = "NotInitialized";

public DbNet() { }

~DbNet()
{
dbNet.Dispose();
dbNet?.Dispose();
}

public async Task InitModel(string path, int numThread)
public async Task InitModel(string path, int numThread, bool useGpu = false, int gpuDeviceId = 0)
{
try
{
SessionOptions op = new SessionOptions();
op.GraphOptimizationLevel = GraphOptimizationLevel.ORT_ENABLE_EXTENDED;
op.InterOpNumThreads = numThread;
op.IntraOpNumThreads = numThread;
SessionOptions op = OcrUtils.CreateSessionOptions(OcrUtils.NetKind.Detection, numThread, useGpu, gpuDeviceId, out var providerInfo);
ProviderInfo = providerInfo;
Console.WriteLine($"[DbNet] Loading model with provider: {providerInfo}");
dbNet = new InferenceSession(path, op);
inputNames = dbNet.InputMetadata.Keys.ToList();
await Task.CompletedTask;
Expand Down
12 changes: 9 additions & 3 deletions RapidOCRLib/Models/ModeOptions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using System;
using System.Collections.Generic;
using System.Text;
using System;

namespace RapidOCRLib.Models
{
Expand All @@ -26,5 +24,13 @@ public class ModeOptions
/// cpu thread number,default is 70% of toal logic cpu core numbers.
/// </summary>
public int ThreadNum { get; set; } = (int)(Environment.ProcessorCount * 0.7);
/// <summary>
/// Whether to use GPU acceleration via DirectML (Windows built-in DX12 GPU acceleration).
/// </summary>
public bool UseGpu { get; set; } = false;
/// <summary>
/// GPU device ID to use when UseGpu is enabled. Default is 0.
/// </summary>
public int GpuDeviceId { get; set; } = 0;
}
}
14 changes: 12 additions & 2 deletions RapidOCRLib/Models/OcrResult.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Emgu.CV;
using Emgu.CV;
using System.Collections.Generic;
using System.Drawing;
using System.Text;
Expand Down Expand Up @@ -70,16 +70,26 @@ public sealed class OcrResult
{
public List<TextBlock> TextBlocks { get; set; }
public float DbNetTime { get; set; }
public float AngleNetTime { get; set; }
public float CrnnNetTime { get; set; }
public float PreprocessTime { get; set; }
public float PostprocessTime { get; set; }
public Mat BoxImg { get; set; }
public float DetectTime { get; set; }
public string StrRes { get; set; }
/// <summary>当前推理使用的后端引擎信息,如 "DML(device 0)" / "CPU"。</summary>
public string EngineProvider { get; set; } = "Unknown";

public override string ToString()
{
StringBuilder sb = new StringBuilder();
sb.AppendLine("OcrResult");
sb.AppendLine($"OcrResult [Engine: {EngineProvider}]");
TextBlocks.ForEach(x => sb.Append(x));
sb.AppendLine($"├─PreprocessTime({PreprocessTime}ms)");
sb.AppendLine($"├─DbNetTime({DbNetTime}ms)");
sb.AppendLine($"├─AngleNetTime({AngleNetTime}ms)");
sb.AppendLine($"├─CrnnNetTime({CrnnNetTime}ms)");
sb.AppendLine($"├─PostprocessTime({PostprocessTime}ms)");
sb.AppendLine($"├─DetectTime({DetectTime}ms)");
sb.AppendLine($"└─StrRes({StrRes})");
return sb.ToString();
Expand Down
Loading