← Back
sdcb

sdcb/SimdPaddleOCR

纯 C# PP-OCRv6 推理库:多平台手写 Kernel / GEMM 等算子、超高性能、低内存需求、超高准确率。 自带托管 ONNX 解释器,不依赖 Paddle Inference、ONNX Runtime 或 OpenCV 原生库。

View on GitHub ↗
Stars
383
Forks
54
Watchers
383
Open issues
0
Contributors
5
Language
C#
License
Apache License 2.0
Default branch
main
Created Sep 5, 2026Updated Sep 30, 2026

Star growth

Today—
This week—
This month—

Star history will appear here once this repo has been tracked for a couple of days.

README

Sdcb.SimdPaddleOCR test NuGet License: Apache-2.0 QQ

中文 | English

纯 C# PP-OCRv6 推理库:多平台手写 Kernel / GEMM 等算子、超高性能、低内存需求、超高准确率。 自带托管 ONNX 解释器,不依赖 Paddle Inference、ONNX Runtime 或 OpenCV 原生库。

核心 API 接收交错像素内存(默认 BGR24,也可直接传 RGB24 / BGRA32 / RGBA32),不负责图片解码,因此不会强制引入 ImageSharp、SkiaSharp 或 OpenCvSharp。

快速开始

安装核心包和 tiny 模型(tiny 会传递引用 CLS 与 ModelProvider):

dotnet add package Sdcb.SimdPaddleOCR
dotnet add package Sdcb.SimdPaddleOCR.Models.ChineseV6Tiny
dotnet add package SixLabors.ImageSharp --version 3.1.11

模型从程序集嵌入资源直接加载,不会解压或写入临时文件。stride = 0 表示紧密排列(width * 每像素字节数)。默认 ImagePixelFormat.Bgr24;其它布局在 resize / crop 里就地 swizzle,不会先转成一张中间 BGR 图。

ImageSharp 3(推荐)

using System.Runtime.InteropServices;
using Sdcb.SimdPaddleOCR;
using Sdcb.SimdPaddleOCR.Models.ChineseV6Tiny;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.PixelFormats;

using PaddleOcrAll ocr = await PaddleOcrAll.LoadAsync(ChineseV6TinyModels.Default);
using Image<Rgba32> image = await Image.LoadAsync<Rgba32>(CreateDecoderOptions(), "sample.jpg");
Rgba32[]? packedCopy = null;
if (!image.DangerousTryGetSinglePixelMemory(out Memory<Rgba32> memory))
{
    packedCopy = new Rgba32[checked(image.Width * image.Height)];
    image.CopyPixelDataTo(packedCopy);
    memory = packedCopy;
}

PaddleOcrResult result = ocr.Run(MemoryMarshal.AsBytes(memory.Span), image.Width, image.Height,
    format: ImagePixelFormat.Rgba32);
Console.WriteLine(result.Text);

// 必须 Clone Default:直接改 Configuration.Default 会丢掉 PNG/JPEG 解码器。
// 默认分配器按 4MB 分块,大图会让 DangerousTryGetSinglePixelMemory 失败。
static DecoderOptions CreateDecoderOptions()
{
    Configuration configuration = Configuration.Default.Clone();
    configuration.PreferContiguousImageBuffers = true;
    return new DecoderOptions { Configuration = configuration };
}

ImageSharp 默认分配器会把像素拆成 4MB 块,大图上 DangerousTryGetSinglePixelMemory 会失败。用独立 Configuration 打开 PreferContiguousImageBuffers 尽量零拷贝;仍不连续再 CopyPixelDataTo。不要改 Configuration.Default。

后续三个示例由调用方把 lock / 原生指针包成 ReadOnlySpan<byte> 再交给 Run,加载方式与上面相同。整段 Run 期间不要 Unlock / Dispose 源图。

SkiaSharp

using SkiaSharp;

SKBitmap bitmap = SKBitmap.Decode("sample.jpg")
    ?? throw new InvalidDataException("无法读取图片");
