ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python车牌识别实战:从检测到字符识别的完整链路解析

Python车牌识别实战:从检测到字符识别的完整链路解析 简介这份资源是一套基于Python与OpenCV的车牌识别完整项目源码面向具备一定编程基础、希望入门计算机视觉或图像处理的开发者与学习者可用于理解从图像预处理到字符识别的全流程实现。压缩包共40个文件约18.93MB包含py主程序脚本、jpg与png示例图片、xml配置、dat模型数据、7z压缩子包及js等辅助文件覆盖代码、测试素材与训练数据便于直接运行与调试。项目围绕灰度化、二值化、高斯滤波去噪、Canny边缘检测、形态学膨胀腐蚀、连通组件字符分割以及模板匹配或CNN识别等关键环节展开并涉及识别结果的纠错与格式校验。已有316人学习适合作为图像处理课程实践、自动驾驶感知入门或毕业设计的参考案例帮助读者掌握OpenCV在真实场景中的工程化用法。1. 车牌识别.zip 里到底装了什么一份 Python 落地的完整链路如果你手头正好有一个车牌识别.zip解压后大概率会看到一堆.py文件、几个.pt或.onnx权重、一个requirements.txt外加若干测试图片。很多人第一反应是「这不就是个 OpenCV 调包 demo」但真正跑起来才发现图片能识别视频就卡白天能识别晚上就废单车牌能识别多车牌就乱。这份资源的价值不在于它用了多新的模型而在于它把「检测 → 矫正 → 字符识别」这条链路用 Python 串成了一条能跑通的流水线适合想快速搭出车牌识别原型、又不想从零训模型的开发者。它解决的是「从一张图到一串车牌号」的完整闭环而不是单点算法演示。适合有 Python 基础、懂一点 OpenCV、想拿现成代码改造成自己业务逻辑的从业者。下面我按实际拆包和跑通的顺序把这份资源讲透。2. 拆开压缩包先看结构模块划分与依赖选型2.1 目录结构与各模块职责拿到压缩包后别急着pip install先花两分钟看目录。常见的组织方式是这样plate_recognition/ ├── main.py # 入口处理单图/视频/摄像头 ├── detect.py # 车牌检测输出 bbox ├── rectify.py # 透视矫正把斜车牌拉正 ├── recognize.py # 字符识别输出车牌号 ├── utils/ │ ├── image_utils.py # 预处理、裁剪、归一化 │ └── draw.py # 画框、写结果 ├── models/ │ ├── det.onnx # 检测模型 │ └── rec.onnx # 识别模型 ├── config.yaml # 阈值、尺寸、字符集路径 └── requirements.txt这个划分不是随便摆的。检测和识别分开意味着你可以单独替换其中一环——比如检测换成 YOLOv8识别保留原模型只要接口对齐就行。config.yaml是整条链路的参数入口阈值、输入尺寸、字符集都在这里改参数不用翻代码。utils里放的是无状态函数方便单测。提示如果解压后发现所有逻辑都堆在main.py里说明这份资源是「能跑就行」型改造时要先做模块拆分否则后面调参很痛苦。2.2 依赖选型为什么是 ONNX 而不是直接 PyTorchrequirements.txt里通常会有onnxruntime、opencv-python、numpy、pyyaml有些还会带torch。这里有个选型逻辑值得说清楚推理用 ONNX Runtime 而不是 PyTorch主要图的是部署轻、启动快、不挑环境。PyTorch 装完动辄几个 GONNX Runtime 只要几十兆在边缘设备或纯 CPU 机器上差距很明显。# 建议先建虚拟环境避免和系统里的包打架 python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate # 按 requirements 装但注意 opencv 版本 pip install -r requirements.txt # 如果 requirements 里写的是 opencv-python但你要处理视频 # 建议换成 opencv-python-headless 或确认带 ffmpeg 支持 pip install opencv-python4.8.1.78参数说明opencv-python版本不要盲目追新4.8 和 4.9 在cv2.dnn和视频解码上行为有差异老代码在新版本上可能报Unsupported ONNX opset。onnxruntime分 CPU 和 GPU 两个包onnxruntime-gpu需要 CUDA 版本匹配装错会直接 fallback 到 CPU 且不报错性能差十倍。逻辑说明先隔离环境是为了后面能干净地导出依赖。很多人直接在 base 环境装跑通后换台机器就复现不了血泪经验。2.3 模型文件校验别拿到坏权重就开跑解压后第一件事是校验模型文件完整性。ONNX 文件如果下载中断大小对但内容损坏加载时报的错往往很迷惑。import onnx import hashlib def check_onnx(path): # 先看文件大小正常检测模型几 MB 到几十 MB import os size_mb os.path.getsize(path) / 1024 / 1024 print(f{path} size: {size_mb:.2f} MB) # 再尝试加载能过就说明结构没坏 model onnx.load(path) onnx.checker.check_model(model) print(onnx model ok, ir_version:, model.ir_version) # 打印输入输出名后面写推理代码要用 for inp in model.graph.input: print(input:, inp.name) for out in model.graph.output: print(output:, out.name) check_onnx(models/det.onnx) check_onnx(models/rec.onnx)逻辑说明onnx.checker.check_model会校验计算图结构比直接InferenceSession更早暴露问题。打印输入输出名很关键因为不同导出脚本产出的名字不一样写推理时硬编码input可能对不上。参数上ir_version如果高于你装的 onnxruntime 支持范围会报版本不兼容这时要么升级 runtime要么让模型导出方降 opset。3. 把检测跑起来预处理、推理与后处理的参数怎么设3.1 图像预处理尺寸和归一化的坑检测模型对输入尺寸敏感。常见做法是固定640x640或416x416但车牌是细长目标直接 resize 会严重变形。import cv2 import numpy as np def preprocess(img, input_size(640, 640)): # 保持长宽比的 letterbox避免车牌被拉扁 h, w img.shape[:2] scale min(input_size[0] / h, input_size[1] / w) nh, nw int(h * scale), int(w * scale) resized cv2.resize(img, (nw, nh)) canvas np.full((input_size[0], input_size[1], 3), 114, dtypenp.uint8) canvas[:nh, :nw] resized # BGR - RGB, HWC - CHW, 归一化 blob canvas[:, :, ::-1].transpose(2, 0, 1).astype(np.float32) / 255.0 blob np.expand_dims(blob, axis0) return blob, scale, (nh, nw)逻辑说明114是 YOLO 系列常用的填充灰度值和训练时一致才能保证精度。scale和(nh, nw)要留着后处理把框映射回原图时用。参数上input_size必须和模型导出时一致改大了精度可能略升但速度掉改小了漏检明显。归一化用/255.0还是mean/std取决于训练配置搞错会让置信度整体偏低。3.2 推理与后处理NMS 阈值和置信度阈值import onnxruntime as ort session ort.InferenceSession(models/det.onnx, providers[CPUExecutionProvider]) def detect(img, conf_thres0.4, iou_thres0.5): blob, scale, (nh, nw) preprocess(img) outputs session.run(None, {session.get_inputs()[0].name: blob}) preds outputs[0][0] # shape: [num, 41...] boxes, scores [], [] for p in preds: score p[4] if score conf_thres: continue # cx, cy, w, h - x1, y1, x2, y2 cx, cy, bw, bh p[:4] x1 (cx - bw / 2) / scale y1 (cy - bh / 2) / scale x2 (cx bw / 2) / scale y2 (cy bh / 2) / scale boxes.append([x1, y1, x2, y2]) scores.append(score) # NMS 去重 idx cv2.dnn.NMSBoxes(boxes, scores, conf_thres, iou_thres) return [boxes[i] for i in idx], [scores[i] for i in idx]逻辑说明conf_thres设太低会框出一堆背景设太高会漏掉模糊车牌0.4 是常见起点。iou_thres控制重叠框合并车牌密集时调低到 0.3 能减少误合并。注意outputs[0]的维度顺序不同导出脚本可能是[1, num, 5]或[1, 5, num]搞反了框全乱。参数上如果模型是多类别p[4]后面还有类别分数取 max 即可。3.3 透视矫正把斜车牌拉正再识别检测框里的车牌往往是斜的直接送识别精度会掉。常见做法是用角点做透视变换。def rectify(img, box): x1, y1, x2, y2 map(int, box) roi img[y1:y2, x1:x2] gray cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY) # 边缘检测 轮廓找车牌四角 edges cv2.Canny(gray, 50, 150) contours, _ cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: return cv2.resize(roi, (240, 80)) cnt max(contours, keycv2.contourArea) rect cv2.minAreaRect(cnt) box_pts cv2.boxPoints(rect) # 按左上、右上、右下、左下排序后做透视变换 dst np.float32([[0, 0], [240, 0], [240, 80], [0, 80]]) M cv2.getPerspectiveTransform(box_pts.astype(np.float32), dst) return cv2.warpPerspective(roi, M, (240, 80))逻辑说明240x80是蓝牌常见比例绿牌可改成480x140。minAreaRect比approxPolyDP稳因为车牌边缘不一定闭合。参数上Canny 的50/150对光照敏感晚上要调低。如果矫正后还是歪说明角点排序错了检查boxPoints顺序。4. 字符识别与结果输出字符集、解码与置信度过滤4.1 字符集对齐识别错乱的头号原因识别模型输出的是索引必须和训练时的字符集严格对齐。常见字符集是京沪津渝...ABCDEF...0123456789加一个 blank。# config.yaml 里通常有 charset 路径 import yaml with open(config.yaml, r, encodingutf-8) as f: cfg yaml.safe_load(f) charset cfg[charset] # 例如 京沪津渝冀晋辽吉黑苏浙皖闽赣鲁豫鄂湘粤桂琼川贵云藏陕甘青宁新 idx2char {i: c for i, c in enumerate(charset)} blank_idx len(charset) # CTC 的 blank 通常在最后逻辑说明如果识别结果全是乱码或固定几个字九成是字符集顺序对不上。参数上blank_idx位置要看训练配置有的放 0 有的放最后。建议先用一张已知车牌图跑一遍人工核对输出索引和字符的映射。4.2 CTC 解码与置信度过滤def ctc_decode(logits): # logits shape: [T, num_classes] indices np.argmax(logits, axis1) result [] prev -1 conf_sum, conf_cnt 0.0, 0 for t, idx in enumerate(indices): if idx ! prev and idx ! blank_idx: result.append(idx2char.get(idx, )) conf_sum float(np.max(logits[t])) conf_cnt 1 prev idx conf conf_sum / conf_cnt if conf_cnt else 0.0 return .join(result), conf逻辑说明CTC 解码要去重且去 blankprev记录上一帧索引。置信度取每个有效字符最大概率的均值低于 0.6 的结果建议丢弃或标记待复核。参数上如果模型输出是 log_softmax要先np.exp再取 max。4.3 结果输出与可视化def draw_result(img, boxes, texts): for box, text in zip(boxes, texts): x1, y1, x2, y2 map(int, box) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, text, (x1, y1 - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) return img # 主流程串起来 img cv2.imread(test.jpg) boxes, scores detect(img) texts [] for box in boxes: plate rectify(img, box) logits rec_session.run(None, {rec_input: rec_preprocess(plate)})[0] text, conf ctc_decode(logits[0]) texts.append(text if conf 0.6 else low_conf) out draw_result(img, boxes, texts) cv2.imwrite(result.jpg, out)逻辑说明置信度过滤放在最后避免低质量结果污染输出。参数上0.6是经验值业务要求高可提到 0.8但会漏掉部分模糊车牌。可视化时putText的中文支持需要 PIL 或 freetypeOpenCV 原生不支持中文会显示问号。5. 避坑与排查跑不通时先看这几条5.1 现象模型加载报 Unsupported ONNX opset version原因onnxruntime 版本低于模型导出时的 opset。解决pip install onnxruntime --upgrade或让模型导出方用opset11重新导出。别硬改模型文件容易把图改坏。5.2 现象检测框位置整体偏移或缩放原因预处理用了 letterbox后处理却按直接 resize 算 scale。解决统一用 letterbox 的scale和 padding 反算检查(nh, nw)是否参与坐标映射。常见错误是只除了scale没减 padding。5.3 现象识别结果多字或少字原因CTC 解码去重逻辑写错或 blank_idx 位置不对。解决打印indices序列人工核对确认 blank 在字符集里的位置有的实现把 blank 放 0有的放最后。5.4 现象视频处理越来越慢最后卡死原因每帧都新建InferenceSession或没释放cv2.VideoCapture。解决session 在循环外初始化一次处理完cap.release()如果内存持续涨检查是否每帧都往 list 里塞了图像。5.5 现象晚上或逆光图片完全识别不出原因训练数据以白天为主模型泛化不够。解决预处理加 CLAHE 增强对比度或对暗图做 gamma 校正。参数上CLAHE 的clipLimit2.0、tileGridSize(8,8)是常用起点。如果还不行只能补数据重训别指望调阈值能救回来。6. 进阶技巧把单张推理改成批量与视频流单张图跑通只是起点实际业务多是视频流或批量图片。这里有个我踩过的坑直接 for 循环调detectGPU 利用率不到 30%因为每张图都单独做预处理和后处理计算没打满。改成 batch 推理后吞吐能翻两三倍。def detect_batch(imgs, batch_size8): results [] for i in range(0, len(imgs), batch_size): batch imgs[i:i batch_size] blobs [] metas [] for img in batch: blob, scale, pad preprocess(img) blobs.append(blob) metas.append((scale, pad)) # 拼成 [B, C, H, W] input_tensor np.concatenate(blobs, axis0) outputs session.run(None, {input_name: input_tensor}) for j, out in enumerate(outputs[0]): boxes postprocess(out, metas[j]) results.append(boxes) return results逻辑说明batch 推理要求所有图预处理后尺寸一致letterbox 正好满足。batch_size根据显存调CPU 上 4 到 8 比较稳GPU 可以到 16 或 32。参数上如果显存不够会报 OOM降 batch 即可别去改模型输入尺寸。视频流场景还要注意帧采样。不是每帧都需要识别常见做法是每 5 帧取 1 帧或检测到运动再触发。这样既省算力又能避免同一车牌反复输出。cap cv2.VideoCapture(traffic.mp4) frame_id 0 while True: ret, frame cap.read() if not ret: break frame_id 1 if frame_id % 5 ! 0: continue boxes, _ detect(frame) for box in boxes: plate rectify(frame, box) logits rec_session.run(None, {rec_input: rec_preprocess(plate)})[0] text, conf ctc_decode(logits[0]) if conf 0.7: print(fframe {frame_id}: {text}) cap.release()逻辑说明frame_id % 5是采样间隔交通场景车速快可改成 3静态场景可放到 10。置信度阈值提到 0.7 是为了减少视频里的抖动误报。参数上如果视频有 interlacing 或旋转先做cv2.rotate和去隔行否则检测框会飘。最后说个验证方法拿 20 张不同光照、不同角度的车牌图人工标注正确结果跑一遍算准确率。别只看 demo 图那都是挑过的。我一般会强制走一遍这个验证集确认召回和准确都达标再上业务。从那以后我每次拿到新的识别包都先跑自己的小验证集不再信 demo 图。希望帮到你。本文还有配套的精品资源点击获取
返回列表