资讯动态

OpenCV与ONNX Runtime实现英文数字检测识别的完整推理指南

发布时间:2026/10/1 3:54:17 来源:尧图企业网站定制
简介这套基于OpenCV-Python的英文数字检测识别源码面向从事OCR、图像文本识别方向的中级开发者与学习者主要解决图片中英文和数字的自动定位与识别问题。代码在opencv-python 4.11.0.86环境下测试通过集成文本检测与识别两套深度学习模型可直接对测试图片执行端到端识别也便于根据业务需求替换模型或调整预处理流程。压缩包共21个文件整体大小约118.34MB文件类型涵盖Python调用脚本、ONNX与PB格式的模型文件、JPG/PNG测试样张、依赖清单及字符集说明从源码到模型再到测试数据均包含在内结构清晰。目前已有70人浏览学习属于面向实践的小型源码包。使用者可获得完整可运行的检测识别脚本、基于EAST架构的文本检测模型、基于CRNN的识别模型以及多张不同场景的测试图片和字符集文件省去自行训练与模型转换的繁琐环节能够快速上手OpenCV与深度学习模型结合实现OCR也可作为后续项目二次开发的基础。1. 基于 opencv-python 的英文数字检测识别一个能直接跑的 ONNX 推理源码包生产线上拍下来的批号、快递单上手写体编号、设备铭牌上那一长串字母数字组合这些场景要的不是云端 OCR 那种大而全而是一个离线、可控、能塞进现有 Python 流程里的识别方案。这套基于 opencv-python 的英文数字检测识别源码核心就两样东西一个训练好的 ONNX 模型一份从图像预处理到结果输出的完整推理脚本。你不需要懂训练也不需要搭 GPU 环境装好依赖就能把图片里的英文和数字框出来并转成字符串。适合手里有 Python 基础、想在本地快速跑通检测识别、后续还要做二次开发的从业者。2. 模型加载与预处理ONNX Runtime 会话与 OpenCV 图像管线的衔接2.1 ONNXRuntime 和 ONNX 模型不是一回事先搞清楚推理会话怎么建第一次接触这个资源的人十有八九会卡在onnx和onnxruntime这两个词上。简单说ONNX 是一种模型文件格式就像打包好的权重文件本身不能跑ONNXRuntime通常缩写成 ort才是真正加载并执行这个文件的推理引擎。这个区别不是概念抠字眼而是直接关系到你怎么写代码——你用的是 from onnx import 还是 from onnxruntime import决定了后面一系列 API 的写法。推理侧通常不关心模型是怎么训练出来的。哪怕你看到资源里写了基于 PyTorch 训练只要最终导出成了 .onnx 文件在你的项目里就只需要 onnxruntime 这一个依赖。PyTorch 转 ONNX 是训练侧做的事那是另一个话题你拿到的是成品模型唯一任务就是把它加载起来、喂数据、取结果。import onnxruntime as ort providers [CUDAExecutionProvider, CPUExecutionProvider] sess ort.InferenceSession(ocr_det.onnx, providersproviders) print(可用推理后端:, ort.get_available_providers())providers 列表的作用是让 onnxruntime 优先尝试 GPU如果当前机器没有可用的 CUDA 环境它会自动回退到 CPUExecutionProvider不会直接崩。这样写的好处是同一份代码在开发机和部署机上都能跑区别只是速度。如果你明确知道部署机没有 GPU直接把 providers 写成[CPUExecutionProvider]更稳妥省去启动时的探测开销。拿到会话对象之后第一件事应该是看一眼模型的输入输出信息而不是急着喂图inp sess.get_inputs()[0] print(输入名:, inp.name) print(输入形状:, inp.shape) print(输入类型:, inp.type) for out in sess.get_outputs(): print(输出名:, out.name, 形状:, out.shape)输入形状里如果出现 -1 或者字符串形式的动态维度比如[batch, 3, height, width]说明模型支持动态输入尺寸。这个时候你就需要自己给一个固定的 H 和 W通常取模型的训练尺寸常见的是 640×640 或 320×320。这个尺寸不是随便定的它直接影响检测小目标的成功率后面第 4 章会专门讲怎么调。2.2 给模型喂对数据BGR 转 RGB、Letterbox、归一化一次做齐opencv-python 读出来的图像通道顺序是 BGR但绝大多数检测模型在训练时用的是 RGB。如果你直接把 cv2.imread 的结果喂给模型相当于把红色通道和蓝色通道对调了模型看到的颜色和训练时完全相反。对灰度图没影响但对彩色图就是灾难。踩过这个坑的人会告诉你彩色图识别率暴跌第一怀疑对象永远是通道顺序。读图之后别急着缩放。很多人图省事直接用 cv2.resize 把图片拉成 640×640这在图片比例接近正方形时勉强能用一旦遇到长条形图片内容会被水平拉伸字符变形检测框也跟着偏。正确的做法是 Letterbox等比缩放后用灰色填充到目标尺寸保证图片内容不变形。import cv2 import numpy as np def letterbox(img, dst_size(640, 640)): h, w img.shape[:2] target_w, target_h dst_size scale min(target_w / w, target_h / h) new_w round(w * scale) new_h round(h * scale) resized cv2.resize(img, (new_w, new_h)) canvas np.full((target_h, target_w, 3), 114, dtypenp.uint8) dx (target_w - new_w) // 2 dy (target_h - new_h) // 2 canvas[dy:dy new_h, dx:dx new_w] resized return canvas, scale, dx, dy def preprocess(img_bgr, dst_size(640, 640)): img_rgb cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB) lettered, scale, dx, dy letterbox(img_rgb, dst_size) blob lettered.astype(np.float32) / 255.0 blob np.transpose(blob, (2, 0, 1)) blob np.expand_dims(blob, axis0) blob np.ascontiguousarray(blob) return blob, scale, dx, dy这里返回的 scale、dx、dy 不是临时变量后面把检测框坐标还原到原图时全靠它们千万别丢了。填充值 114 是 YOLO 系列训练时常用的灰色值灰色不会给模型引入额外的纹理干扰如果你的模型源码里用的是 0 填充就把 114 改成 0。怎么确认看一下资源里的训练脚本或者推理 demo 里有没有类似cv2.copyMakeBorder的逻辑这是唯一靠谱的依据。归一化用的是除以 255把像素值压到 0 到 1 之间。有一部分模型训练时用的是 -1 到 1 的归一化这时你需要在除以 255 之后再执行(blob - 0.5) / 0.5。判断方法很简单跑一张图看输出置信度。如果所有框的置信度都异常低比如全部低于 0.1或者输出直接是 nan优先怀疑归一化方式不对把两种都试一遍马上见分晓。3. 输出张量解析与后处理从网络输出到可用的坐标和字符3.1 输出张量是黑匣子的钥匙形状与坐标格式判断onnxruntime 的sess.run()返回的是一个列表里面每个元素对应一个输出张量。检测类模型通常只有一个输出形状一般是(1, N, 6)或者(1, 6, N)其中 N 是模型预测的候选框数量6 代表每条预测的六个值中心点 x、中心点 y、框宽 w、框高 h、置信度 conf、类别编号 cls_id。先别急着解析看一眼形状再动手outputs sess.run(None, {inp.name: blob}) raw outputs[0] print(原始输出形状:, raw.shape)常见做法是输出形状排在第二位的是 6比如(1, 6300, 6)那直接按行解析就行。但如果打印出来是(1, 6, 6300)说明通道维在中间需要先转置。这个判断不费时间但写错了解析代码后面全是乱的。def parse_outputs(raw, conf_thresh0.5): pred raw[0] if raw.ndim 3 else raw if pred.shape[1] 6: pred pred.T boxes [] scores [] class_ids [] for row in pred: cx, cy, w, h row[:4] conf float(row[4]) cls_id int(row[5]) if conf conf_thresh: continue x1 cx - w / 2.0 y1 cy - h / 2.0 x2 cx w / 2.0 y2 cy h / 2.0 boxes.append([x1, y1, x2, y2]) scores.append(conf) class_ids.append(cls_id) return boxes, scores, class_ids这里有个容易被忽略的小坑模型的输出坐标可能是 cxcywh 格式也可能是 xyxy 格式。绝大多数 YOLO 系模型输出的是 cxcywh所以上面代码先按中心点格式解析。如果你发现框的位置整体对不上——比如框的中心明显不在字符上——再尝试直接把前四列当作 x1、y1、x2、y2 用。两种格式在推理代码里就一行判断的区别但写错了结果完全不可用。3.2 类别映射与 NMS 去重把序号变成字符模型输出的 cls_id 是整数它对应的是训练时的类别表顺序。英文数字检测识别这个场景类别表通常是数字 0-9 加字母 A-Z共 36 类有的模型会把大小写字母分开那就是 62 类还有的模型为了降低识别难度会把容易混淆的 0 和 O、1 和 I 合并成同一类。所以类别映射一定要以资源里自带的 class_names.txt 或源码里的 CLASSES 列表为准不要凭感觉猜。CLASSES [str(i) for i in range(10)] list(ABCDEFGHIJKLMNOPQRSTUVWXYZ) # 如果模型包含小写字母继续追加 list(abcdefghijklmnopqrstuvwxyz) # 部分模型会将 0/O 合并、1/I 合并此时类别表短一截按实际文件来有了坐标和分数还有一步不能省NMS非极大值抑制。模型在同一个字符周围往往会输出多个重叠的候选框NMS 的作用就是保留置信度最高的那个把重叠度过高的其他框去掉。def xyxy_to_xywh(boxes): converted [] for x1, y1, x2, y2 in boxes: converted.append([x1, y1, x2 - x1, y2 - y1]) return converted def nms(boxes, scores, iou_thresh0.45): if len(boxes) 0: return [] xywh xyxy_to_xywh(boxes) keep cv2.dnn.NMSBoxes(xywh, scores, 0.4, iou_thresh) if isinstance(keep, np.ndarray): keep keep.flatten() elif len(keep) 0: keep [] return keep注意cv2.dnn.NMSBoxes在 OpenCV 4.5.4 之后返回值类型有变化有时返回np.ndarray有时返回空元组。上面代码里做了两种兼容直接复制就能用不需要逐版本适配。NMS 的 iou_thresh 参数控制的是两个框重叠到什么程度算重复。默认 0.45 对字符检测来说偏高一点如果场景里字符排得很密比如序列号连在一起建议降到 0.3 左右否则相邻的字符框可能被误合并。到这一步你拿到的还是 Letterbox 坐标系里的坐标需要还原到原始图像尺寸def restore_box(box, scale, dx, dy): x1, y1, x2, y2 box return [ (x1 - dx) / scale, (y1 - dy) / scale, (x2 - dx) / scale, (y2 - dy) / scale ]这段逻辑很短但它是后处理里最容易出错的地方。第 5 章会专门列一个避坑条目这里先记住公式原图坐标 Letterbox 坐标 − 填充偏移÷ 缩放系数。4. 完整调用把检测识别过程串成一个可复现的 Python 脚本4.1 主流程代码图片、文件夹、摄像头三种入口一套逻辑前面的预处理、推理、后处理都是零件这一节把它们装配成一个能直接跑的脚本。我一般习惯把源码包里的推理 demo 拆成三个入口单张图片、批量文件夹、摄像头实时流。三种入口共用同一套 detect 逻辑只是数据来源不同。这样做的好处是开发时用单张图调试上线前用文件夹批量验证现场演示时切摄像头不用改核心代码。import os import cv2 import numpy as np import onnxruntime as ort import argparse def load_model(model_path): providers [CUDAExecutionProvider, CPUExecutionProvider] return ort.InferenceSession(model_path, providersproviders) def detect(sess, img_bgr, conf_thresh0.5, iou_thresh0.45): blob, scale, dx, dy preprocess(img_bgr) outputs sess.run(None, {sess.get_inputs()[0].name: blob}) raw outputs[0] boxes, scores, class_ids parse_outputs(raw, conf_thresh) keep nms(boxes, scores, iou_thresh) results [] for i in keep: x1, y1, x2, y2 restore_box(boxes[i], scale, dx, dy) label CLASSES[class_ids[i]] results.append((x1, y1, x2, y2, scores[i], label)) return results def draw_result(img, results): vis img.copy() for x1, y1, x2, y2, conf, label in results: x1, y1, x2, y2 int(x1), int(y1), int(x2), int(y2) cv2.rectangle(vis, (x1, y1), (x2, y2), (0, 255, 0), 2) text f{label} {conf:.2f} cv2.putText(vis, text, (x1, max(0, y1 - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) return vis入口部分用 argparse 接收参数这样不用改代码就能切换运行模式def main(): parser argparse.ArgumentParser() parser.add_argument(--model, defaultocr_det.onnx) parser.add_argument(--source, requiredTrue, help图片路径 / 文件夹路径 / 摄像头索引) parser.add_argument(--conf, typefloat, default0.5) parser.add_argument(--iou, typefloat, default0.45) args parser.parse_args() sess load_model(args.model) if os.path.isdir(args.source): for name in os.listdir(args.source): path os.path.join(args.source, name) if not name.lower().endswith((.jpg, .png, .jpeg, .bmp)): continue img cv2.imread(path) if img is None: print(f读取失败: {path}) continue results detect(sess, img, args.conf, args.iou) vis draw_result(img, results) out_path os.path.join(args.source, out_ name) cv2.imwrite(out_path, vis) chars .join([r[5] for r in results]) print(f{name} - {chars} conf均值: f{np.mean([r[4] for r in results]) if results else 0:.3f}) else: cap cv2.VideoCapture(int(args.source) if args.source.isdigit() else args.source) while True: ok, frame cap.read() if not ok: break results detect(sess, frame, args.conf, args.iou) cv2.imshow(detect, draw_result(frame, results)) if cv2.waitKey(1) 0xFF ord(q): break cap.release() if __name__ __main__: main()文件夹模式下输出图片会以out_前缀写入原目录方便你直接对比原图和识别结果。字符顺序这里的做法是按检测顺序拼接但真实场景里常见做法是按 x 坐标排序后拼接否则左右顺序可能乱。如果你识别的是序列号这种对顺序敏感的内容在拼接前把 results 按 x1 排序。4.2 两个阈值和输入尺寸最该调的三个参数这个资源能跑通只是第一步效果好不好全看参数。最主要的三个参数conf_thresh、iou_thresh、输入尺寸。conf_thresh 是置信度阈值低于这个值的预测框直接丢弃。默认 0.5 在光照均衡的图片上表现稳定但遇到昏暗环境或模糊字符真实目标的置信度可能只有 0.3 左右这时候还坚持 0.5 就会漏检。我一般建议先用 0.25 跑一遍看结果如果误检框太多再往上调。误检比漏检更容易发现因为你会看到明显不该出现的框。输入尺寸对检测效果的影响比很多人以为的大。模型训练的原始尺寸通常写在资源描述或训练配置里用 640×640 训练的模型你推理时也尽量用 640×640。强行改成 1280×1280 并不会让精度翻倍反而可能因为尺度分布变化导致识别率下降。如果你想提升小目标的识别率适当加大输入尺寸到 736 或 768 是可行的但要重新跑一遍批量验证不能只看一两张图的效果。iou_thresh 前面提过字符密集场景下调到 0.3。这个参数的调节原则是你发现相邻字符被合并成一个框调低你发现同一个字符出现两个重叠框调高。5. 避坑指南ONNX 推理最常见的五个翻车现场5.1 彩色图片识别率暴跌BGR 和 RGB 的顺序陷阱现象灰度图识别一切正常换成彩色图后漏检率飙升甚至一张都识别不出来。原因opencv-python 的cv2.imread读出来的通道顺序是 BGR而模型训练时用的是 RGB。彩色图的红蓝通道被整体对调模型看到的颜色完全是假的特征提取自然全乱。灰度图没有颜色信息所以不受影响。解决在预处理第一步显式做通道转换。img_rgb cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)。这条代码应该放在读图之后、任何缩放操作之前。如果资源里的源码已经写了别以为就安全了检查一下它是否在 resize 之前执行顺序错了也可能出问题。5.2 目标变小或坐标偏移Letterbox 缺失与还原公式错误现象小字符检测不到或者检测框位置和字符实际位置错位框偏左上或右下。原因直接cv2.resize到固定尺寸导致图片内容被拉伸变形字符比例失真模型检测不到或者虽然做了 Letterbox但还原坐标时忘了把偏移量减掉。解决使用第 2 章的 letterbox 函数做等比缩放并且保证每一处坐标还原都用到对应的 scale、dx、dy。一个实用技巧是在调试阶段把检测框画在 Letterbox 之后的图上而不是原图上先确认框位置对不对再谈还原。这样能快速定位问题出在检测阶段还是还原阶段。5.3 输出全 0 或 nan归一化方式和训练时不匹配现象模型跑完输出张量里所有值都是 0或者出现 nan检测结果为空没有任何报错。原因模型训练时用的是 -1 到 1 的归一化推理代码只除以 2550 到 1或者反过来。模型输入的数据分布和训练时完全不同网络输出自然就是垃圾。解决做一个小实验把同一张图分别用 0-1 和 -1-1 两种归一化方式各跑一次对比输出值的绝对值最大值。数值明显合理的那一个不是 0 也不是 nan就是正确的归一化方式。还有一种可能模型要求先减均值再除标准差这种情况资源源码里应该有对应的 mean 和 std 值翻一下训练脚本。5.4 换机器就报错ONNXRuntime 版本与 Provider 不匹配现象在开发机上跑得好好的代码拷贝到服务器或新电脑上import onnxruntime 直接崩或者报DLL load failed、CUDA_VISIBLE_DEVICES相关的错误。原因onnxruntime 和 onnxruntime-gpu 是两个包不能同时装。GPU 版本的 onnxruntime 依赖的 CUDA 和 cuDNN 版本和服务器上装的对不上初始化推理会话时就会失败。解决部署机上先跑ort.get_available_providers()打印一下看哪些后端真正可用。如果服务器没有配置 CUDA 环境就把 providers 改成[CPUExecutionProvider]不要保留 CUDA 的选项。onnxruntime 的 CPU 推理速度对单张图片检测来说通常也能接受别为了 GPU 硬着头皮去配 CUDA那个坑更深。5.5 框能出但整体错位坐标还原时忘了除以 scale现象框能框住内容但明显偏小或者偏大位置整体朝一个方向偏移。原因坐标还原时只减了 dx、dy忘记了除以 scale或者把 cxcywh 转 xyxy 的公式搞反了w 和 h 被当成绝对值直接使用。解决检查还原公式是否完整(letterbox坐标 - 填充偏移) / scale。另外注意偏移量 dx、dy 是 Letterbox 时计算出的填充尺寸不是随便填的居中偏移。如果你自己改了 Letterbox 的逻辑这两个值必须同步重新计算。6. 进阶技巧int8 量化与批量验证部署前的最后一道体检模型跑通只是第一步真正交付之前有两件事值得做一是用 onnxruntime 的量化工具把模型从 FP32 压到 INT8二是做一次完整的批量验证。先把量化做了因为它直接影响部署时的硬件门槛。from onnxruntime.quantization import quantize_dynamic, QuantType quantize_dynamic( ocr_det.onnx, ocr_det_int8.onnx, weight_typeQuantType.QInt8 )动态量化不需要校准数据集API 调用两行代码就能拿到一个新模型模型体积通常能缩小到原来的四分之一左右CPU 推理速度也有明显提升。代价是精度可能有轻微下降某些边界情况的置信度会变低。所以量化后的模型必须重新跑一遍批量验证不能直接替换上线。批量验证的方法很简单准备一批覆盖不同光照、不同角度、不同清晰度的真实图片循环跑检测把每张图的平均置信度和最低置信度记下来。我习惯把结果汇总成一张表格逐行看哪些图的最低置信度接近阈值。这些图就是上线后最容易翻车的高危样本。如果发现某一类场景比如逆光拍摄整体置信度偏低就应该针对这类场景做一些预处理补偿比如先做一次直方图均衡化再进模型。如果你的最终目标不是 PC 端运行而是部署到瑞芯微 RK 系列这类边缘设备这步做完之后还有一个转换路径用 rknn-toolkit2 把 ONNX 模型转成 RKNN 格式。这个转换过程和量化类似也需要准备一批校准图片而且对算子的支持程度有要求转之前先确认模型里没有不支持的算子否则会卡在转换阶段。我接过一个螺丝批号识别的项目单张测试图跑得完美上线后被车间灯光角度变化集体搞翻车根源就是验证样本太单一。从那以后我每次换视觉场景都强制跑一遍批量验证脚本把最低置信度的十张图拉出来逐张看再决定要不要调参数或换预处理方式。这个习惯帮我避掉了很多上线后的尴尬。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