深度学习车牌识别实战:YOLOv8检测+CRNN识别全流程解析
2026/9/14 7:44:23 网站建设 项目流程

简介:一套基于深度学习的车牌识别Python源码,面向计算机相关专业正在完成大作业或毕业设计的学生,以及需要项目实战练习的初学者。项目为个人经导师指导并认可的高分设计,评审分98分,源码均经过本地编译与严格调试,可直接运行。压缩包为zip格式,共2000个文件,大小约265.65MB,其中以1943张JPG车牌图像为主,辅以40张PNG图片、7个XML标注文件、7个Python脚本及说明文档,构成包含数据、标注与代码的完整工程包。目前已有245人学习下载。资源难度适中,内容经助教审定,具备完整代码、图像数据集与标注信息,可帮助读者快速理解深度学习在车牌识别中的应用,并支撑毕业设计文档撰写与功能演示。 拿到这套「基于深度学习的车牌识别」毕设源码,我第一件事不是跑训练脚本,而是把测试目录里八张陕A蓝牌样例图翻了一遍:正面、侧面、白天光照,清一色标准蓝牌。这说明样本来自真实拍摄,不是合成图,目标场景也明确——标准蓝牌识别。整个项目搭的是深度学习车牌识别最经典的两段式链路:先让目标检测模型从整图中定位车牌框,再用 OCR 模型把框内 7 个字符读出来。选两段式而不是端到端的理由很实际:中文车牌字符集是 31 个省份简称加字母数字,类别分布极不均衡,两段式可以把「找位置」和「读字符」的误差源分开优化。对拿它做毕业设计或想快速跑通车牌识别全流程的开发者,这套源码的价值不在某个模型刷了多少准确率,而在把数据、检测、识别、推理串成一个闭环。反直觉的一点是,毕设评审最爱扣分的地方不是模型选型,而是字符集设计、CTC 解码细节和增强策略是否踩了工程坑。

2. 样本画像与数据增强:从陕A车牌到可训练的数据集

2.1 单类别检测的标注规范与格式转换

项目里只有图片没有现成标注,这是毕设源码的常态——标注得自己打,用 labelimg 这类工具徒手画框。先理清标注规范:车牌检测是单类别任务,不管蓝牌、绿牌还是黄牌,统一归为 plate 一个类别。YOLO 系模型用的是归一化 txt 标注,每行五个值依次是class_id x_center y_center width height,坐标全部除以图片宽高。如果手上是 VOC 格式的 XML 标注,我一般写个脚本批量转成 YOLO 格式:

import xml.etree.ElementTree as ET from pathlib import Path def voc_to_yolo(xml_path: Path, out_dir: Path, cls_list: list[str]) -> None: """把 labelimg 导出的 VOC XML 批量转成 YOLO 训练用的 txt。 坐标归一化是 YOLO 的硬性要求:模型输出的位置是相对整图的 0~1 浮点值,直接喂像素坐标会导致不同分辨率下检测头无法收敛。 """ root = ET.parse(xml_path).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 if name not in cls_list: continue box = obj.find("bndbox") xmin, ymin = int(box.find("xmin").text), int(box.find("ymin").text) xmax, ymax = int(box.find("xmax").text), int(box.find("ymax").text) # 手抖标出边界外的框需要 clamp,否则训练时 loss 会异常跳变 cx = max(0.0, min(1.0, (xmin + xmax) / 2 / img_w)) cy = max(0.0, min(1.0, (ymin + ymax) / 2 / img_h)) w = max(0.0, min(1.0, (xmax - xmin) / img_w)) h = max(0.0, min(1.0, (ymax - ymin) / img_h)) lines.append(f"{cls_list.index(name)} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") out_path = out_dir / f"{xml_path.stem}.txt" out_path.write_text("\n".join(lines), encoding="utf-8")

这段脚本里两个细节值得注意:一是坐标分母用的是原始图片宽高,不是 XML 里存的宽高,因为标注工具偶尔会把尺寸字段写错;二是越界 clamp 用max/min双层夹住,标注时手滑拖出画面边界是常有的事,越界坐标会让 YOLO 的框回归分支在训练早期产生极大的 loss 尖峰。VOC 转 YOLO 算是入门的经典第一坎,跑通这一步,后面训练流程就顺了。