if (bitmap.ColorType != SKColorType.Bgra8888)
    bitmap = bitmap.Copy(SKColorType.Bgra8888)
        ?? throw new InvalidDataException("无法转换到 BGRA");
int stride = bitmap.RowBytes;
unsafe
{
    PaddleOcrResult result = ocr.Run(new ReadOnlySpan<byte>((byte*)bitmap.GetPixels(), stride * bitmap.Height),
        bitmap.Width, bitmap.Height, stride, ImagePixelFormat.Bgra32);
}

OpenCvSharp5

using OpenCvSharp;

using Mat image = Cv2.ImRead("sample.jpg", ImreadModes.Color);
if (image.Empty()) throw new InvalidDataException("无法读取图片");
int stride = (int)image.Step();
unsafe
{
    PaddleOcrResult result = ocr.Run(new ReadOnlySpan<byte>((byte*)image.Data, stride * image.Height),
        image.Width, image.Height, stride, ImagePixelFormat.Bgr24);
}

Bitmap

using System.Drawing;
using System.Drawing.Imaging;

using Bitmap bitmap = new("sample.jpg");
Rectangle rectangle = new(0, 0, bitmap.Width, bitmap.Height);
BitmapData data = bitmap.LockBits(rectangle, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
try
{
    unsafe
    {
        PaddleOcrResult result = ocr.Run(new ReadOnlySpan<byte>((byte*)data.Scan0, data.Stride * bitmap.Height),
            bitmap.Width, bitmap.Height, data.Stride, ImagePixelFormat.Bgra32);
    }
}
finally
{
    bitmap.UnlockBits(data);
}

NuGet 包

NuGet 包 版本 说明
Sdcb.SimdPaddleOCR NuGet 纯托管推理核心(net10.0;netstandard2.0)
Sdcb.SimdPaddleOCR.ModelProvider NuGet 模型契约(IPaddleOcrModelProvider / PaddleOcrModelBundle),通常被传递引用
Sdcb.SimdPaddleOCR.Models.ChineseV6Tiny NuGet PP-OCRv6 tiny DET+REC+字典;ChineseV6TinyModels.Default 含 CLS
Sdcb.SimdPaddleOCR.Models.ChineseV6Small NuGet PP-OCRv6 small;ChineseV6SmallModels.Default
Sdcb.SimdPaddleOCR.Models.ChineseV6Medium NuGet PP-OCRv6 medium;ChineseV6MediumModels.Default
Sdcb.SimdPaddleOCR.Models.TextLineOrientation NuGet PP-LCNet 文本行方向 CLS,被三个中文模型包传递引用

每个 IPaddleOcrModelProvider 提供 Name、Kind、Format、语言和版本元数据以及 OpenRead() / OpenReadAsync()。完整 OCR 组合由 PaddleOcrModelBundle 表达(DET、REC、字典和可选 CLS)。当前语言代码为 zh。单个模型也可被其他推理实现消费,例如 ChineseV6TinyModel.Detection.OpenReadAsync()。Model、PaddleOcrDetector、PaddleOcrClassifier、PaddleOcrRecognizer 和 PaddleOcrAll 均提供 Stream 加载入口;解析完成后不会继续保留完整的 ONNX 原始字节。

使用本地模型

核心不下载模型。使用本地 DET、CLS、REC 和字典文件时:

using Sdcb.SimdPaddleOCR;

using PaddleOcrAll ocr = await PaddleOcrAll.LoadAsync(
    detectionPath: "models/det.onnx",
    classificationPath: "models/cls.onnx",
    recognitionPath: "models/rec.onnx",
    dictionaryPath: "models/ppocr_keys.txt");

PaddleOcrOptions 里两套并行不要混用:DetIntraOpThreads 是检测图内的卷积线程 (一份 session,默认最多 8);LineWorkerCount 是一行一组的 CLS/REC worker 路数 (每路一个 session,上限,实际 min(请求, ProcessorCount);0 为 min(ProcessorCount, 4))。检测阈值、边界长度、方向分类、 动态识别宽度和 Session 缓存上限等也在同一组 options 里。

示例

四个示例共用 examples/sample.jpg,图片由示例负责解码并转换为 BGR:

  • examples/ImageSharp.AspNetCore:ASP.NET Core + ImageSharp 3,可上传体验与 POST /api/ocr JSON API。
  • examples/SkiaSharp.Avalonia:Avalonia 桌面示例,SkiaSharp 解码。
  • examples/OpenCvSharp5.Wpf:WPF 示例,OpenCvSharp5 解码。
  • examples/SystemDrawing.WinForms:.NET 10 Windows / .NET Framework 4.8 双目标 WinForms 示例,使用 Bitmap/LockBits;运行 net48 前请安装 .NET Framework 4.8 Developer Pack,项目平台选择 x64。
dotnet run --project examples/ImageSharp.AspNetCore
dotnet run --project examples/OpenCvSharp5.Wpf -- path/to/image.jpg
dotnet run --project examples/SkiaSharp.Avalonia -- path/to/image.jpg
dotnet run --project examples/SystemDrawing.WinForms --framework net10.0-windows

Web 示例打开站点即可上传;API 为 POST /api/ocr(multipart/form-data 字段 file、model),文档在 /scalar。

Sdcb.SimdPaddleOCR 还带 4 个 LINQPad 脚本(imagesharp / skiasharp / opencvsharp5 / bitmap):下载示例图,tiny 模型识别,控制台输出文字。bitmap 同时能在 LINQPad 5 / .NET Framework 4.8 和 .NET 10 上跑。包打了 linqpad-samples 标签,免费版也能用。

常见问题

为什么调试时 OCR 识别特别慢?

调试器可能会在模块加载时取消 JIT 优化,使 OCR 的计算密集型代码无法获得应有的运行时优化,从而导致识别明显变慢。

请关闭该选项,然后重新启动调试会话:

  • Visual Studio:工具 > 选项 > 调试 > 常规,取消勾选在模块加载时取消 JIT 优化。
  • Rider:构建、执行、部署 > 调试器 > JIT,取消勾选在加载模块时禁用 JIT 优化。
  • VS Code:打开设置 (JSON),添加 "csharp.debug.suppressJITOptimizations": false。

Native AOT 发布后为什么比 JIT 慢很多?

x64 发布 Native AOT 时,可执行项目里必须设置:

<IlcInstructionSet>avx2</IlcInstructionSet>

不设的话,ILC 按 SSE2 / 128-bit Vector<T> 基线编译,Avx2.IsSupported 会被折成 false,AVX2 内核整段裁掉,推理会慢一截。没有 AVX2 的 CPU 不要设这项。ARM64 的 AOT 基线已带 NEON / AdvSimd,一般不用写 IlcInstructionSet。

ImageSharp 报「图片像素不是连续内存」或 DangerousTryGetSinglePixelMemory 失败?

ImageSharp 默认分配器会把像素拆成 4MB 块,大图上拿不到一整块连续缓冲。按上面「ImageSharp 3」示例:Clone 一份 Configuration 后打开 PreferContiguousImageBuffers,仍不连续再 CopyPixelDataTo。不要改 Configuration.Default,否则 PNG/JPEG 解码器会丢。完整写法见 examples/ImageSharp.AspNetCore/Program.cs。

支持范围

说明
目标框架 核心 net10.0;netstandard2.0;ModelProvider 与全部模型包为 netstandard2.0
推荐运行时 .NET 10:完整 x86 SIMD 与 NativeAOT(IsAotCompatible)
兼容运行时 netstandard2.0 可在 .NET Framework 4.8 等环境使用;编译时去掉 AVX / AVX-512 / VNNI 源,走 System.Numerics.Vector / 标量
CI 架构 Windows x64 / x86 / ARM64,Linux x64 / ARM64,macOS x64 / ARM64
SIMD .NET 10 运行时探测 AVX → AVX2 → AVX-512 / VNNI;无对应指令集或 ARM 时用 Vector/标量
输入 交错像素内存(默认 BGR24,也可 RGB24 / BGRA32 / RGBA32);无图片路径、文件或图片库 API
设备 默认 CPU;net10.0 另有可选 GPU 后端(OcrBackend):Vulkan 直连系统加载器(Windows vulkan-1.dll、Linux libvulkan.so.1、Android libvulkan.so),Metal 走 macOS 系统框架;netstandard2.0 只有 CPU
安卓 目前通过开发用宿主 test/Sdcb.SimdPaddleOCR.AndroidBench(net10.0-android,引用 net10.0 库,adb 驱动)在骁龙 8 Gen 3 上跑 CPU 与 Vulkan,见 docs/vulkan-8gen3.md;桌面的 Vulkan 路由和 shader 没有变
NativeAOT 裁剪发布时请保留核心程序集和所用模型程序集

许可证与第三方组件

本仓库中由本项目编写的源代码和文档采用 Apache License 2.0 发布。 Apache-2.0 提供明确的专利授权条款,更适合公开发布的库和 NuGet 包。

模型资源和第三方代码不因本项目许可证而被重新授权:

  • PP-OCRv6 DET/REC、TextLineOrientation CLS 及字典来自 PaddleOCR 生态,来源资料标记为 Apache-2.0;发布模型包时请保留来源和许可证说明。
  • 示例依赖遵循各自上游许可证;特别是 ImageSharp 3.x 使用 Six Labors Split License, 不是普通 MIT 许可证。

完整的第三方归属和分发说明见 THIRD-PARTY-NOTICES.md。PaddleOCR、PP-OCR 及相关名称归其 各自权利人所有,本项目不代表官方,也不构成官方背书。

性能

1.4.2 相对 1.3.0:图级 NHWC 从仅 AVX2 扩到 ns2 / x64 scalar / net10 AdvSIMD,预处理直接写 NHWC。 内存占用大幅下降:tiny-4w 工作集峰值大约少 300 MB(win-x64 817→515 MB,linux-arm64 840→572 MB),Δ WS 从约 400 MB 降到约 100–160 MB。 CI tiny 满勤 767/1032、CER 2.36%(cls 1020/1020;1.3 跳过首张是 757/1022、3.53%,尺子不同,不能当涨幅)。本机行精确仍是 742 / 950 / 1004;CER 2.78% → 2.37%、0.60% → 0.41%、0.67% → 0.14%(左 1:4 CLS,倒长行先转正再进 REC)。

GitHub-hosted runner、PP-OCRv6 tiny、去掉首张 warmup 后的中位墙钟:

路径 1.3 1.4.2 相对 工作集峰值 Δ WS
linux-arm64 N2 tiny-4w(net10 AdvSIMD) 241 180 0.75× 840 → 572 MB 418 → 162 MB
linux-arm64 tiny-4w-ns2 374 295 0.79×
linux-arm64 tiny-4w-scalar 984 856 0.87×
win-x64 7763 tiny-4w(AVX2) 167 ~184 持平(噪声) 817 → 515 MB 398 → 107 MB
win-x64 7763 tiny-4w-ns2 343 228 0.66×
win-x64 7763 tiny-4w-noavx 481 380 0.79×
win-x64 7763 tiny-4w-scalar 1368 1220 0.89×

本机 Ryzen 7 5800X、4 worker、仓库 dataset/ 100 张(n=99)本库 mean(1.3 NuGet → 1.4.2):tiny 86.0 → 63.1 ms(0.73×),small 222 → 200 ms(0.90×),medium 628 → 585 ms(0.93×)。同机 ns2:tiny 203 → 96.5 ms(0.48×),small 432 → 303 ms(0.70×),medium 1606 → 874 ms(0.54×)。 同机 C 引擎与各 ISA 比值见 docs/perf.md。

性能复现

GitHub Actions test 工作流 会跑单元测试,并在 Windows / Linux / macOS 多架构上对 tiny / small / medium 做 bench (含关闭 AVX-512 / AVX2 / AVX / 全部硬件加速,以及 netstandard2.0 库)。汇总报告写入 job summary 与 perf-report artifact。

微信群

如果微信群二维码过期了,请加入 QQ 群 C#/.NET计算机视觉技术交流 579060605。