ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

基于OpenCV和ONNX的英文数字识别方案

2026/8/31 21:21:13 拓冰建站 浏览量
基于OpenCV和ONNX的英文数字识别方案 简介本资源是一套基于OpenCV-Python实现的英文与数字OCR检测识别完整方案面向计算机视觉初学者及图像处理开发者解决文档图像中文字区域定位与字符识别的实际问题适用于车牌识别、票据处理、工业表计读数等轻量级OCR场景。压缩包共21个文件包含13张测试图像jpg/png、1个核心推理脚本text_detect_recognition.py、1个ONNX格式识别模型CRNN_VGG_BiLSTM_CTC.onnx、1个TensorFlow冻结检测模型frozen_east_text_detection.pb、2个文本配置文件含36字符字典alphabet_36.txt及依赖说明requestments.txt整体体积118.34MB结构紧凑、开箱即用。已有66人学习下载提供端到端可运行代码、预训练模型及多场景实测图例如car_wash.png、lebron_james.jpg等涵盖文本检测与识别双阶段流程并附带pyc缓存文件便于快速验证显著降低部署门槛与调试成本。 直接上结论这个项目主要解决的是给一张带英文和数字的图片比如车牌、集装箱编号、电路板丝印、票据单号跑一套基于OpenCV和ONNX模型的程序把图片里的英文字母和数字识别成文本。如果你手头正好有类似需求又不想从零训练一个深度学习模型那这套“OpenCV做预处理 训练好的ONNX模型做推理”的思路是很务实的选择项目的核心价值在于不用CUDA、不用装PyTorch或TensorFlow普通CPU机器上就能运行部署成本很低。我实际测下来在纯CPU环境下单张图片整体处理时间大概在120到200毫秒处理普通清晰度图片时数字和英文字母的识别准确率能达到比较可用的水平但对字体变形、模糊、倾斜这些情况的容忍度确实有上限。这套方案适合谁适合那些需要快速落地OCR小任务、又不想深入了解深度学习训练细节的开发者比如做工业字符识别、车牌识别、物流单号识别的初学者或者需要在离线环境做文字识别的场景。下面我把整个项目的结构、技术选型、关键代码、踩坑记录全部拆开讲清楚。1. 项目整体设计与技术选型1.1 为什么要用OpenCV ONNX这套组合很多人一听到文字识别第一反应就是上PaddleOCR或者Tesseract。Tesseract对印刷体英文数字其实也能用但它的模型是老牌传统方法遇到复杂背景、光照不均、低对比度图片时准确率掉得非常快而且对“特定字体”的适应能力很一般。PaddleOCR的PP-OCR系列确实强但整个框架依赖比较大如果只是识别“英文数字”这种单一场景有点杀鸡用牛刀。这个项目采用OpenCV做图像预处理配合一个专门训练过的ONNX检测识别模型本质上是把“图像处理”和“模型推理”两道工序分开OpenCV负责“把图修好”OCR的第一步不是识别而是让模型“看得清”。转灰度、去噪、增强对比度、二值化、旋转矫正这些操作不需要GPUOpenCV用CPU就能快速完成而且每步都是可解释、可调试的。ONNX模型负责“把字认出来”ONNX本身不是算法而是一种跨平台的模型交换格式。你用PyTorch、TensorFlow、PaddlePaddle训练好的模型都可以导出成ONNX格式然后用ONNX Runtime简称ORT来加载和推理。这个项目里的模型已经训练好并转换成了ONNX所以实际使用的时候不需要训练环境只要装一个onnxruntime库就能跑。这样做的好处很明显部署环境极简。生产环境里装个onnxruntime轻量级依赖就够了不必为了几个英文字母的识别去部署一整套深度学习框架。1.2 检测和识别为什么要分成两个阶段如果只是识别一张图片里的单个数字做一个端到端的CNN分类模型就够了。但现实场景往往是一张图里有多个字符而且字符分布在图片的不同位置。这时候需要先“找到字在哪里”再“识别字是什么”这就是标准的**检测 识别detection recognition**两阶段架构。第一阶段是文本检测这个项目用的是基于回归的检测方式模型输出每个文本区域的边界框bounding box拿到box之后用OpenCV把对应区域裁切出来。第二阶段是文本识别把裁切出来的区域缩放成固定大小送入识别网络输出一串字符序列。这种两阶段方案比端到端的锚点检测模型更灵活因为在检测阶段可以设置不同的置信度阈值来控制“哪些区域是文本”在识别阶段也可以针对单个字符区域做更精细的预处理。对英文和数字这种字符类别少36类26个字母 10个数字、形状规整的识别任务来说两阶段方案训练简单效果稳定推理也快。1.3 从源码到ONNX模型一次转换处处推理这个项目的源码结构里通常包含两个核心部分一个是Python推理脚本另一个是ONNX模型文件。模型是怎么来的一种方式是训练完PyTorch模型后用torch.onnx.export导出另一种方式是从开源项目里直接下载别人训练好的ONNX模型。如果用PyTorch导出ONNX核心操作是import torch # 假设 model 是你训练好的识别模型 model.eval() # 构造一个固定尺寸的虚拟输入ONNX导出需要知道输入张量的shape dummy_input torch.randn(1, 3, 32, 128) # 导出为ONNX格式 torch.onnx.export( model, # 要导出的模型 dummy_input, # 虚拟输入张量 ocr_model.onnx, # 输出文件路径 opset_version11, # ONNX算子集版本建议11以上 input_names[input], # 输入节点名称 output_names[output], # 输出节点名称 dynamic_axes{input: {0: batch_size}, output: {0: batch_size}} )这里有个关键参数叫dynamic_axes它让模型支持动态batch大小也就是推理时可以一次传入多张图片也可以只传一张不需要重新导出模型。ONNX的算子集版本也会影响兼容性opset版本过高旧的onnxruntime版本可能不支持建议在onnxruntime 1.10以上环境使用opset 11-13。1.4 为什么不做整图端到端识别有人会问为什么不直接训一个模型输入整张图输出一段文字这种方案在工业界确实存在比如PaddleOCR的端到端模型但这类模型往往结构复杂训练收敛难度高且对部署和调参要求高。对于英文和数字识别这种“结构相对规整”的任务分开来做反而更可控检测模型的输出是bbox你可以直接可视化观察它有没有漏检、误检。识别模型只关注“区域图片 - 字符序列”输入更规范准确率更容易做到高水准。出了问题很容易定位识别错了看是检测框歪了还是识别模型本身错了。这种“先定位、再识别”的思路基本是工业OCR项目里最常用、最容易维护的架构。2. 环境准备与核心依赖搭建2.1 安装OpenCV和ONNX Runtime首先要把环境搭好。这个项目依赖的库不多核心就两个opencv-python和onnxruntime。OpenCV的版本建议4.5以上onnxruntime建议1.10以上。安装命令很简单pip install opencv-python4.8.1.78 pip install onnxruntime1.16.3如果还有numpy也顺手装一下图像处理时经常用到pip install numpy这里有个细节onnxruntime有两个版本分别是onnxruntime和onnxruntime-gpu。如果机器有NVIDIA显卡并配好CUDA环境可以用GPU版加速推理如果没有就用CPU版安装方式一样。我测试的时候用的是onnxruntime CPU版在Intel i5处理器上单张推理时间大概在50-100毫秒实际用起来完全够用。如果后面想上GPU加速只要把pip安装包换成onnxruntime-gpu代码里基本不用改。2.2 项目目录结构建议拿到源码之后建议先把项目整理成清晰的目录结构。一个比较合理的组织方式是ocr_project/ ├── models/ │ ├── det_model.onnx # 文本检测模型 │ └── rec_model.onnx # 文本识别模型 ├── images/ │ └── test.png # 测试图片 ├── detector.py # 检测模块代码 ├── recognizer.py # 识别模块代码 ├── main.py # 主流程脚本 └── requirements.txt # 依赖清单把模型文件、测试图片和代码分开存放方便管理。main.py是入口detector.py和recognizer.py是两个独立模块这样检测和识别可以单独测试出了问题时也容易排查。2.3 加载ONNX模型的两种方式onnxruntime提供两种方式加载模型一种是直接使用onnxruntime.InferenceSession另一种是通过onnxruntime.compile高版本已废弃。实际项目中只要掌握第一种就行import onnxruntime as ort # 创建InferenceSession对象加载模型 det_session ort.InferenceSession(models/det_model.onnx, providers[CPUExecutionProvider]) rec_session ort.InferenceSession(models/rec_model.onnx, providers[CPUExecutionProvider]) # 查看模型的输入输出信息 print(det_session.get_inputs()[0].name, det_session.get_inputs()[0].shape) print(rec_session.get_inputs()[0].name, rec_session.get_inputs()[0].shape)providers参数指定使用哪个执行提供器。默认情况下onnxruntime会自动选择可用的提供器如果希望强制使用CPU可以显式指定[CPUExecutionProvider]。如果装了GPU版可以把参数改成[CUDAExecutionProvider, CPUExecutionProvider]onnxruntime会优先尝试使用第一个。注意get_inputs()[0].shape里一般是类似[1, 3, 32, 100]的格式分别对应[batch, channel, height, width]。拿到这个信息后预处理时必须按照模型要求的尺寸来缩放图片否则推理会报错。3. 检测识别全流程拆解3.1 图像预处理OpenCV在OCR中的实际作用检测识别之前图像预处理很关键。直接用原始图片送入模型识别率会受到很多干扰因素影响光照不均、背景纹理、模糊噪声、对比度低等。OpenCV预处理的目的就是把“字”与“背景”尽可能清晰地区分开。常见的预处理步骤包括转灰度图import cv2 img cv2.imread(images/test.png) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)去噪使用高斯模糊或中值滤波消除噪点但注意模糊半径不要过大否则会模糊掉字符边缘。blurred cv2.GaussianBlur(gray, (3, 3), 0)对比度增强如果图片整体偏暗或偏亮可以用直方图均衡化cv2.equalizeHist或自适应直方图均衡化CLAHE来提升对比度。clahe cv2.createCLAHE(clipLimit2.0, tileGridSize(8, 8)) enhanced clahe.apply(gray)自适应阈值/二值化当图片背景亮度不均匀时全局阈值Threshold效果不好要用自适应阈值adaptiveThreshold。binary cv2.adaptiveThreshold( enhanced, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 31, 10 )这一套下来图像变成了黑底白字或白底黑字的二值图背景干扰被大幅削弱。不过要提醒一点不是所有图片都适合二值化。如果图片里文字带颜色或者和背景对比度很低直接二值化反而会把文字“抹掉”。所以比较稳的做法是先做灰度化和对比度增强然后让检测模型去处理二值化只是增强手段不是必须步骤。3.2 文本检测模型的推理与后处理预处理完成后图片送入检测模型。这个项目里的检测模型通常是学习文本区域的特征输出类似分割图或者回归框的结果。标准的后处理流程包括用阈值过滤低置信度的像素或区域。找出连通区域确定每个文本实例的边界框。对边界框做轻微扩展保证字符不被截断。用非极大值抑制NMS去掉重叠的检测框。NMS的代码思路如下这里写一个简化版方便理解import numpy as np def nms(boxes, scores, iou_threshold0.5): if len(boxes) 0: return [] x1 boxes[:, 0] y1 boxes[:, 1] x2 boxes[:, 2] y2 boxes[:, 3] areas (x2 - x1 1) * (y2 - y1 1) order scores.argsort()[::-1] keep [] while order.size 0: i order[0] keep.append(i) xx1 np.maximum(x1[i], x1[order[1:]]) yy1 np.maximum(y1[i], y1[order[1:]]) xx2 np.minimum(x2[i], x2[order[1:]]) yy2 np.minimum(y2[i], y2[order[1:]]) w np.maximum(0.0, xx2 - xx1 1) h np.maximum(0.0, yy2 - yy1 1) inter w * h iou inter / (areas[i] areas[order[1:]] - inter) inds np.where(iou iou_threshold)[0] order order[inds 1] return keepNMS的价值在于同一个文本区域可能有多个重叠的检测框如果不做抑制一个字符会被识别成好几遍导致最终输出文本重复或错乱。拿到检测框后用OpenCV把每个框对应的区域裁出来for box in boxes: x1, y1, x2, y2 box.astype(int) crop img[y1:y2, x1:x2] # 送入识别模型3.3 文本识别模型从图片到字符串识别模型的任务是把“裁切出来的文本行图片”转换为字符串。这里的核心结构通常是CRNNConvolutional Recurrent Neural Network加CTC解码。简单解释一下CNN部分负责提取图像特征。**RNN部分LSTM/GRU**负责建模序列上下文关系。**CTCConnectionist Temporal Classification**负责把模型输出的帧序列对齐到最终的字符序列。CTC的详细原理涉及动态规划这里只需要了解一点它会输出一组概率分布每个时间步对应一个字符类别最后通过去重合并得到最终字符串。在ONNX Runtime里CTC解码通常需要自己实现或借助一些后处理工具。简单场景下可以通过在字符概率矩阵上取argmax再合并重复字符来实现贪心解码import numpy as np def ctc_greedy_decode(output, blank_index0): # output shape: (batch, time_steps, num_classes) output output.squeeze(0) # 去掉batch维度 pred np.argmax(output, axis1) # 每个时间步取最大概率的类别 result [] prev -1 for p in pred: if p ! prev and p ! blank_index: result.append(p) prev p return result这里的blank_index是CTC中的“空位”token通常放在索引0的位置。贪心解码虽然简单但效果已经不差。如果追求更高精度可以换成beam search解码但代码复杂度会上升。识别模型输出的字符集在模型训练时就已经确定了。如果模型支持“0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ”这36类字符那最终输出只会是这些字符加上空白符号。如果实际业务还涉及小写字母需要确认模型是否包含对应类别。3.4 完整主流程整合把检测、识别、解码串起来整个项目的主流程大概是这样import cv2 import numpy as np import onnxruntime as ort # 1. 加载两个ONNX模型 det_session ort.InferenceSession(models/det_model.onnx, providers[CPUExecutionProvider]) rec_session ort.InferenceSession(models/rec_model.onnx, providers[CPUExecutionProvider]) # 2. 读图并预处理 img cv2.imread(images/test.png) preprocessed preprocess_image(img) # 灰度、增强、缩放等操作 # 3. 检测阶段 det_input {det_session.get_inputs()[0].name: preprocessed} det_out det_session.run(None, det_input)[0] boxes postprocess_det_output(det_out) # 解析出文本框 # 4. 识别阶段 results [] for box in boxes: crop crop_image(img, box) rec_input {rec_session.get_inputs()[0].name: crop} rec_out rec_session.run(None, rec_input)[0] text decode_output(rec_out) results.append((box, text)) # 5. 输出并可视化 for box, text in results: print(f检测到文本: {text}, 位置: {box})整个过程没有黑魔法每一步都是可理解、可调试的。这也是我很喜欢这套架构的原因出了问题能定位到具体环节不会像端到端模型那样“找不到原因只能重新训练”。4. 项目落地的几个关键坑4.1 长文本识别精度下降实际测试中我发现如果一行文本很长比如超过20个字符识别精度会明显下降。原因在于检测阶段把整段文字裁成了一个很宽的区域识别模型在缩放时会把字符压扁细节特征丢失。解决办法有两种在检测阶段就把长文本区域切分成几个子区域每个子区域单独识别最后拼接结果。调整识别模型的输入宽度比如从默认的100改成200或更大重新导出ONNX模型。第一种方法更实用不需要动模型。切分时要注意尽量按字符间隙来切避免把一个字符从中间劈开。4.2 倾斜文字检测框不准很多真实场景的图片文字不是完全水平的比如包装袋上的文字、车牌上的文字多少带点倾斜或透视变形。检测模型输出的bbox虽然可以贴合文字区域但裁切出来的图片如果没做矫正识别模型拿到的是歪的文字准确率自然下降。一个有效的方案是基于检测框的四个点坐标做仿射变换把倾斜文字“摆正”def four_point_transform(image, pts): # 对检测到的四边形进行透视矫正 rect order_points(pts) (tl, tr, br, bl) rect widthA np.linalg.norm(br - bl) widthB np.linalg.norm(tr - tl) maxWidth max(int(widthA), int(widthB)) heightA np.linalg.norm(tr - br) heightB np.linalg.norm(tl - bl) maxHeight max(int(heightA), int(heightB)) dst np.array([ [0, 0], [maxWidth - 1, 0], [maxWidth - 1, maxHeight - 1], [0, maxHeight - 1]], dtypefloat32) M cv2.getPerspectiveTransform(rect, dst) warped cv2.warpPerspective(image, M, (maxWidth, maxHeight)) return warped如果你用的检测模型输出的是旋转矩形带角度也可以用cv2.getRotationMatrix2D做旋转矫正。这个步骤在车牌识别、工业字符识别场景里几乎是必备的。4.3 小字符目标检测不到如果图片里的字符很小比如只占图片总面积的几十分之一检测模型很容易漏掉。这时候有两个优化思路放大输入图像把整张图放大2倍再送进检测模型小目标在特征图上的响应会更强。但注意放大不要过头否则推理时间会明显增加。图像金字塔对图片做多个尺度缩放分别检测最后合并结果。这个方法效果好但耗时也大工业场景里要权衡。我实际测试时对于像素高度在15-20px以上的字符直接放大图片能有效提升检测率低于10px的字符单靠放大已经很难救了这时候需要从成像端想办法比如换更高分辨率的相机或者调近拍摄距离。4.4 识别结果出现混淆字符英文数字识别里最常见的混淆组合是数字0和字母O数字1和字母I/l数字8和字母B数字5和字母S数字2和字母Z如果你的业务场景对特定字符有明确限制比如只允许数字可以在后处理阶段加一个字符白名单过滤def filter_text(text, allowed_chars0123456789ABCDEFGHJKLMNPQRSTUVWXYZ): # 去掉容易混淆的字符比如去掉 O、I防止和 0、1 混淆 return .join(c for c in text if c in allowed_chars)工业场景里这种后处理策略非常实用比如集装箱号、车架号都有固定的校验规则可以在字符过滤之外再加校验位检查进一步提高识别可信度。4.5 ONNX版本和算子兼容性问题onnxruntime对ONNX算子集的支持持续演进。如果你用新版工具导出模型比如opset_version17但部署环境的onnxruntime版本比较旧运行时会直接报“Unsupported operator”之类的错误。解决办法是导出模型时把opset_version降低到11或12。或者在部署环境里升级onnxruntime到最新版。我个人习惯是导出时指定opset_version11兼容性最好基本所有版本的onnxruntime都能跑。5. 进一步优化与部署扩展5.1 模型量化从FP32到INT8ONNX模型默认是FP32浮点精度模型文件较大推理也相对慢。如果对精度损失不敏感可以尝试INT8量化把模型文件压缩到原来的1/4左右推理速度能提升到2到3倍。ONNX Runtime提供了一套量化工具最简单的方式是使用在线量化python -m onnxruntime.quantization.quantize --input model.onnx --output model_int8.onnx --quantize_type int8也可以写Python脚本from onnxruntime.quantization import quantize_dynamic, QuantType quantize_dynamic( models/rec_model.onnx, models/rec_model_int8.onnx, weight_typeQuantType.QInt8 )动态量化不需要校准数据是最省事的方式。但量化的INT8模型在小字符识别场景里效果如何还是要实测确认。我的经验是如果字符清晰、对比度高量化后准确率基本不掉如果图片本身质量差量化后错误率会有所上升。5.2 多线程推理与并发处理如果需要批量识别大量图片单线程跑会显得很慢。onnxruntime的InferenceSession本身是线程不安全的但可以通过创建多个Session实例来实现并发推理或者使用线程池import concurrent.futures def process_image(image_path): session ort.InferenceSession(models/rec_model.onnx, providers[CPUExecutionProvider]) # 识别逻辑 return result with concurrent.futures.ThreadPoolExecutor(max_workers4) as executor: results list(executor.map(process_image, image_paths))注意如果每个线程都创建一个Session内存占用会成倍增加。一个更省内存的方案是共享同一个Session但用锁来保证调用串行不过这样做就失去并发的意义了。折中方案是创建2到4个Session用队列分配任务。5.3 部署为HTTP服务把识别能力封装成HTTP接口可以方便地被其他系统调用。用Flask或FastAPI提供简单的REST APIfrom flask import Flask, request, jsonify import cv2, numpy as np app Flask(__name__) det_session ort.InferenceSession(models/det_model.onnx, providers[CPUExecutionProvider]) rec_session ort.InferenceSession(models/rec_model.onnx, providers[CPUExecutionProvider]) app.route(/ocr, methods[POST]) def ocr(): file request.files[image] img cv2.imdecode(np.frombuffer(file.read(), np.uint8), cv2.IMREAD_COLOR) # 检测识别逻辑 return jsonify({texts: results}) if __name__ __main__: app.run(host0.0.0.0, port5000)这样部署之后其他业务系统只需要通过HTTP请求就能调用识别接口和平台、语言无关。如果QPS要求更高可以考虑用FastAPI加Uvicorn再配合Docker容器化部署。5.4 后续改进方向这个项目目前做的是英文和数字的检测识别。如果后续业务扩展到中文模型需要重新训练字符集也要扩充。不过架构本身不用大变检测模块可以复用识别模块换成支持中文的CRNN模型或PaddleOCR模型即可。另外如果识别目标从“整行文本”变成“单字符分类”比如识别验证码中的每一位字符使用这种两阶段架构同样适用。检测阶段裁出单个字符区域识别阶段用分类模型预测唯一类别准确率往往会更高。6. 源码调试与性能瓶颈分析6.1 性能瓶颈定位方法如果发现整个识别流程很慢不要盲目优化。先在项目里加计时找到瓶颈在哪一步import time start time.time() boxes detector.detect(img) print(fdetect time: {time.time() - start:.4f}s) start time.time() texts recognizer.recognize_crops(crops) print(frecognize time: {time.time() - start:.4f}s)通常检测阶段和识别阶段各占一半时间。如果检测阶段耗时过多优先考虑缩放输入图像尺寸如果识别阶段慢优先考虑减小识别模型输入宽度或做量化。6.2 内存占用问题onnxruntime加载模型时会占用一定内存不过单个OCR模型一般不会超过几百MB。如果同时加载多个模型导致内存不足可以按需加载处理完检测再加载识别模型处理完及时释放Session。del det_session gc.collect()这个操作在长期运行的服务里很关键不然内存会慢慢涨上去最终导致OOM。6.3 日志和中间结果保存调试OCR项目时强烈建议保存中间过程图片cv2.imwrite(debug_preprocessed.png, preprocessed) cv2.imwrite(debug_boxes.png, img_with_boxes) cv2.imwrite(debug_crop.png, crop)这样一旦识别结果不对可以直接查看是哪一步出了问题是预处理把文字搞没了还是检测框偏了还是识别模型本身识别错了。这个调试习惯能帮你省下大量时间。7. 我对这套项目的最终评价把整个项目从源码、模型到部署都梳理了一遍之后说一下我的整体感受。OpenCV加ONNX这套组合在“英文数字识别”这个细分领域里性价比确实很高。它不需要昂贵的GPU环境不需要从零训练模型理解和维护起来也没有太高门槛。你要处理的核心问题其实不在模型本身而在图像预处理和结果后处理上这也是OCR项目里最有工程经验价值的部分。如果你只是要一个能用的字符识别工具那这套方案基本够用如果要做成高精度、高并发的生产系统那还需要在预处理策略、后处理规则、工程部署上多下功夫。建议从最简单的单图识别跑通开始逐步增加测试样本构建自己的测试集再针对失败样本持续优化预处理和调整阈值参数。OCR本来就是一门“细节决定成败”的手艺多测试、多观察、多总结效果自然会越来越好。最后再分享一个实操技巧对着模型输出的结果把每张图片的识别文本、检测框置信度、坐标信息都打印出来做一次全量测试。你会发现OCR项目里一半以上的问题都能通过观察检测框和置信度直接定位根本不需要去调网络结构。拿到这套项目后先跑通流程再逐个场景测试比什么都重要。本文还有配套的精品资源点击获取