OpenCV+ONNX模型实现英文数字检测识别源码解析
发布时间:2026/10/1 20:38:23来源:尧图网络
简介基于opencv-python的英文与数字检测识别方案内含可直接运行的源码与ONNX模型面向需要快速实现图像中英文、数字检测与识别功能的开发者适用于文本定位与OCR识别场景。整个压缩包共21个文件大小118.34MB主要包含Python主程序text_detect_recognition.py、CRNN_VGG_BiLSTM_CTC.onnx识别模型、frozen_east_text_detection.pb检测模型以及多个jpg/png测试图片和依赖说明txt便于直观验证效果并自行替换图片。目前已有70人学习下载。方案采用EAST文本检测与CRNN序列识别组合文件组织简洁主程序逻辑清晰适合有Python基础的中级开发者参考运行环境对应opencv-python4.11.0.86模型与脚本已配套能省去训练模型的时间帮助读者快速掌握英文与数字检测识别的完整流程。1. 这个标题到底在说什么一段python代码加一个onnx模型够不够用设备铭牌上的序列号、仓库里的物料编码、试剂标签上的批号这些场景有一个共同点字符内容基本只有英文和数字但背景、字体、拍摄角度都很杂。会去搜“基于opencv-python英文数字检测识别源码onnx模型”的读者多半是手里没有现成OCR能力又不想为几十个字符专门搭一套云端服务。这个标题描述的组合其实很标准opencv-python负责读图、缩放、画框这类图像杂活onnx模型负责检测和识别推理由onnxruntime执行整个链路全部在本地跑。它能解决的是从图片里稳定抠出英文数字串比如单号、序列号、版本号适合做方案验证、样机演示也适合学生把检测识别这条线完整跑通。先给结论拿到这套东西不要急着逐行读源码先打印两个onnx模型的输入定义后面的工作全是按部就班。2. 先把模型跑起来onnxruntime加载与最小可用代码2.1 要装的依赖以及onnxruntime和onnx到底什么关系先明确依赖边界。这个标题能跑起来只需要三个包pip install opencv-python numpy onnxruntime参数说明opencv-python负责图像解码、resize、颜色转换和画框numpy负责把图像内存转成模型需要的多维数组onnxruntime是执行推理的引擎。很多人第一次接触时把onnx和onnxruntime混成一个概念实际两者分工不同onnx是模型文件的格式描述的是网络结构和权重类似一个打包协议onnxruntime才是真正加载这个文件并执行算子的运行时。pip里还有一个叫onnx的包主要用在模型转换、shape推断和结构检查跑推理用不上它。标题写的是“源码onnx模型”说明你手里已经有权重文件推理阶段不需要pytorch环境这是个很大的优势。安装完先做一次环境自检确认三个库同时可以importpython -c import cv2, numpy, onnxruntime; print(cv2.__version__, onnxruntime.__version__)参数说明这行命令能一次性暴露缺包、版本冲突和onnxruntime安装失败三类问题。我在Windows和Linux上都这么干过比直接跑demo快。GPU版onnxruntime还得额外对应CUDA版本建议先把CPUExecutionProvider跑通再考虑加速。2.2 检测模型的最小推理代码读图、letterbox和输出解析假设你手上的检测模型是yolo系导出的onnx输入是[1,3,640,640]输出是[1,25200,85]也就是每个预测框有4个坐标加1个物体置信度再加80个类别分数。英文数字场景类别数可能不是80而是数字加字母的36类或62类理解结构即可。先写最小推理函数import cv2 import numpy as np import onnxruntime as ort det_session ort.InferenceSession( models/num_det.onnx, providers[CPUExecutionProvider] ) det_input det_session.get_inputs()[0] print(det_input.name, det_input.shape, det_input.type) def letterbox(img, new_size640): h, w img.shape[:2] r min(new_size / h, new_size / w) nh, nw int(round(h * r)), int(round(w * r)) resized cv2.resize(img, (nw, nh), interpolationcv2.INTER_LINEAR) canvas np.full((new_size, new_size, 3), 114, dtypenp.uint8) top (new_size - nh) // 2 left (new_size - nw) // 2 canvas[top:top nh, left:left nw] resized return canvas, r, left, top def detect(img_bgr): img_rgb cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB) canvas, r, left, top letterbox(img_rgb, 640) blob canvas.astype(np.float32) / 255.0 blob blob.transpose(2, 0, 1)[None, :, :, :] preds det_session.run(None, {det_input.name: blob})[0] return preds, r, left, top逻辑说明cv2.imread读进来的图是BGR通道顺序而onnx模型训练时几乎都用RGB。不转通道检测框可能整体偏移尤其在红色和蓝色物体附近最明显。letterbox的作用是等比缩放图片并填充灰边避免直接resize把字符压扁。yolo系列训练时通常用114做填充色但也有模型用0填充后面会讲怎么判断。blob变换把HWC格式转成CHW再加一个维度凑成batch。session.run的第一个参数传None表示取全部输出第二个参数是输入字典键是模型输入名值就是预处理后的数组。输出解析是多数人第一次翻车的点。按[1,25200,85]的结构写def parse_output(preds, score_thr0.25, nms_iou0.45): preds preds[0] boxes, scores, class_ids [], [], [] for det in preds: obj_conf det[4] cls_confs det[5:] cls_id int(np.argmax(cls_confs)) cls_conf cls_confs[cls_id] conf obj_conf * cls_conf if conf score_thr: continue cx, cy, bw, bh det[:4] * 640.0 x1, y1 cx - bw / 2, cy - bh / 2 boxes.append([x1, y1, bw, bh]) scores.append(float(conf)) class_ids.append(cls_id) final_boxes, final_scores, final_labels [], [], [] for c in set(class_ids): idxs [i for i, v in enumerate(class_ids) if v c] cb [boxes[i] for i in idxs] cs [scores[i] for i in idxs] keep cv2.dnn.NMSBoxes(cb, cs, score_thr, nms_iou) keep np.atleast_1d(keep) for k in keep: i idxs[int(k)] x1, y1, bw, bh boxes[i] final_boxes.append([x1, y1, x1 bw, y1 bh]) final_scores.append(scores[i]) final_labels.append(c) return final_boxes, final_scores, final_labels逻辑说明yolo输出里的cx、cy、bw、bh是相对于输入尺寸640的归一化值先乘640转回像素坐标再从中心点转成左上角加宽高的格式。置信度由物体置信度和类别置信度相乘得到这个乘积低于score_thr的框直接丢弃。英文数字类别多相邻字符的框可能互相重叠按类别分别做NMS比全局NMS更合理。cv2.dnn.NMSBoxes接收的是[x,y,w,h]格式不是x1y1x2y2。返回值的类型在不同opencv版本里不一样np.atleast_1d包一层能兼容新老版本。这一段还需要补一个步骤把检测框坐标还原到原图。letterbox里记录的比例r、pad值left和top还原公式是x_orig (x_box - left) / r。坐标还原的代码通常和画框一起出现我在第4章会给出完整可跑的版本。2.3 识别模型接力把裁剪区域送进第二个onnx检测模型只负责告诉你“哪里是字符”要读出内容还得第二个模型。英文数字识别模型常见输入是[1,1,32,64]或[1,3,32,64]前者是灰度图后者是三通道。拿到模型第一件事仍然是打印输入metacls_session ort.InferenceSession( models/num_cls.onnx, providers[CPUExecutionProvider] ) cls_input cls_session.get_inputs()[0] print(cls_input.name, cls_input.shape, cls_input.type) CHARLIST 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ def recognize(roi_bgr): gray cv2.cvtColor(roi_bgr, cv2.COLOR_BGR2GRAY) gray cv2.resize(gray, (64, 32)) blob gray.astype(np.float32) / 255.0 blob blob[None, None, :, :] logits cls_session.run(None, {cls_input.name: blob})[0] pred np.argmax(logits, axis-1)[0] return .join(CHARLIST[int(i)] for i in pred)逻辑说明识别模型通常按灰度图训练所以先把裁剪区域转成灰度。输入宽高以你打印出的shape为准这里写的64和32指宽和高。有的crnn模型输出是序列对数argmax后得到每个时间步的字符索引再用CHARLIST映射成字符串如果你的CHARLIST长度和类别数对不上argmax取到的索引越界或映射乱码优先检查模型最后一维的类别数。参数说明roi_bgr是检测框从原图裁出来的那一小块如果框太小比如低于20像素先放大两倍再送识别否则resize到32高度时字符会被压成一团。两段式结构说明检测加识别分开好处是任一段效果不对都能单独替换。比如换一个更大的识别模型不影响检测逻辑英文数字识别不准时也可以先怀疑是不是检测框裁剪得不够准剪歪了识别模型再强也白搭。3. 参数怎么调预处理、后处理与五个必调参数3.1 预处理里的玄学归一化、BGR/RGB和letterbox填充归一化是第一个坑。我见过同一个检测onnx网上demo用/255.0跑效果全空白最后发现模型导出前用的是(x/255 - 0.5) / 0.5也就是归一化到[-1,1]。判断方法很简单先看模型输入meta的type如果读入的float32大概率需要归一化再看模型第一个卷积层的权重数值分布接近0.05量级时输入范围通常在[0,1]如果权重初始值接近0.3量级输入可能是[-1,1]。还有一个更快的试验法同一张图分别用/255和/127.5 - 1跑一次对比输出置信度哪个框的置信度高就说明哪个预处理更接近训练时的设置。通道顺序同样影响巨大。opencv的imread永远是BGR但pyTorch训练时读图用的多是PIL或cv2转RGB。检测阶段转不转通道症状不会立刻崩而是框的位置偏那么几个像素或者小目标全漏。字符这类小目标对位置偏差很敏感我一般直接转RGB。识别阶段如果模型输入是三通道也要转RGB再喂如果模型输入是单通道灰度那就先cvtColor到GRAY再resize。cvtColor这一步不能省因为onnxruntime不会帮你做颜色空间转换。letterbox填充值也值得注意。yolo系常用114作为灰色填充但有些在自定义数据上训练的模型导出时预处理代码里写的是(0,0,0)填充。如果填充值差太远贴边的字符会被pad区域干扰出现“框在图片边缘特别多”的假阳性。想快速验证填充色对结果的影响把np.full里的114改成0跑一遍对比即可不需要重新导出模型。3.2 后处理两个阈值score_thr和nms_iou怎么配合score_thr和nms_iou是检测阶段最常调的两个旋钮。score_thr控制一个框“算不算数”过低会让背景区域乱出框过高会让模糊字符漏检。英文数字检测和通用目标检测不太一样字符印刷体通常清晰推理置信度集中在0.6到0.9之间起始值放在0.4比较平衡如果画面里有严重反光、遮挡0.4可能漏降到0.2就能找回一部分但误检会成倍增加。nms_iou控制两个框重叠到什么程度算同一个目标。字符场景里有个特殊问题相邻字符的框天然重叠比如字母“rn”中间的缝或者数字“11”两条竖线。如果iou阈值设成0.45可能把两个本来分开的字符框合并成一个识别模型拿到的就是半个字符加半个背景。我的经验是字符框重叠严重时把nms_iou降到0.3再验证如果场景字体间隔本来就宽0.45更合适。不同类别的框不会互相抑制所以按类别分别做NMS的代码要保留不能图省事把所有框丢进一个NMS。3.3 五个必调参数对照表写方案时把参数定明白后面调参才不会靠猜。下面这张表是我跑英文数字检测和识别时的默认起点参数起始值影响对象调参注意检测输入分辨率640小字符召回率降到416速度提升但小字漏检升到1280对内存和耗时都不友好score_thr0.25-0.4误检和漏检比例低对比度图片上提高阈值后画面全空是典型现象nms_iou0.45重复框、相邻框合并字符粘连场景降到0.3间隔大时可保持0.45letterbox填充色114边缘目标误检模型训练时若用的是0填充保留114会让图片边缘多一排假框识别ROI最小尺寸高小于24px直接放大识别准确率不放大时字符在resize后被压扁CRNN类模型几乎必然出错表格之后说两个实践细节。第一不要因为一张低质量图把全局参数改掉先看这张图是预处理问题还是模型能力问题把同一张图放大两倍再跑如果结果变好说明是输入分辨率和ROI尺寸的锅不是阈值的问题。第二跑一批图片后统计一下所有框的置信度分布如果大多数框都在0.8以上说明场景干净可以把score_thr抬到0.6减少后台误检如果分布在0.3到0.5之间检查预处理是否和模型训练时不一致。4. 工程化落地目录批处理、视频流与量化int84.1 把单张demo改成目录批处理并落盘CSV源码仓库里的demo一般只跑单张图片但真实需求往往是一次处理一个目录里几百张照片。先把检测和识别封装成一个函数返回带坐标的识别结果def detect_and_ocr(img_bgr): preds, r, left, top detect(img_bgr) boxes, scores, labels parse_output(preds) results [] for (x1, y1, x2, y2), conf in zip(boxes, scores): x1, x2 (x1 - left) / r, (x2 - left) / r y1, y2 (y1 - top) / r, (y2 - top) / r x1, y1 max(0, int(x1)), max(0, int(y1)) x2, y2 int(x2), int(y2) roi img_bgr[y1:y2, x1:x2] if roi.size 0: continue text recognize(roi) results.append((x1, y1, x2, y2, text, conf)) return results逻辑说明这里把letterbox里的ratio和pad还原回原图坐标。max(0, int(x1))是为了防止检测框落在图片边界外裁剪ROI时越界会直接报错。roi必须从原始BGR图上裁因为recognize内部会自己转灰度如果从RGB图上裁画框坐标又要多一次换算。最后返回的是坐标、文本、置信度的一列tuple。批量处理并写CSVimport csv from pathlib import Path with open(ocr_result.csv, w, newline, encodingutf-8-sig) as f: writer csv.writer(f) writer.writerow([file, x1, y1, x2, y2, text, conf]) for ext in (*.jpg, *.png, *.jpeg): for img_path in sorted(Path(samples).glob(ext)): img cv2.imread(str(img_path)) results detect_and_ocr(img) for x1, y1, x2, y2, text, conf in results: writer.writerow([ img_path.name, x1, y1, x2, y2, text, f{conf:.3f} ])参数说明encoding用utf-8-sig而不是utf-8这样CSV用Excel打开时中文文件名不会乱码。glob的三个模式覆盖常见图片后缀如果目录里还有bmp就再加一行。这一步跑完后结果文件里每一行就是一个识别到的字符或字符串字符框坐标是原图像素坐标可以直接用来做数据标注或距离测算。顺手把识别结果画到图上也很简单cv2.rectangle画框cv2.putText把text画到框上方我习惯在批量处理时同时输出一份带框的预览图方便肉眼核对CSV内容对不上的图是哪一步出的错。4.2 摄像头和视频流上跑的三个注意点视频流和单张图片不同最常见的问题不是模型不准而是卡顿和初始化延迟。第一onnxruntime第一次执行推理时要初始化线程池和算子kernel第一次推理可能比后续慢几十倍体验上就像程序死掉。解决方式是预热启动时喂一张黑图或第一帧跑一次把初始化开销提前消耗掉。第二不要每帧都跑完整检测字符画面变化通常没那么快每3帧或5帧处理一次就能保证实时性还能给CPU留出余量。第三视频流尺寸最好先固定摄像头支持的分辨率变化会让letterbox的pad值抖动我一般先resize到宽960再进检测管线保持宽高比即可。capture cv2.VideoCapture(0) detect_and_ocr(np.zeros((480, 640, 3), dtypenp.uint8)) frame_id 0 while True: ok, frame capture.read() if not ok: break frame_id 1 if frame_id % 3 ! 0: continue results detect_and_ocr(frame) for x1, y1, x2, y2, text, conf in results: cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(frame, text, (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) cv2.imshow(result, frame) if cv2.waitKey(1) 0xFF ord(q): break逻辑说明detect_and_ocr接收的是BGR图画框也在同一张图上进行省去一次颜色转换。跳帧逻辑用frame_id对3取模每3帧推理一次中间两帧直接丢弃对实时场景够用。waitKey(1)让画面刷新保持正常按q退出循环。4.3 pytorch转onnx和onnx量化int8的实用路径很多人手里其实没有现成onnx只有pytorch权重。常见做法是用模型自带或通用的导出脚本torch.onnx.export( model, dummy, num_det.onnx, input_names[images], output_names[outputs], dynamic_axes{images: {0: batch, 2: height, 3: width}}, opset_version13 )逻辑说明dummy是随机输入tensor尺寸要和模型期望一致。opset_version优先用13或更高太低会导致部分算子无法导出。dynamic_axes让batch和宽高变为动态但注意如果你后续要转rknn动态尺寸会大幅增加转换成本不如先固定导出等下位机确定后再说。yolo源码仓库里通常自带export.py优先用现成脚本自己写容易漏掉模型里的自定义输出层。量化int8是缩小模型和提升CPU推理速度的常见手段。onnxruntime提供一条很轻量的动态量化路径from onnxruntime.quantization import quantize_dynamic, QuantType quantize_dynamic( num_det.onnx, num_det_int8.onnx, weight_typeQuantType.QInt8 )逻辑说明这里的动态量化主要对权重做int8激活仍用浮点输入输出也保持浮点。好处是代码改动极小模型文件通常能缩到原来的四分之一CPU推理速度有可感知的提升坏处是识别模型量化后更容易掉点字符类别密集时尤其明显。要想做真正的激活int8量化需要用quantize_static配合校准数据集流程复杂很多。我的建议是检测模型先动态量化试一下识别模型先在未量化版本上把准确率调好再量化对比一遍真实图片集掉点超过一个可接受范围就回退。5. 避坑opencv-python跑onnx检测识别的五个翻车现场5.1 检测框集体往左上角偏letterbox坐标没还原现象检测框都能框住目标但整体往图片左上角偏移偏移量随图片原尺寸变化。原因检测结果坐标是letterbox画布上的坐标直接用这个坐标在原始图上画框没有减去pad值也没除以缩放比例。左上角偏移是因为letterbox后图被放在画布中央原图区域左上角正好在pad之后。解决用第4章detect_and_ocr里的还原公式把每个框的x、y先减left、top再除以r。r和left、top来自letterbox函数不能重新resize图片后再算一次否则pad值会对不上。5.2 识别结果乱码或全是同一个字符通道顺序和输入格式不对现象检测框画得准但识别结果是一串随机字符或者所有框都识别成同一个字母。原因识别模型期望输入是灰度图你喂进去的是三通道BGR图或者模型期望三通道RGB你喂了灰度。还有一种是输入宽高顺序写反比如模型要的是[1,1,32,64]你把resize参数写成了(32,64)。解决第一件事永远是打印cls_session.get_inputs()[0].shapeshape里的第2、3维就是高和宽。然后按通道数做转换shape[1]等于1就转灰度等于3就转RGB并保持三通道。resize的顺序是(width, height)也就是先宽后高和shape里的顺序相反这个细节最容易写反。5.3 第一次推理慢到像死机会话初始化和线程池的问题现象程序启动后第一张图处理了整整几秒后面图片突然变快摄像头场景里第一帧卡住不动。原因onnxruntime在创建InferenceSession时就初始化了执行环境和线程池但在第一次run时才会触发部分内核加载和内存分配。有的模型还有NMS、anchor等后处理算子第一次运行会做形状推断。解决在正式流程开始前跑一次空图预热把这次开销提前消耗掉。还有一个常见误用是每张图都新建InferenceSession这会让初始化成本反复出现正确做法是全局只创建一次session处理函数里只调run。5.4 onnx输入尺寸被写死直接喂原图导致报错或全空白现象detect阶段报Unexpected input shape或者不报错但结果全空白。原因导出的onnx把输入尺寸固定成了640x640你直接用原图尺寸喂进去或者dynamic_axes只留了batch维度宽高维度仍是固定值你换了一组不同大小的图它就拒绝执行。解决打印det_input.shape看到里面有具体数字而不是字符串就是固定尺寸所有输入图都先letterbox到那个尺寸。如果模型支持动态宽高用之前pytorch导出时的dynamic_axes配置重新导一次如果已经是动态尺寸但推理结果空白检查是不是归一化方式随尺寸变化了有些模型在训练时对不同尺度用了不同预处理。5.5 cv2.dnn.NMSBoxes返回值类型随opencv版本漂移现象同一份代码一台机器跑得好好的换台机器报TypeError或IndexError问题集中在NMS那几行。原因opencv 4.5之前NMSBoxes返回numpy数组之后的版本部分场景返回list遍历时直接用for i in keep没问题但用keep[0]取下标就会出错。解决统一用keep np.atleast_1d(keep)包一层然后int(k)转成整数下标。这行代码成本极低但能让你的脚本在多个opencv版本下都不翻车。顺便检查cv2.dnn.NMSBoxes的输入boxes必须是一组[x,y,w,h]格式的list不能是numpy array的二维数组否则部分版本会报形状错误。6. 验证onnx转换有没有炸以及准备迁移rknn/tensorrt的两个习惯6.1 用固定图片集做数值一致性校验每次拿到一个新导出的onnx第一件事不是看检测效果而是做数值一致性验证。方法很朴素同一张输入图分别经过torch模型和onnx模型比较输出tensor的差异上限diff np.abs(onnx_out - torch_out.cpu().numpy()) print(diff.max())这个最大绝对差异在1e-3量级以内说明转换基本没炸。但数值一致不代表效果一致还需要在20张真实图上分别跑推理比较检测框的IoU和识别字符是否相同。这套验证集要固定下来以后每次换模型、换量化参数都跑一遍。我养成的习惯是新建一个samples_golden目录里面放各种字体、光照、角度的样品图任何改动都不能让这批图的结果变差。6.2 往rknn和tensorrt迁移前的两个准备边缘端部署是英文数字检测识别的常见下一步rknn和tensorrt各有各的雷但它们有两个共同点。第一转换工具对动态尺寸支持有限导出onnx时如果把宽高设为动态rknn转换阶段往往直接报错或不支持稳妥做法是先固定到你实际要用的分辨率比如640x640再导一个转换专用版本。第二边缘端量化几乎都要求提供校准图片集用合成数据蒙混过关后在真机上一跑就原形毕露。准备几百张和你现场分布一致的图片单独保存转换时一次性喂进去。这一步没偷懒空间我在rknn上试过一次用纯黑图做校准结果检测框全飘到角落最后老老实实拍了几百张场景图重新量化才正常。onnx模型加opencv-python这套组合本质是让你用最小的依赖把检测识别能力先跑通。遇到效果不对时按“预处理是否一致、输入尺寸是否一致、输出解析是否一致”的顺序排查比闷头调阈值高效得多。希望这些折腾过程中总结的检查顺序和踩坑记录能帮到你让你少走几趟我走过的弯路。本文还有配套的精品资源点击获取
网站建设高端定制企业官网