简介:这是一份面向开发者与算法工程师的离线文字识别依赖库资源,围绕RapidOcr与Onnxruntime组合方案,解决在无网络环境下进行高效OCR识别的需求,适用于文档处理、车牌识别、屏幕截图转文本等实时场景。压缩包共991个文件,约192.61MB,包含134个hpp头文件、134个json配置、106个txt说明、81个o目标文件、76个cmake构建脚本、52个so动态库、48个bin可执行文件及10个onnx模型文件等,覆盖C++源码、构建产物与模型资源,便于直接集成或二次编译。目前已有1493人学习下载。资源内含Onnxruntime运行时依赖、OpenCV图像处理静态库及RapidOcr模型文件,读者可据此完成模型加载、图像预处理、推理执行与结果后处理全流程,并参考目录结构快速定位所需依赖,减少环境配置与排错成本。
1. 离线文字识别为什么值得用 RapidOcr-Onnxruntime 重做一遍
很多做桌面端、边缘盒子或内网工具的开发者都遇到过同一个尴尬:OCR 功能在联网环境跑得好好的,一到客户现场断网、涉密内网或者低配工控机上就集体翻车。要么是云端接口调不通,要么是本地部署的模型依赖一大堆 Python 环境、CUDA 版本对不上,装完还得祈祷别缺 DLL。RapidOcr-Onnxruntime 这个组合之所以被反复提起,就是因为它把「离线」和「依赖库」这两件事同时压到了一个很轻的层面:用 ONNX Runtime 做推理后端,把检测、方向分类、识别三个模型串起来,不依赖网络、不依赖完整训练框架,一个可执行文件加几个模型文件就能跑。它适合谁?做桌面软件需要内嵌 OCR 的、做工业质检要在无网环境识别字符的、做文档扫描工具不想把图片传到别人服务器上的,都属于它的目标人群。这一章先把「它到底解决什么问题」讲清楚,后面再拆依赖、拆参数、拆踩坑。
2. 把 RapidOcr 和 Onnxruntime 的依赖关系拆开看
2.1 三个模型各干什么,为什么不能只留一个
RapidOcr 的离线识别不是单个模型端到端出结果,而是经典的三段式流水线:检测模型负责找出图中文字区域,方向分类模型判断每个区域是否旋转了 180 度,识别模型再把区域里的字符序列解出来。很多人第一次用的时候会想「能不能只留识别模型」,答案是不行,因为识别模型只认已经裁剪好的单行文字图,你直接把整张截图丢进去,它会把整行当成一个超长字符序列去解,结果基本不可用。
检测模型常见的是 DBNet 系列结构,输出的是文字区域的多边形或矩形框;方向分类模型是一个小分类网络,输入是裁剪后的文字条,输出 0 或 180 两个类别;识别模型通常是 CRNN 加 CTC 解码,输出字符序列和置信度。这三个模型在 RapidOcr 的发布包里一般以.onnx文件形式存在,配合一个keys.txt字符集文件。依赖库层面,你真正需要的是 OnnxRuntime 的运行时库,而不是完整的 PyTorch 或 PaddlePaddle。
提示:如果你只做固定版式的票据识别,且文字方向永远水平,可以跳过方向分类模型,但检测和识别两个模型不能省。
2.2 Onnxruntime 选 CPU 还是 GPU,先看你的部署环境
OnnxRuntime 提供 CPU、CUDA、DirectML、TensorRT 等多种执行提供器(Execution Provider)。选型逻辑其实很直接:目标机器有 NVIDIA 显卡且驱动可控,就上 CUDA EP;是 Windows 桌面且显卡杂,可以用 DirectML;是纯 CPU 工控机或信创环境,就老老实实 CPU EP。不要一上来就追求 GPU,因为 OCR 单张图的推理量并不大,CPU 上优化好线程数往往已经够用,而 GPU 版本会额外引入 CUDA 运行时和 cuDNN 依赖,部署包体积和兼容风险都会上升。
| 执行提供器 | 适用场景 | 额外依赖 | 部署复杂度 |
|---|---|---|---|
| CPU | 工控机、内网服务器、信创 | 无 | 低 |
| CUDA | 有 N 卡且驱动固定 | CUDA、cuDNN | 中 |
| DirectML | Windows 桌面多显卡 | DirectML 运行时 | 中 |
| TensorRT | 固定型号 N 卡、追求极限 | TensorRT | 高 |
我一般会先在 CPU 上把整条链路跑通,记录单图耗时,再决定要不要换 GPU。因为很多项目瓶颈其实在图片预处理和前后处理,不在矩阵计算本身。
2.3 最小依赖清单和目录结构
一个能跑的最小目录通常长这样:可执行文件或主程序、models目录放三个 onnx 模型和字符集、OnnxRuntime 的动态库。Windows 下是onnxruntime.dll,Linux 下是libonnxruntime.so。如果你用 C# 或 Java 封装,还需要对应的绑定库。下面是一个典型的目录布局,用 bash 表示:
rapidocr_demo/ ├── models/ │ ├── det.onnx # 检测模型 │ ├── cls.onnx # 方向分类模型 │ ├── rec.onnx # 识别模型 │ └── keys.txt # 字符集,每行一个字符 ├── onnxruntime.dll # Windows 运行时库 └── rapidocr_demo.exe # 主程序逻辑说明:models目录名可以改,但代码里加载路径要同步改;keys.txt的行顺序必须和识别模型训练时的字符集顺序完全一致,否则解码出来的字会整体错位。参数上,检测模型输入尺寸一般是 960 或 640 的倍数,识别模型输入高度固定 32 或 48,宽度动态。这些在加载模型时不用手动设,但你要知道它们决定了预处理怎么缩放。
3. 用 Onnxruntime 跑通检测、分类、识别的最小步骤
3.1 图片预处理:缩放、归一化、通道顺序
OnnxRuntime 本身不做图像处理,你得自己把图片转成模型要的 NCHW 浮点张量。检测模型一般要求输入归一化到 0 到 1 或减均值除方差,识别模型要求灰度或 RGB 三通道。下面是一段 Python 预处理示例:
import cv2 import numpy as np def preprocess_det(image, input_size=960): # 保持长宽比缩放,短边对齐到 input_size h, w = image.shape[:2] scale = input_size / max(h, w) new_h, new_w = int(h * scale), int(w * scale) resized = cv2.resize(image, (new_w, new_h)) # BGR 转 RGB,归一化到 0-1,转 NCHW rgb = cv2.cvtColor(resized, cv2.COLOR_BGR2RGB).astype(np.float32) / 255.0 mean = np.array([0.485, 0.456, 0.406], dtype=np.float32) std = np.array([0.229, 0.224, 0.225], dtype=np.float32) rgb = (rgb - mean) / std tensor = rgb.transpose(2, 0, 1)[np.newaxis, ...] return tensor, scale def preprocess_rec(crop, rec_height=48): # 识别输入:高度固定,宽度按比例缩放 h, w = crop.shape[:2] scale = rec_height / h new_w = max(1, int(w * scale)) resized = cv2.resize(crop, (new_w, rec_height)) rgb = cv2.cvtColor(resized, cv2.COLOR_BGR2RGB).astype(np.float32) / 255.0 rgb = (rgb - 0.5) / 0.5 tensor = rgb.transpose(2, 0, 1)[np.newaxis, ...] return tensor逻辑说明:检测预处理里的scale要保留,因为模型输出的框坐标是在缩放后图上的,映射回原图需要除以这个比例。识别预处理不做保持长宽比的 padding,而是直接拉伸到固定高度,宽度动态,这是因为识别模型对字符宽度变化有一定鲁棒性。参数上,input_size越大检测小字越准但越慢,rec_height必须和模型训练时一致,常见是 32 或 48,设错会直接导致识别乱码。
3.2 检测模型推理与文本框后处理
检测模型输出通常是概率图或阈值图,需要做二值化、找轮廓、再拟合最小外接矩形。下面用 OnnxRuntime 跑检测并做简单后处理:
import onnxruntime as ort import cv2 import numpy as np session_det = ort.InferenceSession("models/det.onnx", providers=["CPUExecutionProvider"]) def detect(image): tensor, scale = preprocess_det(image) input_name = session_det.get_inputs()[0].name prob_map = session_det.run(None, {input_name: tensor})[0] # prob_map 形状 [1,1,H,W],取第一通道 prob = prob_map[0, 0] binary = (prob > 0.3).astype(np.uint8) * 255 contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) boxes = [] for cnt in contours: if cv2.contourArea(cnt) < 10: continue rect = cv2.minAreaRect(cnt) box = cv2.boxPoints(rect) / scale boxes.append(box.astype(np.int32)) return boxes逻辑说明:providers指定 CPU 执行,如果要用 GPU 就换成CUDAExecutionProvider。阈值0.3是经验值,调低会检出更多弱文字但误检增加,调高会漏掉浅色文字。contourArea过滤掉太小的噪点框。boxPoints得到的是旋转矩形四个角点,除以scale映射回原图坐标。这里没有做非极大值抑制,实际项目里如果框重叠严重,需要自己加 NMS。
3.3 方向分类与识别解码
方向分类模型输入是裁剪并缩放到固定尺寸的文字条,输出两个分数。识别模型输出的是时间步乘字符集大小的矩阵,需要 CTC 贪心解码。下面把两步串起来:
session_cls = ort.InferenceSession("models/cls.onnx", providers=["CPUExecutionProvider"]) session_rec = ort.InferenceSession("models/rec.onnx", providers=["CPUExecutionProvider"]) with open("models/keys.txt", "r", encoding="utf-8") as f: keys = [line.strip() for line in f.readlines()] keys = ["blank"] + keys # CTC blank 放第 0 位 def classify_and_recognize(crop): # 方向分类 cls_input = cv2.resize(crop, (192, 48)) cls_tensor = preprocess_rec(cls_input, 48) cls_out = session_cls.run(None, {session_cls.get_inputs()[0].name: cls_tensor})[0] if cls_out[0, 1] > cls_out[0, 0]: crop = cv2.rotate(crop, cv2.ROTATE_180) # 识别 rec_tensor = preprocess_rec(crop, 48) rec_out = session_rec.run(None, {session_rec.get_inputs()[0].name: rec_tensor})[0] # rec_out 形状 [1, T, num_classes] indices = rec_out[0].argmax(axis=1) text = "" prev = -1 for idx in indices: if idx != 0 and idx != prev: text += keys[idx] prev = idx return text逻辑说明:方向分类的输入尺寸192x48是常见配置,具体看模型要求。cls_out[0,1] > cls_out[0,0]表示判定为 180 度,就旋转回来。识别解码用 CTC 贪心策略:跳过 blank(索引 0),并合并连续重复字符。keys前面加blank是因为训练时字符集通常把 blank 放在第 0 位,如果你的模型不是这样,要按实际调整。参数上,识别输出的时间步T和输入宽度相关,宽度越大T越大,长文本更不容易丢字,但显存和耗时也上升。
4. 依赖库部署时最容易翻车的几个地方
4.1 现象:程序启动报找不到 onnxruntime 动态库
原因:OnnxRuntime 的 DLL 或 so 没有放在可执行文件同目录,或者系统 PATH 里没有。Windows 下还会因为缺少 VC 运行时而报另一个错。解决:把onnxruntime.dll复制到 exe 同级目录,Linux 下用ldd检查依赖,缺什么补什么。如果报的是VCRUNTIME140.dll缺失,装对应版本的 VC 运行时分发包。
4.2 现象:识别结果全是乱码或重复字符
原因:keys.txt和模型不匹配,或者 CTC 解码时 blank 索引搞错。解决:先确认keys.txt行数和模型输出类别数一致,再确认 blank 是不是第 0 位。可以打印rec_out.shape看最后一维,和len(keys)对比。如果差 1,说明你漏了 blank 或多加了。
4.3 现象:检测框位置整体偏移
原因:预处理时做了保持长宽比的缩放,但后处理没有把坐标映射回原图,或者映射时用错了比例。解决:检查scale的计算是input_size / max(h, w)还是input_size / min(h, w),两者结果不同。另外如果做了 padding,还要减去 padding 偏移。
4.4 现象:GPU 版本推理比 CPU 还慢
原因:图片太小或 batch 为 1 时,GPU 的 kernel 启动开销占主导,数据在主机和设备间拷贝也耗时。解决:要么增大 batch,要么直接换回 CPU。OCR 单图场景下,CPU 多线程往往更划算。可以设sess_options.intra_op_num_threads为物理核心数。
4.5 现象:长文本识别丢字或末尾截断
原因:识别模型输入宽度被限制,或者检测阶段把长行切成了多段但没合并。解决:识别时不要固定宽度,按比例缩放到高度固定、宽度自适应;检测后如果同一行被切成多个框,按 x 坐标排序后合并再识别。
5. 把离线 OCR 做稳的几个进阶习惯
5.1 用置信度过滤和二次校验兜住脏数据
识别模型输出的每个时间步都有分数,把有效字符的分数平均一下就是整段置信度。我一般会设一个阈值,比如 0.5,低于这个值的识别结果标记为可疑,交给人工复核或走备用规则。对于票据类固定字段,还可以用正则做二次校验,比如金额字段必须匹配数字和小数点,日期字段必须符合日期格式。这样即使模型偶尔抽风,也不会把错误结果直接写进业务库。
def recognize_with_score(crop): rec_tensor = preprocess_rec(crop, 48) rec_out = session_rec.run(None, {session_rec.get_inputs()[0].name: rec_tensor})[0] indices = rec_out[0].argmax(axis=1) scores = rec_out[0].max(axis=1) text = "" prev = -1 valid_scores = [] for idx, score in zip(indices, scores): if idx != 0 and idx != prev: text += keys[idx] valid_scores.append(score) prev = idx avg_score = sum(valid_scores) / len(valid_scores) if valid_scores else 0.0 return text, avg_score逻辑说明:valid_scores只收集非 blank 且非重复字符的分数,避免 blank 的高分拉高整体置信度。avg_score低于阈值时业务层可以决定丢弃或转人工。参数上阈值不是固定的,印刷体清晰可以设 0.7,手写或低质量扫描件设 0.4 更合适,需要在自己的数据上统计分布后定。
5.2 模型量化与线程调优的取舍
OnnxRuntime 支持动态量化和静态量化,能把模型体积压到原来四分之一左右,CPU 推理速度也有提升。但量化会带来精度损失,尤其是识别模型,字符区分度可能下降。我的习惯是:检测模型可以量化,因为框位置对精度不那么敏感;识别模型先不量化,除非实测精度下降在可接受范围内。线程方面,intra_op_num_threads控制算子内并行,inter_op_num_threads控制算子间并行,OCR 这种串行流水线主要调前者,设成物理核心数通常比逻辑核心数更稳,因为超线程在计算密集场景收益有限。
| 优化手段 | 体积变化 | 速度变化 | 精度风险 |
|---|---|---|---|
| 动态量化 | 降至约 1/4 | 提升 20% 到 50% | 中 |
| 静态量化 | 降至约 1/4 | 提升更多 | 中高 |
| 线程数调优 | 不变 | 提升 10% 到 30% | 无 |
| 输入尺寸缩小 | 不变 | 明显提升 | 小字漏检 |
5.3 一个我踩过的坑:别在循环里反复创建 InferenceSession
早期写 demo 的时候,我在每次识别调用里都ort.InferenceSession(...)新建会话,结果单张图要几百毫秒,后来发现创建会话本身就要几十到上百毫秒。正确做法是全局创建一次,复用会话对象。会话是线程安全的,多线程调用时可以用同一个 session,但要注意输入输出不要共享可变缓冲区。这个习惯看起来小,但在批量处理场景下能差出好几倍的总耗时。希望帮到你。
本文还有配套的精品资源,点击获取