ARTICLE DETAIL

资讯详情

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

深度学习车牌识别实战:YOLOv8检测+CRNN识别全流程解析

深度学习车牌识别实战:YOLOv8检测+CRNN识别全流程解析 简介一套基于深度学习的车牌识别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), encodingutf-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_limit0.15, contrast_limit0.1, p0.5), A.HueSaturationValue(hue_shift_limit5, sat_shift_limit20, val_shift_limit10, p0.3), A.GaussianBlur(blur_limit(3, 5), p0.15), A.Perspective(scale(0.02, 0.06), p0.3), A.Affine(rotate(-5, 5), translate_percent(0.02, 0.02), p0.4), ], bbox_paramsA.BboxParams(formatyolo, label_fields[labels])) # 用法读项目里一张陕A9M24H.jpg传入归一化框坐标 # bbox 格式为 [cx, cy, w, h]labels 与 bboxes 一一对应 out train_transform( imageimg, bboxes[(0.55, 0.72, 0.18, 0.07)], labels[0], )特别注意一个常被忽视的坑车牌检测不要开水平翻转。虽然翻转后检测框形状不变但车牌字符序列的语义顺序会反这个雷不会立刻爆在检测阶段而是在后续 CRNN 训练时表现为识别准确率死活上不去因为模型被迫学习「反字车牌」和「正字车牌」两种完全矛盾的映射。角度扰动同样有上限5 度是经验值超出后蓝牌边框被透视变形压成不规则四边形检测框会顺带框入一大片干扰背景。各增强项的参数边界可以按下表控制增强操作常用参数解决什么边界亮度/对比度brightness_limit0.15停车场夜间、逆光过暗会吞掉字符纹理HSV 扰动sat_shift_limit20不同摄像头白平衡差异蓝底饱和度偏移过大会接近绿牌高斯模糊blur_limit(3, 5)车辆低速行驶的运动模糊模糊过大后字符不可读透视变换scale0.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 \ dataplate.yaml \ modelyolov8s.pt \ epochs120 \ imgsz640 \ batch16 \ lr00.01 \ lrf0.01 \ optimizerSGD \ patience15 \ device0参数表里的每一项都有讲究列几个实际会踩到的参数设定值作用踩坑提示imgsz640输入分辨率车牌小于 32×32 时建议提到 960大分辨率直接翻倍吃显存batch 要跟着降batch16单卡 8G 可跑4G 降到 8batch 过小会导致 BN 统计量抖动大lr00.01SGD 默认学习率换 Adam 时 lr0 建议 0.001否则 loss 震荡patience15验证集连续 15 轮无提升就早停测试图太少时 val 抖动大容易误触发早停optimizerSGD配合官方预训练权重最稳Adam 需要更大的 weight_decay 防过拟合这里提一下modelyolov8s.pt的含义加载 COCO 预训练权重做迁移学习而不是从随机初始化开始。车牌虽然是新类别但底层的边缘、纹理、颜色特征和 COCO 里车辆类目标共享微调 120 epoch 足够收敛。如果显存够可以把 imgsz 提到 960 试一次侧拍和远距离小目标场景的 mAP 提升明显代价是训练时间接近翻倍。3.3 训练日志该盯哪些指标训练跑起来之后别只看 mAP50。车牌检测在 120 epoch 后 mAP50 普遍能到 99 以上但真正有区分度的两个指标是val/box_loss和val/dfl_loss的衰减曲线。如果 box_loss 降到 0.03 附近不再动而 dfl_loss 还在缓慢下行说明回归框已经很准分布收敛还差几个 epoch这时候加训比调参更有效。另一个典型场景训练日志显示 Precision 和 Recall 都很高画验证图却发现车灯、车标被误检成车牌。绝大多数原因是验证集里没有这些难例训练时负样本不足。解决办法是单独收集一批不含车牌的夜间车尾图作为背景负样本塞进 train 目录让模型见过「长得像车牌但不是车牌」的纹理。这种 hard negative mining 在毕设源码里很少见但答辩时提一句会很加分。提交模型时记住用best.pt而不是last.ptbest.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_classes66, hidden_size256): super().__init__() self.cnn nn.Sequential( nn.Conv2d(3, 64, 3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.MaxPool2d(2, 2), # 48 - 24 nn.Conv2d(64, 128, 3, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.MaxPool2d(2, 2), # 24 - 12 nn.Conv2d(128, 256, 3, padding1), nn.BatchNorm2d(256), nn.ReLU(inplaceTrue), nn.MaxPool2d((2, 1), (2, 1)), # 高 12 - 6宽方向不缩 nn.Conv2d(256, 512, 3, padding1), nn.BatchNorm2d(512), nn.ReLU(inplaceTrue), ) # 经过上述卷积池化后特征图尺寸: (B, 512, 6, 160) self.lstm nn.LSTM(512, hidden_size, num_layers2, bidirectionalTrue, dropout0.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(dim2) # 压缩高度保留宽度 - (B, 512, 160) seq feat.permute(2, 0, 1) # 转成序列格式 (T, B, C)T160 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 自带的 CTCLossimport torch loss_fn nn.CTCLoss(blank0, zero_infinityTrue) # 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_infinityTrue是毕设里容易漏掉的参数当某条样本的对齐概率下溢为 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-910blankCTC 保留位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, conf0.45, iou0.5, imgsz640, verboseFalse)[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]})conf0.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放大后字符边缘更锐利识别成功率能提升一截。本文还有配套的精品资源点击获取
返回列表