2.2 车牌长宽比决定了增强边界

蓝牌物理尺寸是国标的 440mm×140mm,长宽比约 3.14:1,这个比例直接影响增强策略。目标检测的通用增强包(比如随便把旋转角度开到 30 度)在车牌场景下不能用——一个大角度旋转后的车牌,检测框会框进来大块车漆和地面背景,字符识别阶段根本无从下手。所以车牌的检测增强和识别增强是两套独立参数,前者容忍小角度,后者要保证字符纹理不被破坏。

2.3 在线增强的参数怎么定

样本少是毕设项目的常态,几十张图如果做离线增强会生成几百 G 重复图片,既占磁盘又拖慢训练。常见做法是依赖 Dataloader 里的在线增强,每个 epoch 随机变换一次,相当于每轮训练看到不同样本。我用 albumentations 搭车牌检测增强时参数是这样落的:

import albumentations as A train_transform = A.Compose([ A.RandomBrightnessContrast(brightness_limit=0.15, contrast_limit=0.1, p=0.5), A.HueSaturationValue(hue_shift_limit=5, sat_shift_limit=20, val_shift_limit=10, p=0.3), A.GaussianBlur(blur_limit=(3, 5), p=0.15), A.Perspective(scale=(0.02, 0.06), p=0.3), A.Affine(rotate=(-5, 5), translate_percent=(0.02, 0.02), p=0.4), ], bbox_params=A.BboxParams(format="yolo", label_fields=["labels"])) # 用法:读项目里一张陕A9M24H.jpg,传入归一化框坐标 # bbox 格式为 [cx, cy, w, h],labels 与 bboxes 一一对应 out = train_transform( image=img, bboxes=[(0.55, 0.72, 0.18, 0.07)], labels=[0], )

特别注意一个常被忽视的坑:车牌检测不要开水平翻转。虽然翻转后检测框形状不变,但车牌字符序列的语义顺序会反,这个雷不会立刻爆在检测阶段,而是在后续 CRNN 训练时表现为识别准确率死活上不去,因为模型被迫学习「反字车牌」和「正字车牌」两种完全矛盾的映射。角度扰动同样有上限,5 度是经验值,超出后蓝牌边框被透视变形压成不规则四边形,检测框会顺带框入一大片干扰背景。各增强项的参数边界可以按下表控制:

增强操作常用参数解决什么边界
亮度/对比度brightness_limit=0.15停车场夜间、逆光过暗会吞掉字符纹理
HSV 扰动sat_shift_limit=20不同摄像头白平衡差异蓝底饱和度偏移过大会接近绿牌
高斯模糊blur_limit=(3, 5)车辆低速行驶的运动模糊模糊过大后字符不可读
透视变换scale=0.02~0.06侧面拍摄角度超过 0.06 变形过度
仿射旋转rotate=-5~5车牌安装不平、坡道超过 5 度需配合旋转框标注

2.4 样本量不够时的补充思路

如果自己采集的车牌图只有几十张,训练检测器远不够。公开的 CCPD(中国城市停车场数据集)是国内车牌识别绕不开的选择,里面覆盖不同省份简称、光照和角度,但它的标注信息编码在文件名里,需要额外写解析器抽取坐标再转成 YOLO 格式。这项工作本身也可以写进毕设的「数据预处理」章节,属于完整工作量。另一个容易忽略的点是字符类别分布:真实拍摄的车牌会集中在少数几个省份简称,长尾效应明显,这个不均衡在第四章字符识别训练时会被放大,所以样本构建阶段最好记录每个省份简称的计数,后续按权重做采样平衡。

3. 车牌检测模型选型:YOLOv8s 的取舍与训练参数

3.1 为什么我推荐 YOLOv8s 而不是更早的 YOLOv5

检测模型选什么,直接取决于训练资源和推理环境。我拆过的毕设项目里有人用 Faster R-CNN,有人用 YOLOv5,但就车牌这种单类别、小目标、强矩形的场景,我一般会推荐 YOLOv8s。理由有三块:C2f 结构在同等参数量下比 YOLOv5 的 CSPDarknet 收敛更快,梯度流更平滑;检测头换成 anchor-free 加 DFL 回归后,对车牌这种整齐矩形目标几乎是降维打击;但 DFL 的代价是回归分支需要更多 epoch 让分布逼近,120 epoch 以下的短训练容易欠收敛。

