项目:lw.PPOCR.C
GitHub:github.com/lxw112190/l...
一个面向 PP-OCR 的轻量级纯 C 推理运行时:开发阶段把 ONNX 转换成自定义 LWM 模型,部署端只保留 C11 Runtime,完成 DET → CLS → REC 全链路 OCR。

做 OCR 推理,今天已经不算什么新鲜事了。
Python、PaddleOCR、ONNX Runtime、OpenVINO、TensorRT......任选一套成熟框架,跑起来并不困难。
真正麻烦的,往往是部署。
尤其当目标环境不是一台配置充足的服务器,而是工业现场的一台 Windows 工控机、一套需要长期维护的桌面软件、一个嵌入式 Linux 设备,甚至还要兼顾 32 位程序时,问题很快就会从:
"模型能不能识别?"
变成:
"为了识别几行字,我到底要带多少东西?"
这也是我做 lw.PPOCR.C 的出发点。
我想尝试一件看起来有点"反潮流"的事情:
不在部署端加载 ONNX,不依赖 ONNX Runtime,不依赖 OpenCV,不依赖 OpenVINO,也不依赖 TensorRT,自己用纯 C 写一个只服务于 PP-OCR 的专用推理 Runtime。
它不追求成为下一个通用 AI 框架。
恰恰相反,它从一开始就主动收缩边界:
只解决 PP-OCR 的部署问题,并尽可能把这件事做小、做稳、做清楚。
于是有了:
lw.PPOCR.C
一、为什么还要自己写一个 OCR Runtime?
先说结论:
如果你的部署环境完全不在意依赖体积,也不需要兼顾旧系统,更不关心运行时内部到底发生了什么,那么成熟框架通常仍然是更省事的选择。
lw.PPOCR.C 并不是为了证明"自己造轮子一定比成熟框架好"。
它解决的是另一类问题。
比如:
- 一个 C/C++ 桌面软件,只想嵌入 OCR,不想再附带一整套推理框架;
- 一个 C# 工业软件,希望通过稳定的 C ABI 直接调用 OCR;
- 一个老项目仍然需要兼顾 32 位程序;
- 一台离线设备需要尽可能简单的部署目录;
- 工业现场更在意可控、可审计、可长期维护,而不是模型框架功能有多丰富;
- 嵌入式 Linux 上,希望将来可以只针对 CPU/NEON 做优化;
- 业务模型非常明确,就是 PP-OCR,并不需要执行任意 ONNX 网络。
在这些场景里,一个通用 Runtime 的"强大",有时候也意味着你必须一起承担它的复杂度。
而 lw.PPOCR.C 选择了另外一条路:
objectivec
PP-OCR ONNX
│
▼
开发期 Converter
│
▼
平台无关 LWM 模型
│
▼
纯 C Runtime
│
├── DET 文字检测
├── CLS 方向分类
└── REC 文字识别
一句话概括:
把复杂工作尽可能放到开发阶段,把部署阶段压缩成一个专用执行器。
这也是整个项目最核心的设计思想。
二、最关键的一步:部署端干脆不解析 ONNX
这是 lw.PPOCR.C 和很多"C/C++ OCR 封装项目"最大的区别。
常见方案大致是:
应用程序
↓
ONNX Runtime / OpenVINO / TensorRT / OpenCV DNN
↓
ONNX 模型
而 lw.PPOCR.C 是:
markdown
开发环境:
PP-OCR.onnx
↓
Model Converter
↓
*.lwm
部署环境:
*.lwm
↓
lw.PPOCR.C
↓
Inference
为什么这么做?
因为 ONNX 首先是一个优秀的模型交换格式,但它不一定是一个"极简专用 Runtime"最合适的直接执行格式。
如果部署端直接解析 ONNX,就意味着 Runtime 还需要面对很多原本可以提前完成的工作,例如:
protobuf
ModelProto / GraphProto
NodeProto / TensorProto
AttributeProto
opset 兼容
Shape 推导
常量处理
图结构解析
算子属性归一化
模型合法性检查
内存规划
如果目标是"支持任意 ONNX",这些复杂度没有办法绕开。
但 lw.PPOCR.C 根本不准备支持任意 ONNX。
它只运行经过验证的 PP-OCR 模型。
那么一个自然的问题就出现了:
为什么不在开发机上把这些事情提前做完?
于是项目引入了自己的模型格式:
LWM ------ LightWeight Model
当前版本是实验性的 LWM v0.1。
Converter 可以使用 Python、ONNX、NumPy、protobuf,因为它只存在于开发环境。
但生成 .lwm 后,部署端就不再需要这些东西。
这种思路有点像一个很小的:
Model Compiler + Runtime
而不是:
Runtime 现场解析一个复杂的模型交换格式。
三、LWM 不是"换个后缀的 ONNX"
LWM 并不是把 ONNX 内容原样塞进另外一个文件。
它有自己明确的二进制布局。
当前 LWM v0.1 使用:
- little-endian 固定宽度整数;
- IEEE-754 FP32;
- 不保存原生指针;
- 不保存
size_t、long等平台相关类型; - Section 使用绝对偏移;
- Section 按 8 字节对齐;
- Tensor 和 Node 使用固定记录结构;
- 模型加载前必须完成边界、结构和 checksum 校验。
核心文件结构大致可以理解为:
css
┌──────────────────────────────┐
│ Header │
├──────────────────────────────┤
│ Graph Input / Output Table │
├──────────────────────────────┤
│ Tensor Table │
├──────────────────────────────┤
│ Node Table │
├──────────────────────────────┤
│ Operator Parameters │
├──────────────────────────────┤
│ String Section │
├──────────────────────────────┤
│ FP32 Weight Blob │
└──────────────────────────────┘
当前格式定义了 21 个 Runtime Operator ID,包括:
css
Conv
Add
Mul
Div
Erf
HardSigmoid
BatchNormalization
ReduceMean
Relu
AveragePool
Squeeze
Transpose
Unsqueeze
MatMul
Softmax
Reshape
Concat
ConvTranspose
MaxPool
Resize
Sigmoid
注意,这并不意味着:
"现在已经支持所有包含这 21 个算子的 ONNX 模型。"
并不是。
项目的原则恰恰相反:
支持的是经过 Converter 和完整测试验证的具体 PP-OCR 图,而不是看到某个算子名字相同就认为任意网络都能运行。
这是一个非常重要的边界。
四、从 REC 开始,最后做到了 DET → CLS → REC
项目最初并没有一上来就做完整 OCR。
第一阶段只做了一件事:
PP-OCRv6 tiny REC。
也就是输入一张已经裁剪好的文本行图片:
scss
text crop
↓
resize / normalize
↓
REC graph
↓
logits
↓
CTC Decode
↓
UTF-8 text
这个阶段真正跑通以后,才继续向外扩展:
css
REC
↓
CLS
↓
DET
↓
Perspective Crop
↓
Full OCR
现在完整流程已经是:
scss
BGR8 Image
│
▼
DET Resize / Normalize
│
▼
DET Graph
│
▼
DB-style Postprocess
│
▼
Reading-order Quadrilaterals
│
▼
Pure-C Perspective Crop
│
├── Tall text → 90° correction
│
▼
Optional CLS
│
├── 0°
│
└── 180°
│
▼
REC
│
▼
CTC Decode
│
▼
UTF-8 Text Lines
也就是说,目前已经不是"纯 C 跑一个识别模型"的实验代码,而是把 OCR 的核心链路串了起来:
检测、方向分类、透视裁剪、文字识别、CTC 解码、坐标恢复、阅读顺序输出。
五、连透视裁剪都没有依赖 OpenCV
这一点我个人很在意。
因为如果所谓"纯 C OCR Runtime"最后在关键路径上还是需要 OpenCV 做:
css
warpPerspective
rotate
resize
那么部署边界其实还是没有真正收干净。
所以 lw.PPOCR.C 的完整 OCR 路径自己实现了透视裁剪与双线性采样。
检测得到四边形后,根据几何关系构造目标区域,再完成:
scss
四点坐标
↓
Homography
↓
Bilinear Sampling
↓
Text Crop
遇到高大于宽很多的竖向文本区域,还会按策略进行 90° 旋转,再进入 CLS/REC。
那怎么确认自己写的裁剪没有悄悄写错?
测试中仍然会使用 OpenCV 作为独立参考实现。
也就是说:
OpenCV 可以出现在开发测试环境中,但不进入正式 Runtime。
测试会将纯 C 透视裁剪结果与 OpenCV warpPerspective / rotate 的结果进行像素误差比较,同时完整 OCR 还要经过真实图片的 Golden Test。
这种"测试可以依赖成熟工具,部署核心不依赖"的边界,是项目一直在坚持的原则。
六、公共接口不是简单地 char* ocr(...)
既然要做 DLL、C#、Win32、Linux 甚至以后 ARM,那么 ABI 设计必须认真。
当前公开头文件只有一个核心入口:
bash
include/lw_infer.h
分别提供:
lw_model
lw_session
lw_detector
lw_classifier
lw_recognizer
lw_ocr
这些对象对调用者都是 opaque handle。
典型调用方式类似:
ini
lw_ocr* ocr = NULL;
lw_error error;
lw_error_init(&error);
lw_status status = lw_ocr_create(
"det.lwm",
"cls.lwm",
"rec.lwm",
"ppocr_keys.txt",
&options,
&ocr,
&error
);
执行完整 OCR:
arduino
lw_ocr_run_bgr_u8(
ocr,
image,
image_bytes,
width,
height,
stride,
lines,
line_capacity,
text_buffer,
text_capacity,
&result,
&error
);
这里有几个看起来不起眼,但对 DLL 工程非常重要的细节。
1. 不跨 ABI 释放内存
输入和输出 Buffer 都由调用方拥有。
Runtime 不会:
c
DLL 内 malloc
EXE 外 free
也不会要求不同编译器、不同 CRT 之间互相释放对象。
2. 公共结构都有 struct_size
例如:
arduino
typedef struct lw_error {
uint32_t struct_size;
int32_t code;
char message[256];
} lw_error;
初始化函数会填写结构大小并清零保留字段。
这样做的目的,是给未来 ABI 演进留下空间。
3. 错误不是只返回一句字符串
API 有稳定的 lw_status:
OK
INVALID_ARGUMENT
IO_ERROR
OUT_OF_MEMORY
INVALID_FORMAT
UNSUPPORTED_VERSION
OUT_OF_BOUNDS
CHECKSUM_MISMATCH
UNSUPPORTED
INVALID_SHAPE
MEMORY_LIMIT
自然语言错误消息主要用于诊断,而不是让业务程序去解析字符串。
七、模型文件也按照"不可信输入"处理
一个推理 Runtime 最容易被忽略的问题之一,就是:
模型文件本身也是输入。
LWM Loader 不会因为文件扩展名叫 .lwm 就直接信任它。
加载过程会检查:
- magic;
- format version;
- header size;
- reserved fields;
- 文件实际大小;
- section offset;
- section size;
- alignment;
- section 是否重叠;
- tensor rank;
- tensor dtype;
- shape 乘法是否溢出;
- weight 范围;
- node input/output index;
- operator ID;
- parameter version;
- checksum;
- workspace 约束。
任何格式错误都应该返回稳定错误码,而不是:
c
assert
abort
exit
当前 checksum 使用的是 FNV-1a 64。
项目文档也明确说明:
它只是损坏检测,不是密码学签名。
如果需要模型真实性校验,仍然应该依靠可信发布渠道和 SHA-256。
这一点我觉得很重要,因为"做一个二进制模型格式"容易,把它当作需要防御性解析的文件格式来设计,才是真正进入工程阶段。
八、现在到底支持哪些模型?
目前项目明确聚焦于:
PP-OCRv6 tiny
包括:
| 模型 | 用途 |
|---|---|
| PP-OCRv6 tiny DET | 文字检测 |
| PP-OCRv6 tiny CLS | 0° / 180° 方向分类 |
| PP-OCRv6 tiny REC | 文字识别 |
当前精度:
FP32
当前设备:
CPU
当前 x86 SIMD:
objectivec
Scalar
SSE2
AVX2
当前 LWM v0.1 转换出的模型文件大约为:
| 模型 | LWM 大小 |
|---|---|
| REC | 4.46 MB |
| CLS | 1.02 MB |
| DET | 1.77 MB |
这些数字只是当前格式和当前转换策略的结果。
LWM v0.1 尚未冻结,后续如果加入权重重排、Fusion、INT8 或新的内存计划,文件 Hash 和大小都可能变化。
九、最有意思的一段:从 570 ms 优化到了 18.865 ms
如果只看功能,自己实现一个 Runtime 其实还不够。
因为一个最直接的问题一定会出现:
"能跑是能跑,但会不会慢得没法用?"
项目里目前记录得最完整的是 REC 单行文字识别 Benchmark。
先把测试条件说清楚:
sql
CPU:AMD Ryzen 7 7735H
OS:Windows 10 x64
Compiler:MSVC 19.40
Build:Release /O2
Precision:FP32
Thread:single-thread
测试图片:295 × 46 文本裁剪图
模型:PP-OCRv6 tiny REC
所以,下面的数据:
不是任意电脑的性能承诺,也不是整张图片的 Full OCR 延迟。
它只是同一测试环境下用来观察 Runtime 优化效果的一组可复现实验数据。
最初纯 Scalar 版本:
| 平台 | REC 平均耗时 |
|---|---|
| Windows x64 | 570.752 ms |
| Windows x86 | 1416.520 ms |
这个速度显然还不够理想。
于是项目没有直接开始"凭感觉上 SIMD",而是先做 Profile。
第一轮优化 Scalar Conv:
yaml
x64: 570.752 → 381.206 ms
x86: 1416.520 → 669.329 ms
接下来发现 Pointwise Conv 占据大量 Conv 时间,于是为 1×1 Conv 单独设计 cache-contiguous path:
makefile
x64: 381.206 → 51.495 ms
x86: 669.329 → 139.297 ms
这一步非常关键。
它说明在专用 Runtime 里,最大的优势有时候并不是"我会不会写 AVX2",而是:
我知道自己只需要跑哪一张图,所以可以围绕真实模型的数据访问模式做优化。
随后又继续优化:
objectivec
3×3 stride-2 Conv
MatMul
SSE2 MatMul
AVX2 MatMul
SIMD Pointwise Conv
Flat Binary SIMD
Broadcast SIMD
Depthwise 3×3 SIMD
Stride-2 3×3 SIMD
最终当前记录的结果是:
| 平台 | 初始 Scalar | 当前结果 | 相对初始 |
|---|---|---|---|
| Windows x64 | 570.752 ms | 18.865 ms | 30.255× |
| Windows x86 | 1416.520 ms | 38.833 ms | 36.477× |
Windows x64 在这组测试中的吞吐达到:
53.010 次 REC / 秒
而且优化过程中有一个要求始终没有放松:
Golden 结果不能因为性能优化而变化。
性能 Benchmark 不只是记录 mean latency,还会记录:
sql
Median
P95
Min / Max
Throughput
Workspace
RSS after warm-up
Final RSS
RSS growth
Peak RSS
Backend
在文档记录的优化测试里,结果文本保持一致,测量阶段 RSS growth 为 0。
这里也必须再次强调:
18.865 ms 是一张 295×46 文字裁剪图的 REC 数据,不是 DET + 多行 REC 的整图 OCR 数据。
完整 OCR 的最终延迟还会受到:
- 原图分辨率;
- DET;
- DB 后处理;
- 文本框数量;
- 每个文本框宽度;
- 是否启用 CLS;
等因素影响。
项目后续也很有必要增加正式的 Full OCR Benchmark。
十、为什么专用 Runtime 可能有性能优化空间?
通用 Runtime 必须考虑:
vbnet
很多模型
很多 Shape
很多 Operator
很多硬件
很多边界条件
而 lw.PPOCR.C 的思路是:
我知道模型是谁
我知道哪些算子真的会出现
我知道热点节点在哪里
我知道哪些 Shape 模式最常见
于是可以针对真实 PP-OCR 图做一些非常明确的 fast path。
例如:
- 1×1 Pointwise Conv;
- 3×3 Depthwise Conv;
- 特定 stride-2 Conv;
- MatMul 行访问;
- flat Add/Mul/Div;
- 单轴 Broadcast;
- SSE2 / AVX2 runtime dispatch。
同时仍然保留:
Scalar fallback
CPU 检测不会简单地因为"编译了 AVX2 文件"就假设运行机器支持 AVX2。
x86/x64 下会检查:
objectivec
CPUID
OSXSAVE
XGETBV
AVX2
如果不满足条件,会自动回落到 SSE2 或 Scalar。
这对于需要在不同年代 CPU 上部署的软件很重要。
十一、完整 OCR 不只返回文字,还保留坐标和各阶段信息
完整接口最终返回按阅读顺序排列的文本行。
每一行包含:
objectivec
四点检测坐标
DET score
REC score
CLS score
CLS label
实际应用的旋转角度
CTC emitted count
UTF-8 text offset
UTF-8 text length
这样调用方不仅可以得到:
"识别出了什么"
还可以得到:
"文字在哪里"
"检测有多可信"
"识别有多可信"
"是否发生方向纠正"
这对很多实际项目非常有用,比如:
- OCR 结果叠加显示;
- 文档结构分析的前置处理;
- 工业字符读取;
- 票据字段定位;
- 证照信息提取;
- 图片搜索;
- OCR 服务 API。
十二、核心库不负责 JPEG / PNG 解码,这是故意的
很多人第一次看到 API 输入是:
BGR8 pixels
可能会问:
"为什么不直接给我一个 jpg 文件路径?"
因为图片解码也是一种依赖策略。
如果核心 Runtime 自己支持:
erlang
JPEG
PNG
BMP
TIFF
GIF
WebP
...
那么你马上又需要选择一套图片库,并让它进入核心部署边界。
lw.PPOCR.C 的做法是:
核心只接受调用方已经解码好的 BGR8。
所以:
css
Camera Frame
OpenCV Mat
System.Drawing Bitmap
stb_image
FFmpeg Frame
浏览器 Canvas
自研图像 SDK
都可以成为它的上游。
这让 Runtime 不必绑定某一种图片生态。
为了证明这件事确实容易集成,仓库里目前已经提供了三类使用方式。
十三、最朴素的 C Demo:不用图片库也能跑
为了证明核心库本身不依赖图片解码组件,原生 C Demo 直接使用非常简单的:
P6 PPM
例如完整 OCR:
r
.\bin\lw-ocr-ppm.exe `
.\models\det.lwm `
.\models\cls.lwm `
.\models\rec.lwm `
.\models\ppocr_keys.txt `
.\models\sample.ppm
它会输出:
- UTF-8 文字;
- 检测框四点坐标;
- DET / CLS / REC 分数;
- 方向分类;
- 应用旋转。
PPM 当然不是为了让用户以后都改用 PPM。
它的意义是:
用一个几乎不需要额外解码库的格式,证明纯 C API 从模型加载到最终 OCR 是完整可运行的。
十四、C# WinForms Demo:直接 P/Invoke
如果你写的是 Windows 工业软件,那么 C# 可能比 C/C++ 更常见。
项目提供了一个:
.NET Framework 3.5
WinForms
Demo。
调用链是:
css
System.Drawing
↓
BGR pixels
↓
P/Invoke
↓
lw_ocr_run_bgr_u8
Demo 可以:
- 打开常见图片;
- 后台执行 OCR;
- 在原图绘制四边形;
- 显示逐行文字;
- 显示各阶段 score;
- 显示旋转信息;
- 输出完整 JSON。
这里选择 .NET Framework 3.5 也不是因为"越老越好"。
它主要是为了验证:
公共 C ABI 可以被较老的 Windows/.NET 应用以非常传统、稳定的 P/Invoke 方式调用。
但需要注意:
这不等价于已经完成 Windows 7 实机认证。
当前 Win7 x86 仍然是兼容目标,正式声明之前还需要实体机验证。
十五、还有一个原生 HTTP / Web Demo
另一种很直观的体验方式,是直接启动:
lw.PPOCR.C.HttpServer
默认监听:
arduino
http://127.0.0.1:8787/
然后用浏览器选择图片。
浏览器通过 Canvas 完成常见图片解码,再转成 P6 PPM 上传。
服务端:
javascript
HTTP Request
↓
PPM validation
↓
RGB → BGR
↓
lw_ocr_run_bgr_u8
↓
JSON
页面会把 OCR 文字和四边形框重新绘制出来。
接口提供:
bash
GET /health
以及:
bash
POST /api/ocr
支持二进制 P6 PPM,也支持 JSON + Base64 PPM。
需要特别说明:
这个 HTTP Server 是测试和本地集成 Demo。
它默认没有:
TLS
Authentication
Rate Limit
CORS Policy
生产级 Access Log
所以项目文档明确建议:
如果真的暴露到公网,应放在带 HTTPS、认证、限流和请求大小控制的反向代理后面。
一个 Demo 如果愿意把"不适合直接上公网"写清楚,我反而更愿意相信它的工程边界。
十六、现在的跨平台状态到底怎么样?
目前项目的主目标平台包括:
Windows x64
Linux x64
兼容目标包括:
Windows 7 SP1 x86
后续计划:
Linux ARM64
最近 GitHub Actions 已经完成:
yaml
Windows Server 2022 + Visual Studio 2022 x64
Ubuntu 22.04 + GCC/Ninja
两套环境的完整 CI。
CI 不只是编译 Runtime,而是继续执行:
objectivec
Converter dependencies
Model conversion
Runtime build
Model analysis regeneration
CTest
CPack
Generated-report consistency check
Artifact upload
最终分别生成:
go
Windows x64 development package
Linux x64 development package
这意味着 Linux x64 已经不是"代码理论上应该能编译"的状态,而是实际进入远程 CI 构建、测试和打包链路。
不过仍然需要区分:
CI 验证 ≠ 所有机器实机验证
例如:
arduino
Windows Server 2022 CI success
并不能自动推导出:
所有 Windows 10 / 11 机器都已经验证
更不能推导出:
Windows 7 x86 已经验证
项目当前仍然把:
源码目标
CI 验证
实体机验证
作为不同层级。
这种表述可能没有一句"全平台支持"那么吸睛,但更接近真实的软件工程。
十七、32 位为什么还值得保留?
2026 年再讨论 x86 32 位,听起来似乎有点"考古"。
但工业软件和普通消费软件的生命周期并不一样。
现实中仍然有一些设备:
- Windows 7 32 位;
- 老工控机;
- 已经冻结开发环境的行业软件;
- 无法随意升级 OS 的现场系统;
- 第三方 SDK 只能提供 Win32 DLL 的旧项目。
因此 lw.PPOCR.C 从设计阶段就尽量避免:
arduino
把 64 位指针写进模型
默认 size_t 永远是 64 位
默认地址空间无限
公共 ABI 暴露平台原生结构
Windows x86 已经进行过本机构建、ABI、Runtime 和 Demo 测试。
但 Windows 7 SP1 x86 仍然只是兼容目标。
后面真正值得做的是:
用最终发布包在实体 Windows 7 SP1 32 位机器上完成完整 OCR、DLL、C Demo、C# Demo 和依赖检查。
如果这一步最终跑通,它会成为这个项目一个非常有辨识度的特点。
十八、它适合什么场景?
我认为 lw.PPOCR.C 比较适合下面几类用户。
1. 桌面软件嵌入 OCR
如果你的程序本来就是:
bash
C
C++
C#
只需要一个稳定 C API 完成 OCR,不想把业务代码绑定到某个大型推理框架,专用 Runtime 会比较舒服。
2. 工业现场和离线设备
这类项目往往更重视:
少依赖
可复制部署
稳定 ABI
长期维护
明确资源上限
而不是需要支持几十种 AI 网络。
3. 嵌入式 Linux
目前 Linux x64 已经进入 CI。
Linux ARM64 / NEON 仍在规划中。
如果后续完成 ARM64 + NEON,这套"同一种 LWM + 不同 CPU Kernel Backend"的架构会更有价值。
4. 学习一个小型神经网络 Runtime 是怎么工作的
如果你一直想知道:
模型文件怎么设计?
ONNX 怎么转换?
Shape 怎么传播?
Tensor 生命周期怎么算?
Workspace 怎么规划?
Graph Executor 怎么调 Kernel?
Conv 为什么慢?
SIMD 到底应该优化哪里?
C ABI 怎么设计?
这个项目也可以作为一个相对聚焦的研究对象。
因为它没有试图把所有 AI 问题都塞进来。
模型范围小,反而更容易沿着一条完整链路看到:
ONNX → Converter → Model Format → Loader → Shape → Memory → Executor → Kernel → OCR Pipeline → API
十九、它不适合什么场景?
这一部分也必须说清楚。
如果你的目标是:
任意 ONNX
YOLO
LLM
语音模型
多模型混跑
GPU 高吞吐
CUDA
TensorRT
大 Batch
自动图优化
训练
那么 lw.PPOCR.C 不是正确工具。
当前它明确不是:
通用 ONNX Runtime。
也不是:
Paddle Inference 的替代品。
更不是:
TensorRT 的 CPU 版竞争对手。
它现在的范围就是:
objectivec
PP-OCRv6 tiny
FP32
CPU
DET / CLS / REC
这样的边界虽然让项目"看起来没那么万能",却也是它能够保持 Runtime 简单的前提。
二十、当前仍然存在的限制
项目现在已经可以跑完整 OCR,但距离"成熟稳定版"还有不少工作。
目前比较明确的边界包括:
LWM v0.1 还没有冻结
也就是说:
文件格式
模型 Hash
ABI
在 1.0 之前仍可能调整。
当前主要是 FP32 CPU
还没有:
INT8
FP16
GPU
NPU
ARM64 / NEON 还没有完成
Linux ARM64 是明确的后续方向,但不能提前写成"已经支持"。
Windows 7 还需要实体机验证
设计目标和真正兼容验证是两回事。
一个 OCR Handle 不设计为并发调用
单个 Handle 应由调用方串行使用。
如果应用需要并发,可以创建多个 Handle,由上层做 Worker 调度。
还需要更大的真实图片回归集
现有测试已经包含真实图片 Golden、REC corpus、图输出对比和完整 OCR 检查。
但更多字体、语言、拍摄角度、低对比度、模糊、复杂背景等真实场景仍然需要继续扩充。
二十一、我觉得下一步最值得做什么?
现在项目已经跨过了"证明可行"的阶段。
后续我反而不建议马上扩成通用 AI Runtime。
更值得做的是把现有范围继续做硬。
我个人比较关注这些方向:
markdown
1. Full OCR Benchmark
2. 更大的 Golden Corpus
3. ASan / UBSan
4. Windows x86 持续构建
5. Windows 7 SP1 x86 实机验证
6. Linux ARM64
7. ARM NEON
8. INT8
尤其是:
Full OCR Benchmark
现在 REC Benchmark 已经可以清楚地看到每一次优化带来的变化。
下一步应该把整图 OCR 拆开统计:
objectivec
DET
DB Postprocess
Crop
CLS
REC
Total
这样使用者才可以真正回答:
"我的一张 1280×720 图片、有 N 行文字时,大概需要多少时间?"
而不是只知道单行 REC 的速度。
二十二、为什么我仍然想把这个项目开源出来?
因为这个项目真正有意思的地方,可能不只是"多了一个 OCR 库"。
它其实是在尝试回答一个更底层的问题:
当模型范围足够明确时,我们到底需不需要把一个通用 AI Runtime 一起带到每一台终端设备上?
我的答案不是:
"永远不需要。"
而是:
有些场景里,确实可以有另外一种选择。
把复杂度放到开发阶段:
ONNX
↓
Converter
↓
LWM
把部署端控制在:
Loader
Shape / Memory
Executor
CPU Kernels
PP-OCR Pipeline
C ABI
然后只为自己真正需要的网络做测试和优化。
这条路肯定没有成熟通用 Runtime 省开发时间。
但它换回来的是:
更明确的运行边界
更少的大型部署依赖
更容易审计的执行路径
更直接的 C ABI
更自由的旧系统 / 嵌入式适配空间
对一部分软件来说,这些东西可能比"支持任意 ONNX"更重要。
二十三、如果你想试一下
项目地址:
GitHub:
当前仓库已经包含:
objectivec
纯 C Runtime
ONNX → LWM Converter
DET
CLS
REC
Full OCR
Scalar / SSE2 / AVX2
C API
C Command-line Demo
C# WinForms Demo
Native HTTP/Web Demo
Tests
Benchmark
CPack Development Package
CI
如果你对下面任何一个方向感兴趣:
- PP-OCR;
- C/C++;
- C# P/Invoke;
- 小型 AI Runtime;
- ONNX 模型转换;
- 自定义模型格式;
- SIMD;
- Windows 32 位兼容;
- 嵌入式 Linux;
- 工业 OCR;
都欢迎看看代码、跑跑 Demo、提 Issue,或者一起讨论。
结语
这几年 AI Runtime 越来越强。
一个成熟框架可以替我们解决模型解析、图优化、硬件调度、算子实现、量化、GPU 等大量复杂问题。
所以自己写 Runtime,并不是一件"显然更先进"的事情。
但有时候,工程也可以反过来问:
如果我只需要 PP-OCR,我到底最少需要什么?
lw.PPOCR.C 目前给出的答案是:
开发端:
ONNX + Converter
部署端:
LWM + Pure C Runtime
从最初一个速度并不理想的 Scalar REC,到现在 DET / CLS / REC 全链路跑通;从单个 Kernel,到 Graph Executor、LWM Loader、Memory Planner、C ABI、SIMD、C# 和 HTTP Demo;再到 Windows / Linux CI 真正跑起来,这个项目现在已经不只是一个想法。
它仍然年轻。
LWM 还没有冻结,ARM64 还没有完成,Win7 还需要实体机验证,INT8 和 Full OCR Benchmark 也还在后面。
但至少有一件事情已经得到了验证:
为一个明确的 OCR 模型族,做一个小而专用的纯 C 推理 Runtime,是可以走通的。
接下来要做的,不是把它变得"什么都能跑"。
而是继续把它做得:
更稳、更快、更容易部署。
如果你也喜欢这种"小而硬"的工程路线,欢迎来 GitHub 看看: