简介:一套基于YOLOv5的车牌识别Python毕业设计项目,面向计算机视觉、深度学习方向的毕业生与开发者,用于解决车牌定位与字符识别的完整流程。资源共78个文件,包含19个Python脚本(如训练、检测与工具脚本)、9个YAML配置(模型与数据参数)、5个PT权重文件,以及测试图片、视频、运行说明等,压缩包约345.91MB,结构清晰便于直接运行与二次开发。内容覆盖深度学习基础、YOLOv5架构、模型微调、图像预处理、OCR车牌识别和性能评估等关键环节,提供从数据标注、数据集划分、训练参数设置到结果可视化的端到端工程实现,并附有mAP、召回率等评估指标的使用说明。已有1506人学习下载,适合作为毕业设计参考或车牌识别入门实战,能帮助读者快速掌握目标检测与OCR结合的项目开发方法。
1. yolo5 车牌识别 python 毕业设计:想清楚两段式管线再动手
“yolo5 车牌识别 python 毕业设计”这个题目,听起来像是用目标检测一个模型就能把车牌号读出来,实际上大多数能跑通、能答辩的方案都是两段式:先用 YOLOv5 把车牌区域框出来,再把裁剪后的车牌小图交给字符识别模型,输出一串文本。检测负责“在哪”,识别负责“是什么”,两者分开训练、分开调参,出问题时也更容易定位。这个标题适合手里有 CCPD 或自拍车牌图片、想用 Python 在笔记本上完成训练到演示全流程的读者。下面按数据准备、训练、识别分支、演示程序、排错和验收六步把这条主线讲透。
2. 把 CCPD 或自采数据整理成 YOLOv5 能吃的格式:转换脚本与训练命令
2.1 车牌检测为什么只标一个类别
很多第一次做这个题目的人会纠结要不要把车牌类型也分出来,比如蓝牌、黄牌、新能源绿牌。从检测角度讲,这些都应该归为同一个 plate 类别,因为后续字符识别阶段会单独处理颜色和字符序列。类别多了反而会让 YOLOv5 在少量数据上更难收敛,尤其是新能源车牌和教练车黄牌,样本数天然不均衡。毕设阶段把检测目标简化成一个类别,输出结果照样能支撑后面的识别流程。
数据来源通常是两个:公开的 CCPD 车牌数据集,或者自己用手机拍一段停车场视频后抽帧。CCPD 的图片文件名里直接带车牌框坐标,省掉手工标注;自采数据则需要用 LabelImg 之类工具逐张标。不管哪种来源,最终都要落到 YOLOv5 的标签格式:每个图片配套一个同名 txt,每行是“class_id x_center y_center width height”,坐标全部归一化到 0~1。目录结构固定为 images 和 labels 分开存放,训练集、验证集各自独立目录。
2.2 把 CCPD 文件名里的坐标转成 YOLOv5 标签:转换脚本
CCPD 文件名里有一段是车牌框的两个对角点,格式形如154&383_386&473。用 Python 的字符串切分加正则就能把它抽出来,再读图片宽高做归一化,写出 txt。下面这个脚本可以直接放在数据集根目录运行:
import cv2 import re import os from pathlib import Path def convert_ccpd_to_yolo(img_dir: str, out_dir: str) -> int: """ 把 CCPD 图片所在目录转换为 YOLOv5 标签目录。 返回成功处理的图片数量。 """ os.makedirs(out_dir, exist_ok=True) count = 0 for img_name in os.listdir(img_dir): if not img_name.endswith('.jpg'): continue # CCPD 文件名用 '-' 分段,第 4 段(下标 3)是车牌框对角点 fields = img_name.split('-') if len(fields) < 4: print(f'跳过无法解析的文件: {img_name}') continue corners = fields[3] # 对角点有两种写法:x1&y1_x2&y2 或 x1&y1_x2&y2_其他 x1, y1, x2, y2 = map(int, re.split(r'[&_]', corners)[:4]) img = cv2.imread(os.path.join(img_dir, img_name)) if img is None: print(f'图片读取失败: {img_name}') continue h, w = img.shape[:2] # 转归一化中心点坐标与宽高 cx = (x1 + x2) / 2.0 / w cy = (y1 + y2) / 2.0 / h bw = abs(x2 - x1) / w bh = abs(y2 - y1) / h txt_name = Path(img_name).stem + '.txt' with open(os.path.join(out_dir, txt_name), 'w', encoding='utf-8') as f: f.write(f'0 {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}\n') count += 1 return count if __name__ == '__main__': # 用法:python convert_ccpd.py n = convert_ccpd_to_yolo('ccpd_images', 'labels/train') print(f'转换完成,共 {n} 张')这段脚本的核心动作是解析文件名、读取图片尺寸、计算归一化坐标。re.split(r'[&_]', corners)[:4]是为了同时处理带旋转角信息的文件名,前四个数字一定是对角点坐标。abs(x2 - x1)和abs(y2 - y1)防止文件名中对角点顺序颠倒导致宽高为负。如果你的数据来自 LabelImg 的 XML,位置数值是xmin/ymin/xmax/ymax,同样是读宽高后套用同一套归一化公式,唯一区别是来源字段名不同。
2.3 训练配置与参数:从 YOLOv5s 开始,不要一上来就换大模型
训练前先建一个plate.yaml,告诉 YOLOv5 数据集在哪里、类别有几个、名字是什么:
# plate.yaml path: ../datasets/plate # 数据集根目录,按实际路径改 train: images/train val: images/val nc: 1 names: ['plate']然后进 YOLOv5 仓库目录执行训练:
python train.py \ --img 640 \ --batch 32 \ --epochs 80 \ --data ../datasets/plate.yaml \ --weights yolov5s.pt \ --cache ram \ --project runs/plate几个关键参数这样调:--img 640是输入尺寸,车牌在整张图中占比不大,缩到 416 容易丢失细节,反而让 mAP 虚低;--batch 32在 8GB 显存上比较稳,显存不够就降到 16;--epochs 80对单一类别检测足够,再多也不会明显涨点;--weights yolov5s.pt用官方预训练权重做迁移学习,比从头训练快一个量级。--cache ram把图片缓存进内存,避免每次 epoch 都重新读磁盘,前提是你的内存比数据集大。训练结束后看runs/plate/exp/weights/best.pt是否生成,验证集结果里的 mAP@0.5 一般能做到 0.95 以上才算正常。
训练完别只盯着 loss 曲线,要打开val_batch0_pred.jpg和val_batch0_labels.jpg对比预测框和真实框的贴合程度。loss 降不代表框得准,尤其车牌是小目标,经常出现 mAP 不低但实际框偏左偏右的情况。这一步不检查,后面识别分支的输入就会带着大量背景噪声。
3. 车牌识别分支:裁剪、灰度化与 LPRNet 推理
3.1 为什么不能用 YOLOv5 直接输出车牌号码
有尝试把车牌号直接当检测类别的人,比如“京A12345”算一类,听起来很省事,实际行不通。车牌字符是组合序列,理论上有上千万种排列,训练数据根本覆盖不过来。更常见的错误做法是让 YOLOv5 检测每一个字符,再按位置排序拼接。这个方案在字符间距均匀、图像清晰时能跑,但省份汉字和字母数字混在一起,样本少、字符小,训练成本翻倍,效果还不稳定。所以识别阶段普遍改用 LPRNet 或 CRNN 这类轻量序列识别模型。
LPRNet 是专门为车牌识别的轻量卷积网络,输入是 94×24 的灰度图,输出直接是字符序列。它不需要先把字符切分成单个字母,而是用 CTC 对齐解决“不知道每个字符边界在哪”的问题。训练数据从检测阶段裁出来的车牌框里取,每一张已经知道车牌号文本,组织成“图片 + 字符串”的配对即可,不需要字符级标注。这个方案代码量小,训练也快,适合毕设阶段快速出效果。
3.2 检测框裁剪与 LPRNet 预处理:推理代码
拿到 YOLOv5 的检测框后,把原始图片的对应区域裁出来,缩放到 94×24,转灰度图,再归一化送进识别模型。完整流程如下:
import cv2 import numpy as np import onnxruntime as ort # 或用 torch 加载你的模型 def preprocess_crop(crop_bgr): """ 车牌裁剪图 -> 94x24 灰度图 -> (1,1,24,94) 的 float32 张量。 LPRNet 的输入约定是宽 94 高 24,和大多数 OCR 模型不同,别搞反。 """ gray = cv2.cvtColor(crop_bgr, cv2.COLOR_BGR2GRAY) gray = cv2.resize(gray, (94, 24), interpolation=cv2.INTER_AREA) inp = gray.astype(np.float32) / 255.0 inp = np.expand_dims(inp, axis=0) # 加 batch 维 inp = np.expand_dims(inp, axis=0) # 加 channel 维 return inp def recognize_plate(crop_bgr, session, labels): """ session: onnxruntime InferenceSession labels: 字符集合,顺序要和模型训练时一致 返回识别出的车牌字符串 """ inp = preprocess_crop(crop_bgr) input_name = session.get_inputs()[0].name output = session.run(None, {input_name: inp})[0] # (1, num_classes, time_steps) # 去掉 batch 维,按时间步取概率最大的字符下标 pred = np.argmax(output[0], axis=0) # CTC 去重:连续相同字符只保留一个,再删掉空白符 decoded = [] prev = -1 for idx in pred: if idx != prev and idx != len(labels): # 假设最后一个下标是 blank decoded.append(labels[idx]) prev = idx return ''.join(decoded) # 用法示例 session = ort.InferenceSession('lprnet.onnx') labels = open('plate_chars.txt', encoding='utf-8').read().split() crop = cv2.imread('crop.jpg') print(recognize_plate(crop, session, labels))这里有两个值得注意的点:cv2.resize的目标尺寸是(94, 24),第一个参数是宽,第二个是高,写反了会直接导致识别几乎全错;CTC 解码时把连续重复字符合并,是因为车牌里可能连续出现两个相同字符,比如“京A·A123”,模型如果在一个时间步里连续输出同一个字符,CTC 约定只保留一个,真正连续同字符的情况由中间的 blank 分隔开。labels 文件里的字符顺序必须和训练时完全一致,否则解码出的文本全是乱的。
3.3 如果手里没有 LPRNet 权重:退而求其次的字符检测方案
有的读者手头只有 YOLOv5 源码,不想额外训练识别网络,这时候可以用 YOLOv5 单独训练一个字符检测器,检测对象是单个汉字、字母、数字。训练数据准备稍麻烦:需要把每张车牌裁剪图按字符位置切成小块,逐块标注类别。推理时先检测字符框,再按 x 坐标从左到右排序,拼接出车牌号。
这个方案能应付清晰的正视角车牌,但有两个绕不开的弱点。第一,省份汉字类别数量少,各省简称出现的频率差别很大,模型对生僻省份汉字的召回率会明显偏低。第二,车牌存在透视倾斜时,字符框水平排序会出错,需要在排序前做透视矫正,代码量立刻变大。我的建议是:如果时间只有一周,优先用 LPRNet;如果目标是想在论文里多写一个对比实验,字符检测方案可以作为“方法对比”出现,而不是主方案。
4. 用 Python + OpenCV 把检测与识别串成演示程序
4.1 单脚本完成图片和视频流的检测
毕设演示通常分两种:上传一张图片立刻显示结果,或者打开摄像头实时识别。核心逻辑都一样,只是输入源不同。下面是图片和视频都能处理的推理脚本:
import time import cv2 import torch # 加载 YOLOv5 检测模型,路径指向你自己训练好的 best.pt det_model = torch.hub.load('ultralytics/yolov5', 'custom', path='runs/plate/exp/weights/best.pt', force_reload=False) det_model.conf = 0.4 # 置信度阈值,低于此值的框直接丢弃 det_model.iou = 0.45 # NMS 合并阈值,框重叠度过高时只保留一个 def detect_plate(frame_bgr): results = det_model(frame_bgr) return results.xyxy[0].cpu().numpy() # 每行: x1,y1,x2,y2,conf,cls def draw_and_recognize(frame_bgr): boxes = detect_plate(frame_bgr) for x1, y1, x2, y2, conf, cls in boxes: x1, y1, x2, y2 = int(x1), int(y1), int(x2), int(y2) crop = frame_bgr[y1:y2, x1:x2] # 这里调用上一章的 recognize_plate(crop, session, labels) plate_text = recognize_plate(crop, session, labels) cv2.rectangle(frame_bgr, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(frame_bgr, plate_text, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) return frame_bgr # 图片模式 src = cv2.imread('test.jpg') result = draw_and_recognize(src) cv2.imwrite('result.jpg', result) # 视频/摄像头模式 cap = cv2.VideoCapture(0) while True: ok, frame = cap.read() if not ok: break start = time.time() frame = draw_and_recognize(frame) fps = 1.0 / (time.time() - start) cv2.putText(frame, f'FPS: {fps:.1f}', (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) cv2.imshow('plate_recognition', frame) if cv2.waitKey(1) == 27: # Esc 退出 break cap.release() cv2.destroyAllWindows()torch.hub.load第一次运行会联网拉取 YOLOv5 仓库结构,之后的force_reload=False可以避免重复下载。如果答辩现场没网,提前把best.pt和ultralytics/yolov5仓库放在本地,用torch.hub.load(..., source='local')加载。det_model.conf调低到 0.25 可以找回更多漏检,但也会引入错误框;调高到 0.6 则更干净,适合演示场景。视频流里建议把识别部分放在检测框存在时才执行,没有框的帧不跑 LPRNet,能省下大量 CPU 时间。
4.2 给老师看的界面:用 Streamlit 做上传式演示
答辩现场最稳的演示方式不是摄像头实时识别,而是让老师现场上传一张照片,立刻看到结果。摄像头受光线影响大,调试时间不可控,图片演示反而稳定。Streamlit 几十行代码就能做出一个可交互页面:
import streamlit as st import cv2 import numpy as np st.title('车牌识别系统') uploaded = st.file_uploader('上传一张包含车牌的图片', type=['jpg', 'png', 'jpeg']) if uploaded is not None: # Streamlit 上传的是字节流,先转成 numpy 数组再解码成 BGR 图 file_bytes = np.frombuffer(uploaded.read(), dtype=np.uint8) src = cv2.imdecode(file_bytes, cv2.IMREAD_COLOR) result = draw_and_recognize(src) # 复用上一节的处理函数 st.image(cv2.cvtColor(result, cv2.COLOR_BGR2RGB), caption='识别结果', use_column_width=True) else: st.info('请上传图片')这个页面虽然简单,但已经包含“上传文件 → 推理 → 回显结果”整个链路。cv2.imdecode是因为 Streamlit 上传对象不是文件路径,而是二进制的 bytes,直接传给 cv2 需要先转成数组。注意显示给用户前要把 BGR 转成 RGB,否则图片颜色偏蓝偏红,答辩时这个细节很容易被老师看出来。如果时间充裕,可以再加一栏显示检测置信度和单张推理耗时,两个指标能让项目看起来更完整。
4.3 环境安装的血泪经验:先从 cv2 和 numpy 开始
这个项目依赖链是python → numpy → opencv-python → torch → onnxruntime,安装顺序错了会浪费大量时间。我的建议是先用pip install opencv-python numpy onnxruntime,再装 PyTorch。有几点经验值得记一下:opencv-python装完后试运行import cv2,如果报 libGL.so.1 缺失,说明系统缺图形库,Debian/Ubuntu 上执行apt install libgl1 libglib2.0-0能解决;numpy 版本不要贪新,PyTorch 和 onnxruntime 对 numpy 2.x 的兼容至今没有完全统一,装成 1.26.x 会让很多坑直接消失。所有依赖装完后再跑python train.py,能少遇到一半的报错。
5. 避坑:车牌识别毕设最常踩的 5 个翻车点
5.1 训练时 mAP 一直是 0,验证集没有绿色框
现象:训练了 30 个 epoch,loss 在下降,但验证集图片上一片空白,mAP 始终为 0。
原因:八成是标签目录和图片目录不匹配。YOLOv5 的默认约定是训练图片在images/train,标签在labels/train,且标签文件和图片文件必须同名。很多刚接触的人把标签和图片放在同一个目录,或者 images 和 labels 的父目录层级不一致,导致 YOLOv5 找不到对应的 txt 文件。另一个常见原因是plate.yaml里的nc: 1写成了nc: 2,但标签里 class id 只有 0,类别索引错位后损失计算混乱。
解决:先检查runs/plate/exp/下的训练输出,确认训练集图片数量是否和你的数据量一致;再手动打开一个验证集图片对应 txt,确认坐标值在 0~1 之间且宽高为正。我的习惯是一开始只放 5 张图片跑 1 个 epoch,肉眼确认数据加载正常后再全量训练。
5.2 检测框能框住车牌,但识别出来的文本全是乱码
现象:框的位置很准,可输出的字符串完全对不上,甚至出现不存在的字符。
原因:最常见的是 LPRNet 输入尺寸或预处理方式不对。车牌是 94×24,你 resize 成 24×94,网络直接拿到一个“竖着”的车牌,输出自然全歪。其次是 labels 字符集合顺序和训练时不统一,CTC 解码时索引错位,每个字符都对应到了错误的类别上。还有一种隐蔽情况:裁剪区域带了大量背景,灰度化后车牌字符和背景对比度下降,模型把背景边缘也当成字符。
解决:打印一张预处理后的灰度图,确认方向、宽高和明暗分布;把 labels 文件打开,逐个字符核对顺序;裁剪框做 5 到 10 像素的向内收缩,去掉边缘背景干扰。
5.3 网上下载的 yolo5 代码启动就报错
现象:clone 或下载了一个标注为 yolo5 的项目,跑 train.py 时直接抛错,连依赖都装不全。
原因:网上流传的项目文件名写的是 yolo5,本质还是 YOLOv5 的早期分支,很多依赖的是旧版 PyTorch API。这些项目有些已经停止维护,和当前 Python 3.10 以上版本不兼容。
解决:优先从 ultralytics/yolov5 官方仓库拉代码,并锁定一个稳定版本标签。训练前先把requirements.txt里所有包装齐,不要仅凭报错信息安装指定的单个包。网上源码可以拿来读思路,不建议直接作为毕设主干,否则答辩时你连代码为什么这么写都说不清。
5.4 显存溢出,或者 loss 突然变成 nan
现象:训练 5 个 epoch 后显存不够直接 OOM;另一个是在某些 batch 上 loss 变成 nan,之后彻底不降。
原因:OOM 通常是一张图里车牌位置偏小,模型用 640 分辨率训练导致显存占用高,batch 设太大。loss 为 nan 则多是图片损坏或者标注越界,比如某个文本框的坐标超出了图片宽高,归一化之后出现负数或大于 1 的值。
解决:OOM 优先把 batch 减半,再不行把--img 640改成536。loss 为 nan 时写个脚本扫描一遍数据集,过滤掉 cv2 读不出的文件和 label 坐标异常的文件。数据集有时混入了非图片文件,YOLOv5 不会主动报错,只会把 batch 内其他样本的状态带坏。
5.5 摄像头演示卡成 PPT,一帧要 1 秒多
现象:模型在图片上效果不错,但切到摄像头后延迟严重,画面一顿一顿。
原因:CPU 上跑 YOLOv5s 检测一帧约需要 50 到 150 毫秒,加上 LPRNet 识别 30 到 60 毫秒,已经接近实时。如果你用的是 YOLOv5m 或更大模型,CPU 推理轻松超过 300 毫秒,演示体验必然差。另一个隐藏开销是每一帧都创建新变量、重新分配内存,Python 里的临时对象多了也会拖慢速度。
解决:演示时固定输入分辨率,把img_size=416作为推理参数;检测不到车牌时跳过识别步骤;帧率显示放在识别之后,让老师直观看到当前性能。如果还想更快,把 YOLOv5 和 LPRNet 都导出为 ONNX,用 onnxruntime 跑推理,比 PyTorch 直接前向快 20% 左右。答辩现场不要勉强用实时视频,准备 5 张不同场景的测试图片轮流展示,完成度和稳定性反而更高。
6. 最后一公里:分层测试集与验收数据怎么补
训练跑通、演示能出结果之后,毕业设计最容易被导师追问的是“你的系统到底有多准”?这时候不能只拿出 mAP 一个指标搪塞。我建议按场景把测试集分成四堆,每堆收集 50 到 100 张图片,分别记录检测成功率和字符识别准确率:
| 测试场景 | 图片数 | 检测成功率 | 字符识别准确率 | 平均耗时 |
|---|---|---|---|---|
| 白天顺光 | 50 | |||
| 夜间/光线不足 | 50 | |||
| 倾斜/透视车牌 | 50 | |||
| 远距离/小目标 | 50 |
表格里只填自己跑出来的数据,检测成功率看车牌框是否完整包住字符,识别准确率看最终字符串是否和真实车牌一致。做完这四组对比,论文里就能画出一张很有说服力的柱状图,也能明确指出下一步改进方向。我当年做类似的题目时,白天顺光准确率已经到 98%,夜间却掉到 71%,排查发现是夜间图片的裁剪区域过暗,LPRNet 输入灰度图几乎看不见字符。后来在预处理里加了一步简单的自适应直方图均衡化,夜间准确率追到了 88%,这个改进过程成了论文最核心的一章。
最后说一个我自己的习惯:把每个失败的识别案例截图存进一个文件夹,起名叫 failure_cases。答辩时老师问“系统的不足是什么”,直接打开这个文件夹按场景讲,比空谈“数据量不足”具体得多。这套两段式方案,胜在每一步都可以独立验证,把数据整理、检测训练、识别训练、系统集成拆开,任何一步出问题都能快速定位。希望帮到你。
本文还有配套的精品资源,点击获取