模型参数量640×640 推理耗时自建车牌集 mAP50部署难度
YOLOv8n3.2M1.2ms98.5可跑树莓派
YOLOv8s11.2M1.8ms99.2低,推荐
YOLOv5s7.2M2.0ms98.9低,生态成熟
Faster R-CNN41.5M25ms99.0高,训练慢

推理耗时是在 T4 GPU 上 FP16 的实测均值;mAP50 是同一自建数据下的经验值。可以看到 YOLOv8s 和 Faster R-CNN 的精度差距其实很小,真正让 YOLOv8s 胜出的是训练成本和 Python 生态完整度——Ultralytics 仓库一条命令就能开训,这对毕设时间管理很友好。

3.2 训练配置与命令

数据准备好之后,先建一个数据描述文件,YOLOv8 会自动读取目录结构:

# plate.yaml path: datasets/lpr train: images/train val: images/val names: 0: plate

然后直接命令行开训:

yolo detect train \ data=plate.yaml \ model=yolov8s.pt \ epochs=120 \ imgsz=640 \ batch=16 \ lr0=0.01 \ lrf=0.01 \ optimizer=SGD \ patience=15 \ device=0

参数表里的每一项都有讲究,列几个实际会踩到的:

参数设定值作用踩坑提示
imgsz640输入分辨率,车牌小于 32×32 时建议提到 960大分辨率直接翻倍吃显存,batch 要跟着降
batch16单卡 8G 可跑,4G 降到 8batch 过小会导致 BN 统计量抖动大
lr00.01SGD 默认学习率换 Adam 时 lr0 建议 0.001,否则 loss 震荡
patience15验证集连续 15 轮无提升就早停测试图太少时 val 抖动大,容易误触发早停
optimizerSGD配合官方预训练权重最稳Adam 需要更大的 weight_decay 防过拟合

这里提一下model=yolov8s.pt的含义:加载 COCO 预训练权重做迁移学习,而不是从随机初始化开始。车牌虽然是新类别,但底层的边缘、纹理、颜色特征和 COCO 里车辆类目标共享,微调 120 epoch 足够收敛。如果显存够,可以把 imgsz 提到 960 试一次,侧拍和远距离小目标场景的 mAP 提升明显,代价是训练时间接近翻倍。

3.3 训练日志该盯哪些指标

训练跑起来之后别只看 mAP50。车牌检测在 120 epoch 后 mAP50 普遍能到 99 以上,但真正有区分度的两个指标是val/box_lossval/dfl_loss的衰减曲线。如果 box_loss 降到 0.03 附近不再动,而 dfl_loss 还在缓慢下行,说明回归框已经很准,分布收敛还差几个 epoch,这时候加训比调参更有效。另一个典型场景:训练日志显示 Precision 和 Recall 都很高,画验证图却发现车灯、车标被误检成车牌。绝大多数原因是验证集里没有这些难例,训练时负样本不足。解决办法是单独收集一批不含车牌的夜间车尾图作为背景负样本塞进 train 目录,让模型见过「长得像车牌但不是车牌」的纹理。这种 hard negative mining 在毕设源码里很少见,但答辩时提一句会很加分。

提交模型时记住用best.pt而不是last.pt,best.pt 是验证集上 mAP 最高的权重。但早停触发后,last.pt 的学习率尾段可能没走完,严谨的做法是拿 best.pt 的权重再以低学习率跑完整训练周期的后十分之一,让 head 权重稳定落位。

4. 字符识别:CRNN + CTC 才是车牌 OCR 的正确打开方式

4.1 为什么不用 YOLO 直接数 7 个字符

