简介一套基于OpenCV-Python的英文与数字检测识别源码包适用于OCR入门开发者或需要快速集成文本检测识别能力的项目。内含完整的Python实现、预训练ONNX模型与EAST文本检测模型测试环境为opencv-python 4.11.0.86可对照作者博文验证效果。资源共21个文件涵盖Python源码、pyc缓存、onnx/pb模型、txt字符表与依赖清单以及13张jpg和2张png测试样例整体压缩包约118.34MB结构清晰便于直接运行与二次开发。已有70人学习下载适合希望在现有项目中加入英文数字识别功能或研究CRNNCTC与EAST检测原理的开发者参考。通过源码可了解文本检测、方向分类与序列识别流程配合多样本图片可快速测试模型泛化能力。1. 先跑通再优化opencv-python 英文数字检测识别源码和 onnx 模型到底该怎么用如果你手里刚好有一份基于 opencv-python 的英文数字检测识别源码外加一个 .onnx 模型文件最常被卡住的地方往往不是模型本身而是这两样东西怎么接起来。很多人第一反应是先去复现训练环境装一堆深度学习库结果在依赖地狱里转了两天还没看到一张识别结果。实际上用 onnxruntime 加载那个 onnx 模型做推理再用 opencv-python 把图像的前处理和结果的后处理包起来几十行代码就能在纯 CPU 上跑完整个检测识别流程毫秒级出结果。这其实就是个典型的 opencv-python 小玩意但别小看它验证码识别、车牌识别、物流单号提取底层套路都是同一套。本文按“模型为什么选 onnx → 源码怎么读 → 参数怎么调 → 坑怎么避 → 怎么批量验证”的顺序讲透适合手里有现成 onnx 模型、想快速落地验证效果的从业者。2. ONNX 为什么适合做部署它与 opencv-python 的分工和边界2.1 ONNX 只是中间格式真正跑起来的是 runtime先把概念捋清楚。onnx 文件不是像那些宣称“直接能跑”的 exe它只是一个开放神经网络交换格式把 PyTorch、TensorFlow、PaddlePaddle 里训练好的权重和网络结构统一到一个中间表示。我之前做过一次 pytorch 转 onnx导出命令就几行import torch model load_model_from_checkpoint() dummy_input torch.randn(1, 3, 32, 32) torch.onnx.export(model, dummy_input, eng_digit.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch}, output: {0: batch}})导出的 onnx 文件里包含每个算子的类型、参数和权重但它本身不会执行。你需要一个推理引擎来解释它。最常见的两个选择是 opencv-python 自带的cv2.dnn.readNetFromONNX和微软的 onnxruntime。这也是很多新手问“onnxruntime 和 onnx 区别”的根源onnx 是描述模型的静态文件onnxruntime 是加载并运行它的引擎。我在实际项目里几乎总用 onnxruntime 而不是 opencv 的 dnn 模块原因不是 opencv 不能用而是算子兼容度和调试信息差异很大。onnxruntime 对标准算子覆盖更全报错会直接告诉你哪个算子哪个维度不匹配不会像 opencv 那样偶尔给一个笼统的“unsupported operation”。所以在以下所有推理代码中我都用onnxruntime.InferenceSession来创建 session。第一步永远是检查模型的输入输出结构这一步能避免后面 80% 的维度错误import onnxruntime as ort session ort.InferenceSession(eng_digit.onnx, providers[CPUExecutionProvider]) for inp in session.get_inputs(): print(f输入: name{inp.name}, shape{inp.shape}, type{inp.type}) for out in session.get_outputs(): print(f输出: name{out.name}, shape{out.shape}, type{out.type})这段代码的输出决定了你后续所有预处理参数。比如输出是[alphabet, decoded]这种带 CTC 解码的输出还是[1, 62]这种分类概率。如果模型是编码-解码结构你还要同时拿到中间的alphabet张量和decoded字符串。不要凭训练时的记忆写 shape一切以 session 打印为准。2.2 opencv-python 负责图像侧onnxruntime 负责张量侧明确分工之后代码结构就清晰了。opencv-python 负责四件事读图、区域裁剪、尺寸归一化、结果可视化。onnxruntime 只做一件事把预处理好的numpy.ndarray转成张量跑前向推理返回原始输出。这种分工的最大好处是解耦。图像增强、倾斜校正、二值化、轮廓查找都是成熟算法不需要 GPU用 opencv 几行就能写完。而英文和数字的分类能力完全由 onnx 模型提供你可以在不改动图像管线的前提下替换模型文件从普通模型换成 int8 量化模型甚至从字符分类模型换成端到端 OCR 模型。但也别把 onnxruntime 当黑匣子乱喂数据。它要求输入必须是一个四维数组通常布局是[N, C, H, W]其中 C 是通道数H 和 W 是高度和宽度。opencv 默认读进图片的通道顺序是 BGR而很多模型训练时用的是 RGB。这是个极其常见的坑稍后在避坑章再详细说。先给你一个最小可用的预处理模板import cv2 import numpy as np def preprocess_for_onnx(image, target_size(32, 32), swap_rgbTrue): if swap_rgb: image cv2.cvtColor(image, cv2.COLOR_BGR2RGB) resized cv2.resize(image, target_size, interpolationcv2.INTER_LINEAR) normalized resized.astype(np.float32) / 255.0 channel_first np.transpose(normalized, (2, 0, 1)) return np.expand_dims(channel_first, axis0).astype(np.float32)注意target_size我写成了(32, 32)但实际必须看模型对输入的定义。如果 onnx 输入 name 为inputshape 是[1,3,32,32]这里的 H32, W32cv2.resize的第一个目标参数是(width, height)所以你应该写(32, 32)它实际就是把宽和高都设为 32。如果模型用的是(64, 32)即高度 32、宽度 64就要写(64, 32)千万别把顺序搞反。我见过不少人因为把宽高颠倒导致识别准确率直接掉一半还以为是模型没收敛。3. 复现源码的推理链路从读图到输出英文数字的四段代码3.1 源码骨架文件清单与调用顺序拿到一份“检测识别源码”先别急着逐行读先看文件结构。常见的组织方式是这样的文件作用infer.py主入口读图 → 调用检测与识别 → 输出结果detector.py检测部分定位英文数字所在区域recognizer.py识别部分加载 onnx 模型对每个区域做字符分类labels.txt字符映射表按 0-9、a-z、A-Z 的顺序排列eng_digit.onnx训练好的识别模型调用顺序一般是主脚本读入一张图片 → 调用检测器得到一组边界框 → 对每个边界框做裁剪、缩放 → 调用识别器得到每个字符的类别 → 合并成字符串。如果你的模型是端到端 OCR则检测器可以简化但思路差不多。3.2 加载模型与一次推理的最小代码先看 recognizer 部分最核心的一小段。假设模型输入是一个[1, 3, 32, 32]的 RGB 图输出是[1, 62]的 logits类别数 62 对应 10 个数字 26 个小写字母 26 个大写字母如果你的映射顺序不同后面 labels 文件也会不同。import onnxruntime as ort import numpy as np class EnglishDigitRecognizer: def __init__(self, onnx_path, label_path): self.session ort.InferenceSession(onnx_path, providers[CPUExecutionProvider]) with open(label_path, r, encodingutf-8) as f: self.labels [line.strip() for line in f.readlines()] self.input_name self.session.get_inputs()[0].name def recognize(self, char_img): # char_img: 单字符小图opencv 格式 import cv2 resized cv2.resize(char_img, (32, 32), interpolationcv2.INTER_LINEAR) rgb cv2.cvtColor(resized, cv2.COLOR_BGR2RGB) rgb rgb.astype(np.float32) / 255.0 tensor np.transpose(rgb, (2, 0, 1))[None, ...] # [1,3,32,32] logits self.session.run(None, {self.input_name: tensor})[0] pred_idx np.argmax(logits, axis1)[0] return self.labels[pred_idx]逻辑说明InferenceSession实例化时就把模型解析到了内存里整个过程只初始化一次不要在每次识别时重复加载。recognize方法里的resized是(32, 32)大小、三通道彩色图然后转 RGB、归一化到 0-1再转成HWC为CHW最后增加一个 batch 维度。session.run(None, {...})的第一个参数可以是输出名列表填None表示要所有输出。返回的是一个列表即使只有一个输出也要用[0]取出来。参数说明这里providers填了CPUExecutionProvider保证在没有 GPU 的机器上也能跑。如果你装了onnxruntime-gpu可以换成CUDAExecutionProvider但注意 CPU 和 GPU 的浮点累计顺序可能不同会导致极细微的精度差异对字符识别来说无关紧要。label_path的内容是从 0 开始索引的字符例如第一行0最后一行为大写Z顺序必须和训练时完全一致否则会出现错位乱码。3.3 把输出张量还原成英文和数字如果模型输出的是[1, seq_len, 62]这种带时序的 logits就不能简单 argmax 了需要做一次解码。常见的有两种一种是贪心解码每一步取概率最大的字符然后合并连续重复字符、去除空白符另一种是 CTC 解码opencv-python 不直接提供可以用 Python 循环代替。def greedy_ctc_decode(logits, labels, blank_index0): logits: [seq_len, num_classes] 的概率或 logits 返回去掉重复和空白的字符串 preds np.argmax(logits, axis1) chars [] prev blank_index for p in preds: if p ! prev and p ! blank_index: chars.append(labels[p]) prev p return .join(chars)逻辑说明CTC 的空白符在训练时通常映射到索引 0常见实现把空白符放在第一个类别。贪心解码的逻辑是先沿序列方向取每步最大概率对应的索引然后跳掉重复的连续字符再跳掉空白索引。这么做的好处是稳定、无额外依赖坏处是错过依赖上下文的最优路径但对短英文数字串验证码、车牌基本够用。如果你模型输出的是单个字符概率那就直接对最后一维做 softmax 再 argmax。写 softmax 时注意数值稳定性先减掉每行最大值再算指数不然在极端情况下会出现 inf 导致 nan。4. 让识别更稳的参数输入尺寸、阈值、NMS 和 int8 量化4.1 输入尺寸和归一化参数训练怎么设推理就怎么设很多人喜欢在推理时随意改尺寸觉得“大一点看得更清楚小一点跑得更快”。实际上onnx 模型里的卷积层对输入尺寸有严格期望尤其是带全连接层的模型输入尺寸固定为训练时的值改大改小都会直接报错或输出垃圾结果。先查 session 的输入 shape。如果是[1, 3, 32, 32]就老老实实把每个字符图 resize 到 32x32。如果模型是动态 shape比如[batch, 3, height, width]你也不能放飞自我不同长宽比会让字符严重变形。我的经验是统一转成正方形不保留原始纵横比对单字符识别反而更稳。多字符连在一起时建议先用检测把每个字符切出来再单独识别。归一化参数最容易踩坑。有些模型在训练时用了 ImageNet 的均值[0.485, 0.456, 0.406]和方差[0.229, 0.224, 0.225]推理时也必须用同样的值。但验证码、车牌这类数据集大多直接从 0-255 缩放到 0-1没有减均值的过程。怎么判断看训练代码。如果只是x / 255.0那你推理时也直接除 255如果用了 transforms.Normalize就补上均值和方差。一个简单方法是把同一张图分别用两种预处理跑一次观察输出向量的差异。如果发现某个字符的置信度始终只有 0.2~0.3且集中错在左侧或右侧多半是归一化方式不对。4.2 置信度与 NMS 阈值什么时候该调什么时候不该调如果你的 onnx 模型是检测模型比如 YOLOv5 转出来的那么后处理里要调两个阈值confidence_threshold 和 nms_threshold。这两个参数直接决定你检测到多少个框、会不会把同一个字符框两遍。常见设置是 confidence_threshold0.25nms_threshold0.45。但英文数字的密集程度通常不高框与框之间重叠很少nms_threshold 可以适当调大到 0.5 也没事。反而是 confidence_threshold 要小心。很多现实图片上的字符模糊、反光、倾斜模型给的置信度普遍偏低你设 0.5 就会漏检一半。我会先设 0.2 跑一遍看哪些是真漏检、哪些是误检再逐步上调。如果发现检测框比字符本身大一圈且框住了背景不要急着调阈值先检查模型输入的 letterbox 填充方式。对纯分类模型不存在 NMS只有一个置信度阈值。比如单字符输出 62 类概率最高概率只有 0.6是接受还是拒绝在实际工程里我会设一个 0.5 的 reject 阈值低于它就返回“未知”避免下游逻辑拿错误结果硬凑。但注意阈值不能设太高否则本来就模糊的字符全被拒绝了。4.3 int8 量化模型的额外注意事项“onnx 量化 int8”常被提起是因为它能把模型体积压到四分之一CPU 推理速度翻倍。但量化不是一个无脑开关。如果你拿到的是别人量化好的 int8 模型首先要确认量化方式是 per-tensor 还是 per-channel另外要确认是否包含了 QDQ 节点。onnxruntime 对 int8 模型有两种运行方式一种是把模型直接转成 uint8 跑量化 kernel另一种是保留 QDQ 节点然后走普通优化。如果 int8 模型在板子上识别率明显下降常见原因是量化校准集太少。比如用 100 张图校准就硬压模型对极端亮度、扭曲字体毫无抵抗力。解决办法一是向模型提供者要一套覆盖全字符、多背景的校准图重新量化二是换回 fp32 模型接受体积大一点换来稳定。我自己的原则是只有在内存吃紧的边缘设备上才用 int8PC 上跑 CPU 推理完全不需要冒这个险。5. 避坑清单加载失败、乱码、定位不准和掉点的排查路线5.1 加载和推理阶段的三个坑坑 1onnx 加载报错No operator named ...现象InferenceSession初始化时直接抛异常提示不支持某个算子。原因很可能是用了cv2.dnn.readNetFromONNX而不是 onnxruntimeopencv 的 dnn 后端长期对某些先进算子系统支持滞后比如动态 reshape、部分 attention 结构。解决换用onnxruntime.InferenceSession并且安装较新版本的 onnxruntime1.16 之后对 transformer 类模型更友好。如果换完还报错检查 onnx 本身是从什么框架导出的有些老框架导出的算子名不标准需要用onnxsim化简一下。坑 2输入维度强度不匹配报KeyError: input现象session.run时传入的字典 key 和模型实际输入名不一致。原因你沿用了训练脚本里的 input name例如叫data但导出 onnx 时改成了input。解决不要靠记忆直接打印session.get_inputs()[0].name获取。还有一个隐藏问题输入张量 dtype 不对。onnxruntime 对 float32 很严格如果你传入了 float64它会报Invalid Input Type。所以预处理最后一定要加.astype(np.float32)。坑 3识别结果全部是同一个字符现象不管输入什么图片输出始终是空白符或0。原因多为空白符索引没跳过。如果模型使用 CTC lossoutput 的第一个通道通常对应空白符你在 argmax 后没有把空白符过滤掉等于把所有结果都判为空白。解决在解码循环里检查if p ! blank_index。另一种原因是 labels.txt 从第 1 行开始索引而模型从 0 开始错位一个字符。5.2 识别效果阶段的三个坑坑 4轮廓定位到的英文数字残缺不全现象用cv2.findContours检测字符时有些字符被切掉一半比如i的上半部分和点被分开。原因二值化阈值固定导致暗色字符和背景粘连或者字符内部有空洞。解决不要用全局固定阈值改用cv2.adaptiveThreshold的ADAPTIVE_THRESH_GAUSSIAN_C并把 blocksize 设成奇数。我现在常用的是 blocksize15C5。对光照不均的图片这个组合比任何手工阈值都稳。切出来的轮廓后还需要做一个简单的高度合并把同一行内、水平距离小于字符宽度的多个轮廓合并成一个大框避免把一个字母拆成多个。坑 5int8 模型在真机上掉点超过 5%现象同一张测试图fp32 模型全对int8 模型错一半。原因校准数据分布与真实输入偏移太大或模型敏感层被量化后动态范围丢失。解决先用 onnxruntime 的quantization工具对精度敏感的层做 mixed precision只把卷积层量化保留最后的全连接层为 fp32。具体做法是from onnxruntime.quantization import quantize_static, QuantType, QuantFormat quantize_static( model_inputeng_digit_fp32.onnx, model_outputeng_digit_int8.onnx, calibration_data_readercalib_reader, quant_formatQuantFormat.QDQ, per_channelTrue, weight_typeQuantType.QInt8, nodes_to_exclude[last_fc] )逻辑说明nodes_to_exclude里填入最后一层全连接层的名字让它保持 float32 计算这通常能保住最高的置信度输出精度。参数per_channelTrue对卷积权重更友好quant_formatQDQ可以同时兼容 TensorRT 和 onnxruntime 新版本。如果你不知道哪层敏感就先用onnxruntime.quantization自带的run_precision_analysis看各层对输出误差的贡献排在前面的几层不要量化。坑 6摄像头实时识别掉帧、CPU 拉满现象用cv2.VideoCapture(0)读摄像头时整个推理循环只有 2~3 FPS。原因每一帧都做全图检测、多次 resize、多次 session.run。解决先缩小检测范围只在画面中间 1/3 区域检测字符每次检测到的字符用最近邻插值cv2.INTER_NEAREST裁剪减少缩放计算量推理引擎不要每帧重建。如果还慢考虑把输入图转成灰度单通道再复制三通道模型输入还是三通道但前面轮廓检测速度大幅提升。6. 批量验证与实时识别最后一招把模型榨干到这一步你已经能稳定跑通单张图片的识别。接下来要验证模型的真实能力不能靠肉眼评分需要一个批量脚本。我常用的做法是准备一个测试文件夹每张图片的文件名就是真值标签脚本自动预测并统计准确率import os import cv2 test_dir test_imgs total 0 correct 0 for fname in os.listdir(test_dir): if not fname.lower().endswith((.jpg, .png)): continue true_text fname.split(_)[0] # 例如: AB3_de1.png - AB3 img cv2.imread(os.path.join(test_dir, fname)) regions detect_char_regions(img) # 你的检测函数 pred_text .join(recognizer.recognize(img[y:yh, x:xw]) for (x, y, w, h) in regions) total 1 correct (pred_text true_text) print(f{fname}: 预测 {pred_text} | 真值 {true_text}) print(f准确率: {correct / total:.2%})逻辑说明文件名里的真值前缀格式可以按你自己的规范改但一定要保持简单、可解析。统计准确率时建议按位统计和整串统计都算一遍整串准确率只要有一个字错就判错能反映端到端稳定性按位准确率更容易定位是哪个字符容易混。如果整串准确率只有 80%按位却超过 97%说明错误集中在极少数易混字符上比如0/O、1/I、5/S这时可以给模型追加易混对样本或者在后处理里加入字符间上下文的规则。实时识别同样可以用这段框架只是把cv2.imread换成ret, frame cap.read()。我自己的习惯是在循环里先跑一帧全尺寸的字符区域检测如果上一帧已经连续 5 帧识别成功率就缩小检测范围只用上一帧位置的邻域这样能省掉不少算力。每帧跳出一次很小的cv2.waitKey(1)不然窗口界面会卡死。最后一个提醒如果你要拿这个方案去处理真实扫拍照一定记得在识别前做一次形态学开闭运算。开运算把细小的杂点去掉闭运算把断裂的字符笔画接上。我用cv2.morphologyEx配合cv2.getStructuringElement(cv2.MORPH_RECT, (3,3))对模糊字符的改善比调模型 threshold 还明显。这一路折腾下来最大的教训是模型文件本身不坑人坑人的永远是预处理和后处理的“我以为”。你不看会话打印的输入 shape就敢把任意尺寸塞进去你不查 labels.txt 的顺序就敢用训练时的索引解码。现在做这类 opencv-python 英文数字识别我已经习惯先花十分钟把输入输出、映射表、图像方向这三件事核对完再谈调参。这套流程移植到别的 onnx 模型上也通用希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?