简介:基于YOLOv5的车牌检测与识别完整实现,面向计算机视觉开发者、智能交通及安防监控领域的算法工程师,可解决车辆场景下车牌自动定位与字符识别的落地问题。资源共84个文件,压缩包78.16MB,涵盖PyTorch权重文件(.pt/.pth)、Python训练与推理脚本、YOLOv5系列模型配置(yaml)、车牌样例图片(jpg/jpeg/png)以及XML标注文件,从模型定义、数据准备到检测识别均有完整覆盖。包内还包含LPRNet字符识别模块、模型导出脚本和多种网络结构配置,可支持从训练、验证到导出的全流程操作;并配有测试图片和识别结果示例,便于直接运行体验或基于自有数据集进行二次开发与参数调优。已有2057人学习下载,适合希望快速上手YOLOv5车牌识别项目、研究检测与OCR结合方案的读者。
1. 车牌识别不是目标检测的简单加法:先想清楚YOLOv5在这条链上干什么
一辆车从远处开进停车场道闸的识别区,几百毫秒之内要把车牌号码读出来并抬杆放行——这是YOLOv5车牌检测和识别最典型的落地场景。YOLOv5在这里负责的是目标检测:从整张画面里把车牌这个目标框出来;而「框里到底是什么字符」是识别阶段的事,通常由另一段网络或字符级检测完成。这两段是串行关系,检测的框质量直接决定识别的上限,所以很多人拿YOLOv5只做检测、把识别交给OCR,这个分工是合理的。做这个方向的人,多半是手里有摄像头和GPU的工程师,想用yolov5训练自己的车牌数据集、从环境配置到部署完整走一遍。先搞清楚这个分工,后面踩坑才知道坑在哪一层。
2. 环境与数据:用conda把YOLOv5跑起来,车牌数据集怎么选、怎么转格式
先把环境装好。这一步别图省事直接往系统Python里塞包,conda隔离环境是踩出来的习惯。yolov5环境配置的核心是版本对齐:Python版本、PyTorch版本、CUDA版本三者要匹配,否则后面训练时莫名其妙报CUDA错误,排查起来非常消耗耐心。装环境这件事看着简单,但每天都有大量时间耗在「torch装好了但是cuda不可用」这类问题上,所以花十分钟把环境弄干净,比后面反复返工划算得多。
2.1 conda创建yolov5环境与依赖安装
conda create -n yolov5 python=3.9 -y conda activate yolov5 git clone https://github.com/ultralytics/yolov5 cd yolov5 pip install -r requirements.txt第一行创建名为yolov5的conda环境并指定Python 3.9。Python版本不能太低也不能太高:3.8以下很多新版本PyTorch不再支持,3.10以上部分依赖编译时容易报错,3.9是目前兼容性最稳的选择。第三行从GitHub拉取YOLOv5源码,官方仓库一直维护了比较久,拉默认分支即可。最后一行安装requirements.txt里锁定的依赖,包含torch、torchvision、opencv-python、numpy等核心库。
装完之后验证一下环境:
python -c "import torch; print(torch.__version__, torch.cuda.is_available())"这行命令确认PyTorch装好且CUDA可用。输出应该类似2.1.0 True,如果后面是False,说明torch装成了CPU版或者CUDA驱动没对上。常见做法是去PyTorch官网用pip安装对应CUDA版本的torch,而不是靠requirements.txt里的默认源。conda yolov5环境装好后,这一层基本不会在训练阶段再出问题,后面所有操作都在这套环境里做,别混用。
2.2 车牌数据集怎么选:CCPD、CRPD还是自己标
YOLOv5训练自己的数据集,第一步是解决数据来源。车牌识别领域公开数据集主要是CCPD(中国城市停车场数据集)和CRPD(中国车牌数据集)。
CCPD是国内停车场场景采集的,特点是场景多样,包含蓝牌和绿牌(新能源),有大量倾斜、逆光、夜间样本。它每个样本存储的是整张停车场画面,车牌位置以json形式给出,适合做YOLOv5检测训练。CRPD是另一个车牌检测与识别数据集,覆盖省份更多,有些版本直接给了车牌字符级标注,适合做识别阶段的训练。
如果项目要求是特定场景——比如只有地下车库的暖光环境,或者只有单位门口的单车道——公开数据集其实不太够用,因为停车场公开数据和你的摄像头视角差异很大。我一般建议先拿CCPD跑通流程,再采集自己场景的几百张图做增量训练。标注工具用LabelImg或者x-anylabeling都可以,前者老牌稳定,后者对旋转框支持更好。车牌经常是倾斜的,这一点后面会专门讲。
2.3 VOC标注转YOLO格式:转换脚本与四个边界坑
LabelImg默认导出VOC XML格式,而YOLOv5需要的是txt格式:每行一个目标,格式为class_id center_x center_y width height,坐标是相对图片宽高的归一化值。转换脚本是绕不开的一步。
import xml.etree.ElementTree as ET import os def xml_to_yolo(xml_path, out_dir, class_names): tree = ET.parse(xml_path) root = tree.getroot() img_w = int(root.find('size/width').text) img_h = int(root.find('size/height').text) lines = [] for obj in root.iter('object'): name = obj.find('name').text cls_id = class_names.index(name) box = obj.find('bndbox') x1 = float(box.find('xmin').text) y1 = float(box.find('ymin').text) x2 = float(box.find('xmax').text) y2 = float(box.find('ymax').text) # 转成YOLO格式:中心点坐标 + 宽高,归一化到[0,1] cx = (x1 + x2) / 2.0 / img_w cy = (y1 + y2) / 2.0 / img_h w = (x2 - x1) / img_w h = (y2 - y1) / img_h lines.append(f"{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") os.makedirs(out_dir, exist_ok=True) txt_name = os.path.splitext(os.path.basename(xml_path))[0] + '.txt' with open(os.path.join(out_dir, txt_name), 'w') as f: f.write('\n'.join(lines))这段脚本把VOC XML里的目标框转换成YOLO训练要的txt标注。核心逻辑是把左上角(x1,y1)、右下角(x2,y2)的坐标换算成中心点(cx,cy)和宽高(w,h),再除以图片宽高做归一化。cls_id是类别在names里的下标,车牌检测只有一个类别plate,所以取值恒为0。
转换时有四个边界坑要留意:
第一,img_w和img_h取的是xml里size节点的值,不是图片实际尺寸。如果标注工具版本不一致,xml里的宽高和图片实际像素对不上,坐标就会整体偏移,模型训练出来检测框全部靠左上角。解决办法是转换前用PIL或cv2重新读一遍图片尺寸,以实际像素为准。
第二,XML里的坐标可能带小数点,也可能是负数。带小数点还好,float()直接解析;负数一般是标注工具误操作产生的越界框,转换时要做clip操作,把坐标约束在[0,img_w]和[0,img_h]内。
第三,标注文件里可能存在<difficult>标记的困难样本,这类目标通常很模糊,直接混进训练集会拉低mAP。转换时加个判断,difficult=1的跳过。
第四,回车换行符。Linux和Windows混用标注文件时,txt里可能出现\r字符,训练时读标注会报格式错误。用open写入时指定newline='\n',或者转换后统一用dos2unix清理一遍。
数据集目录结构也要按YOLOv5约定摆好:train和val两个文件夹,下面分images和labels两个子目录。images目录放图片,labels目录放对应的txt。YAML文件里分别指向train和val的images路径即可。
3. 训练车牌检测模型:yolov5超参数与训练日志解读
数据就绪之后,开始训练。这一章讲三个东西:训练命令怎么写、yolov5超参数怎么调、训练日志和mAP怎么看。这一层直接决定检测模型能不能用,不能盲目跑默认参数。
3.1 训练命令与数据集YAML
# plate.yaml train: /home/user/plate_dataset/train/images val: /home/user/plate_dataset/val/images nc: 1 names: ['plate']先写数据集配置文件。train和val字段指向图片目录,YOLOv5会从同级的labels目录自动找标注txt。nc是类别数,这里只有车牌一个类,写1。names是类别名列表。
python train.py --data plate.yaml --weights yolov5s.pt --epochs 300 --batch-size 16 --img 640 --device 0这是最常用的车牌检测训练命令。--weights yolov5s.pt表示加载YOLOv5s预训练权重做迁移学习,不要小看这一步,从零训练在车牌这种单类目标上收敛极慢,mAP也很难上去。--img 640是输入分辨率,车牌通常占画面比例不大,建议至少640起步。--batch-size 16要看显存,12G显存用yolov5s跑640分辨率,16这个值刚好。
--device 0指定第一块GPU。如果只有CPU,去掉这个参数或者写--device cpu,但训练速度会慢一到两个数量级,300个epoch等不起。
3.2 yolov5超参数必调的5个位置
YOLOv5的超参数在data/hyp.scratch.yaml这类文件里,训练时刻用--hyp指定。车牌检测这种任务,我任务最值得调的5个参数是:
lr0: 0.01 # 初始学习率 lrf: 0.2 # 最终学习率 = lr0 * lrf warmup_epochs: 3 # 预热epoch数 fl_gamma: 0.0 # focal loss强度 mosaic: 1.0 # mosaic增强概率lr0是初始学习率,预训练权重迁移时0.01是安全的起点,如果loss前20个epoch就震荡,降到0.005再看。warmup_epochs预热让模型前几个epoch用很小的学习率慢慢进入状态,车牌数据集如果只有几千张,warmup可以适当加长到5。fl_gamma是focal loss的gamma参数,用于解决正负样本不均衡。车牌目标在画面里通常只有一个,不那么极端,默认0.0即可;但如果你自己做数据时每张图平均不到0.5个车牌目标(很多空背景图),可以调到0.5。
mosaic是马赛克增强,把4张图拼在一起训练,对小目标检测尤其有效。车牌属于中小目标,mosaic保持默认1.0。但要小心:车牌本身是矩形且字符密集,mosaic时如果拼接边恰好切到车牌,会产生大量切了一半的车牌正样本,模型容易学到「半个车牌也能算正例」的坏习惯。真遇到识别阶段经常把半截车牌框出来,就把mosaic降到0.5甚至0.3,这是最直接的后悔药。
3.3 训练日志与mAP怎么看
训练结束后,终端会输出每个类别的P(精确率)、R(召回率)、mAP50和mAP50-95。对车牌检测场景,我最看重的是R而不是mAP50-95,因为漏检一辆车意味着道闸不抬杆,比误检更影响体验。R要做到95%以上才敢上线。
训练过程中还有一个容易忽略的细节:results.png里val_box_loss曲线。如果val的box loss在epoch 200之后还在缓慢下降,说明数据还有学习空间,可以加epoch。如果val loss已经平稳甚至反弹,再多的epoch只会过拟合到训练集,此时看best.pt和last.pt的差别就知道模型有没有飘——best.pt在验证集上表现最好,last.pt是最后一个epoch的权重,两者mAP差距超过2个点,说明后半段训练已经在过拟合。
另一个常被忽略的文件是labels.png,它可视化标注分布:框中心点散布在图像中央区域,宽高集中在一个范围。如果看到标注框的width/height出现离群值,比如某个框宽度归一化值超过0.5,说明有标注错的样本混进来了,优先清洗数据而不是调参——训练数据里的脏框,靠调参永远调不干净。
4. 检测完之后怎么读字符:车牌识别两条可行路线
YOLOv5检测模型输出的是车牌位置框,接下来要把框里的字符读出来。这一步是车牌识别(LPR),常见做法有两条:一是检测车牌后裁剪图像,交给LPRNet这类轻量OCR网络直接输出字符串;二是再用YOLOv5训练一个字符级检测模型,检测出每个字符的位置后按顺序拼接。两条路线在工程上都有大量落地,选哪条取决于你的字符集规模和部署算力。
4.1 路线一:YOLOv5检测车牌 + LPRNet字符序列识别
LPRNet是专门为车牌识别设计的轻量网络,输入车牌的裁剪图(比如宽高94x24),输出是字符串序列,不需要预先做字符分割。它内部用CTC损失做序列建模,训练数据是字符级别的标注(省份汉字、字母、数字)。常见做法是:检测模型每帧输出车牌框,把框内区域用OpenCV裁出来,resize到LPRNet的输入尺寸,前向一次直接得到「京A12345」这样的结果。
import cv2 import torch # plate_det是训练好的YOLOv5检测模型,lprnet是训练好的识别模型 def detect_and_recognize(img, plate_det, lprnet): results = plate_det(img) # YOLOv5前向,得到车牌框 for det in results.xyxy[0]: x1, y1, x2, y2, conf, cls = det.cpu().numpy() if conf < 0.6: continue crop = img[int(y1):int(y2), int(x1):int(x2)] # 裁剪车牌区域 crop = cv2.resize(crop, (94, 24)) # 对齐LPRNet输入尺寸 crop = torch.from_numpy(crop).permute(2, 0, 1).float() / 255.0 plate_text = lprnet(crop.unsqueeze(0)) # 前向得到字符串 print(f"plate: {plate_text}, conf: {conf:.2f}")这段代码把检测和识别串起来。核心在于resize到(94,24)这一步:LPRNet的输入设计是宽94高24固定尺寸,字符长宽比会被拉伸,但CTC训练时已经见过这种拉伸,所以问题不大。裁剪前建议先做一次透视矫正,框是斜的时候直接resize会拉伸变形,识别准确率明显下降,矫正方法在4.3节讲。conf阈值取0.6是经验值,道闸场景漏一拍比误抬杆好,阈值可以提到0.7。
4.2 路线二:用YOLOv5做字符级检测后拼接
如果不想额外维护一个LPRNet网络,也可以把YOLOv5用在字符粒度上:把车牌图里每个字符当成一个目标类,类别是「省份汉字+字母+数字」,训练一个字符检测器。识别时先把大图上的车牌框出来,裁剪,再对裁剪图跑字符检测,最后按x坐标从左到右排序拼接。
python train.py --data char.yaml --weights yolov5s.pt --epochs 200 --batch-size 32 --img 320char.yaml里的nc是字符类别数,常见设置是31个省份汉字+24个字母(I和O通常不用)+10个数字,共65类。输入分辨率降到320,因为字符在裁剪图里已经是大目标,不需要640。这条路线对排布不标准的车牌(比如新能源车牌的8位字符)更友好,LPRNet在长字符序列上偶尔会丢字符,字符级检测配合排序拼接则不会。
代价是工程链路变长:要按x坐标排序、要处理粘连字符的NMS合并,还要处理检测框重叠时字符被重复计数的问题。我见过不少项目在这条路线的后处理上加了一堆规则,最后效果不稳定。纯从省心角度,数据规模不大时优先考虑LPRNet;只有字符集里有特殊字符、或者车牌样式特别不规整时,才值得上字符级检测。
4.3 车牌透视矫正:识别前的一步关键预处理
无论走哪条识别路线,裁剪出来的车牌区域都可能带透视形变。道闸摄像头的安装角度决定了车牌不会总是正对镜头,尤其是车停在坡道或车头偏离时,车牌是一个平行四边形。直接送进识别网络,字符被压缩,识别率掉得厉害。
import cv2 import numpy as np def rectify_plate(crop_gray): # 边缘检测找到车牌四边形的四个角点 edges = cv2.Canny(crop_gray, 100, 200) contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) cnt = max(contours, key=cv2.contourArea) # 车牌是四边形,用approxPolyDP拟合出四个角 epsilon = 0.02 * cv2.arcLength(cnt, True) approx = cv2.approxPolyDP(cnt, epsilon, True) if len(approx) != 4: return crop_gray # 拟合不出四边形,原样返回 # 按左上、右上、右下、左下排序后做透视变换 pts = approx.reshape(4, 2).astype(np.float32) dst = np.float32([[0, 0], [94, 0], [94, 24], [0, 24]]) M = cv2.getPerspectiveTransform(pts, dst) return cv2.warpPerspective(crop_gray, M, (94, 24))这段代码用Canny边缘检测拿到车牌的边缘轮廓,再用approxPolyDP把轮廓拟合成四边形,最后用getPerspectiveTransform计算透视变换矩阵并矫正。核心是透视变换:把任意四边形映射到固定尺寸(94,24)的矩形。
提示:矫正的目标矩形不要设置成正方形,车牌的长宽比大约是3.7:1,保持这个比例字符才不会被拉伸。
这一步对识别效果的提升非常明显。我第一次调通LPRNet时,直接拿倾斜车牌测试识别率只有70%出头,加上矫正之后到96%左右。当时还在怀疑是不是模型训练问题,后来才发现是前处理少了这一步。
5. YOLOv5车牌项目避坑指南:5个高频问题的现象、原因与解决
从数据到模型再到部署,车牌场景有自己特有的坑。这一章写5个实测高频问题,每条按「现象→原因→解决」的结构说。
5.1 蓝牌绿牌混训导致漏检率翻倍
现象:训练集里蓝牌和绿牌各一半,训练完成后蓝牌检测正常,绿牌漏检严重。原因:绿牌是渐变底色,字符对比度比蓝牌低,模型在底层特征上更难区分;另外CCPD数据集中蓝牌数量远大于绿牌,类别内样本不均衡是常见的翻车点。解决:把绿牌数据单独抽出来数一遍,保证训练集里绿牌数量不低于总样本的30%;不够就做数据增强,把蓝牌样本通过HSV变换转成偏绿底色调加进训练集,这是懒人办法,但实测有效。
5.2 车牌倾斜时检测框和字符识别一起出问题
现象:检测框框住了车牌但框得很松,边缘有多余背景;识别阶段字符大量错乱,尤其两端的汉字和最后的数字。原因:标注框是正矩形,倾斜车牌被框成斜矩形,resize的同时把字符压扁了,识别网络看到的字符和训练时的形状分布对不上。解决:采集数据时优先用带旋转框能力的标注工具,导出旋转框后扩展成YOLO的正矩形框;或者训练后用4.3节的透视矫正兜底。注意矫正解决的是识别错乱,解决不了检测框松的问题,检测框松要从标注侧改。
5.3 训练loss前20个epoch不降反升
现象:loss从1.x涨到2.x,然后一直不动。原因:最常见是学习率过大。YOLOv5默认lr0=0.01是配合COCO这种大数据集的,车牌数据量小,0.01偏大。解决:把lr0改到0.005,warmup_epochs从3提到5。如果还是不行,用--weights ''从零训练对比,如果从零训练loss反而降得更快,说明预训练权重的特征跟车牌场景差太多,那就老老实实从零训练,或者换yolov5n这种更轻的预训练模型。
5.4 远距离小目标车牌总是漏检
现象:道闸前3米内车牌秒检,距离超过10米就开始闪烁漏检。原因:摄像头画面里远距离车牌只有几十个像素宽,640分辨率下特征太弱。解决:训练时把--img从640提高到1280,检测时也按1280推理。显存不够就在训练和推理时用--rect保持原图比例,减少无效padding。另外可以从yolov5s换到yolov5m,深度增加对小目标的特征表达能力有实打实的提升。
5.5 识别阶段省份汉字误识别成其他字
现象:「京」被识别成「青」或「冀」,「湘」被识别成「浙」,频率还不低。原因:LPRNet这类OCR网络的训练数据里,省份汉字类别分布很不均衡,北京、山东、广东的样本多,青海、湖南的样本少,模型对低频汉字基本是瞎猜。解决:第一条路是训练时对低频省份做过采样,把「青」「湘」这类样本重复加载到和北京一个量级;第二条路是不训练省份汉字,识别结果里前两位只做校验不参与业务判断,很多停车场系统根本没用到省份信息,可以砍掉。字符集缩小之后识别率会整体上升,这个是立竿见影的。
6. 部署到边缘设备:导出ONNX与TensorRT加速的验证习惯
模型在GPU上跑通了,下一步是部署。车牌检测和识别的典型部署目标是道闸工控机或边缘盒子,NVIDIA平台用TensorRT,树莓派5这类ARM平台用ONNX Runtime,导出流程大同小异。
python export.py --weights best.pt --include onnx --img 640YOLOv5官方就带导出脚本,--include onnx导出ONNX格式,--img 640必须和训练时保持一致。导出后用onnxruntime跑一次推理,对比原模型和ONNX模型的输出差异。差异超过0.1个置信度阈值,就要检查是否开了--dynamic动态输入。车牌场景输入尺寸是固定的,关掉动态维度能让模型更稳定。
TensorRT加速时有一个关键验证习惯:用同一段测试视频跑原模型和TensorRT引擎,逐帧对比检测框坐标。坐标偏差超过2个像素就要警惕量化精度掉了,此时把FP16去掉改用FP32,视觉任务上FP16的加速收益远小于精度损失风险。
部署验收不要只看识别率,要看整条链路的端到端耗时。道闸场景从图像输入到输出字符串,50毫秒以内是可接受的,超出200毫秒就要拆链路逐段计时:图像采集、检测前处理、检测推理、裁剪矫正、识别推理、后处理拼接,哪段耗时长就优化哪段。我见过很多项目栽在识别推理上——LPRNet虽轻,但如果每帧都跑两次,依然会吃掉算力预算。
车牌识别项目的调优顺序我一直按「先数据、再输入尺度、最后引擎」来。数据错了调什么都没用,输入分辨率不够再好模型也白搭,引擎优化永远放在最后。这个习惯帮我避掉了不止一次返工。希望帮到你。
本文还有配套的精品资源,点击获取