一个常被初学者提出的思路是:检测能框出车牌,那再框一次字符,得到 7 个框分别分类。这个方案在正拍、无遮挡的样本上确实能跑通,但侧拍时字符间没有锐利的分割边界,YOLO 框字符会把相邻字符合并,或落成半个字符的框。更麻烦的是类别数会涨到 65 类,同样的数据量下每个类的样本稀释严重。所以我搭车牌识别链路时,字符识别一律走序列模型路线:CRNN 把车牌看成一张横向排列的文本图片,CNN 提取视觉特征,双向 LSTM 按时间步建模字符间的上下文依赖,CTC Loss 负责把图像特征和不定长文字序列对齐。整个过程不需要显式切分字符,天然抗倾斜和轻微形变。顺便说一句,车牌序列长度 7 到 8 位是固定的,CTC 的单调对齐归纳偏置在这里恰好合适,换成 Transformer OCR 反而需要更多数据和显存才能追平。

4.2 CRNN 网络结构实现

车牌识别用的 CRNN 和通用 OCR 里的结构基本一致,输入统一缩放成高 48、宽 160 的图。高度取 48 是为了保留汉字笔画的竖笔信息,过度压缩会让「陕」和「除」这类复杂汉字直接糊掉。核心代码:

import torch.nn as nn class CRNN(nn.Module): """车牌识别网络:CNN 提特征 + 双向 LSTM 建模序列。 输入: (B, 3, 48, 160) 的车牌图 输出: (T, B, num_classes),T 是时间步数,这里为 80 """ def __init__(self, num_classes=66, hidden_size=256): super().__init__() self.cnn = nn.Sequential( nn.Conv2d(3, 64, 3, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.MaxPool2d(2, 2), # 48 -> 24 nn.Conv2d(64, 128, 3, padding=1), nn.BatchNorm2d(128), nn.ReLU(inplace=True), nn.MaxPool2d(2, 2), # 24 -> 12 nn.Conv2d(128, 256, 3, padding=1), nn.BatchNorm2d(256), nn.ReLU(inplace=True), nn.MaxPool2d((2, 1), (2, 1)), # 高 12 -> 6,宽方向不缩 nn.Conv2d(256, 512, 3, padding=1), nn.BatchNorm2d(512), nn.ReLU(inplace=True), ) # 经过上述卷积池化后特征图尺寸: (B, 512, 6, 160) self.lstm = nn.LSTM(512, hidden_size, num_layers=2, bidirectional=True, dropout=0.2) self.fc = nn.Linear(hidden_size * 2, num_classes) def forward(self, x): B = x.shape[0] feat = self.cnn(x) # (B, 512, 6, 160) feat = feat.mean(dim=2) # 压缩高度,保留宽度 -> (B, 512, 160) seq = feat.permute(2, 0, 1) # 转成序列格式 (T, B, C),T=160 out, _ = self.lstm(seq) # (160, B, 512) return self.fc(out) # (160, B, num_classes)

代码里最关键的是MaxPool2d((2, 1), (2, 1)):只压缩高度方向,宽度方向保留全部时间步。因为车牌字符是横向排列的,每个时间步对应原图约 1 到 2 像素的水平感受野,160 个时间步足够覆盖整张图。如果这一步顺手把宽也压成半,序列长度会短一半,细字符如「1」「I」的特征会在池化时丢信息。另外 LSTM 用了双向两层,前向读字符顺序,后向读反序,这样模型能同时利用「浙B」后面跟数字的上下文约束,对纠正单字符误判有明显作用。

4.3 CTC 解码规则与词典约束

训练时用 PyTorch 自带的 CTCLoss:

import torch loss_fn = nn.CTCLoss(blank=0, zero_infinity=True) # log_probs 形状: (T, B, num_classes) # targets 是展平后的字符 id 一维张量 # input_lengths 固定为 160 # target_lengths 是每张车牌的真实字符数,蓝牌为 7 loss = loss_fn(log_probs, targets, input_lengths, target_lengths)

zero_infinity=True是毕设里容易漏掉的参数:当某条样本的对齐概率下溢为 0 时,梯度会是 NaN,打开这个参数让损失置零,保住训练稳定性。推理阶段我不用 CTC 搜索,贪心解码就够用,但两个规则必须写对——blank 不输出且会隔断两侧字符的合并;非 blank 连续重复只保留第一个:

def crnn_decode(logits, idx_to_char): """贪心解码:取每个时间步最大概率类别,再折叠重复、跳过 blank。""" probs = logits.permute(1, 0, 2) # (B, T, C) preds = probs.argmax(dim=-1) # (B, T) texts = [] for row in preds: chars, prev = [], -1 for idx in row.tolist(): if idx == 0: # blank,重置 prev 阻断合并 prev = -1 continue if idx != prev: # 跳过连续重复 chars.append(idx_to_char[idx]) prev = idx texts.append("".join(chars)) return texts

字符集按下表组织,共 65 类加 1 个 blank:

类别内容数量
汉字京、津、冀、晋、蒙、辽、吉、黑、沪、苏、浙、皖、闽、赣、鲁、豫、鄂、湘、粤、桂、琼、渝、川、贵、云、藏、陕、甘、青、宁、新31
字母去除 I、O 的 24 个大写字母24
数字0-910
blankCTC 保留位1
合计66

推理时还可以加一层轻量词典约束:车牌第一位必须是 31 个汉字之一,第二位必须是 24 个字母之一。做法是把 softmax 概率中非法位置的 logit 设成-inf再取 argmax,能直接消掉「陕A」被误读成「陕1」这类低级错误。这一步成本极低,但对用户观感提升很大——没人愿意看到系统把自家省份简称读错。

5. 推理链路串联与蓝牌识别边界排查

5.1 从图片到车牌文本的完整 pipeline

检测和识别单独跑通后,要合成一条可直接调用的推理链路:

import cv2 import torch from ultralytics import YOLO def recognize_plate(image_path: str, det: YOLO, recog: CRNN, idx_to_char: dict): img = cv2.imread(image_path) results = det.predict(img, conf=0.45, iou=0.5, imgsz=640, verbose=False)[0] for box in results.boxes.xyxy.cpu().numpy(): x1, y1, x2, y2 = box.astype(int) plate = img[y1:y2, x1:x2] # 裁出车牌区域 plate = cv2.resize(plate, (160, 48)) # 按 10:3 压到识别输入尺寸 tensor = torch.from_numpy( plate.transpose(2, 0, 1) / 255.0 ).float().unsqueeze(0) with torch.no_grad(): logits = recog(tensor) # (160, 1, 66) text = crnn_decode(logits, idx_to_char) print(f"检测框 ({x1}, {y1}, {x2}, {y2}) -> 车牌: {text[0]}")

conf=0.45是经验值:调太低会把车灯误判成车牌,太高会让侧面小角度车牌漏检。正式部署时建议先看验证集的 PR 曲线再定阈值,毕设演示视频里 0.45 通常够用。

5.2 蓝牌高反光、倾斜、阴阳牌,按这个顺序排查

现象可能原因排查顺序
检测框抖动或漏检conf 阈值设置不当先看 val 集 PR 曲线,再调 conf
检到框但识别乱码车牌倾斜超过 5 度检查输入图有没有角度校正
「I」和「1」混淆字符频次不均衡统计训练集字符分布,重采样
蓝牌高反光发白前照灯强光过曝增强里补 HSV 曝光扰动,推理前做直方图均衡
训练 loss 降到 0.3 后不动长尾汉字欠拟合对汉字类做 hard sample 挖掘

5.3 倾斜修正:不要为它引入第二个学习模型

水平检测框框出来的车牌往往是带倾斜的平行四边形,直接 resize 会带入车漆背景纹理。常见做法是标车牌四角做透视变换,但毕设阶段一般只有水平框标注。我的做法是让检测器只出水平框,把倾斜容忍完全交给增强侧,推理时加一个不超过 20 行的几何粗校正兜底:先框出车牌蓝色区域的最小外接矩形,用其倾斜角对原图做仿射旋转,把车牌摆正后再送 CRNN。这个方案的核心在于:检测网络负责召回,识别网络负责准确率,中间的修正只做几何粗对齐,绝不引入第二个可学习模型,否则出错时根本定位不到是哪一段出的问题。

最后落一个实操细节:如果旋转后车牌宽度仍然超过 200 像素,先按 160 宽压缩再送识别;如果宽度不足 100 像素,说明目标太小,先对检测框做一次双线性插值放大再送,CTC 对过于模糊的小图会频繁输出 blank,放大后字符边缘更锐利,识别成功率能提升一截。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询