ARTICLE DETAIL

资讯详情

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

YOLOv8+CNN车牌识别:从检测到字符分类的实战指南

YOLOv8+CNN车牌识别:从检测到字符分类的实战指南 简介面向计算机及相关专业学生、教师或企业开发者的深度学习车牌识别系统源码包覆盖毕业设计、课程设计与项目初期演示等场景也适合小白学习进阶。项目以YOLOv8完成车牌区域检测用CNN网络实现字符识别并提供完整源码与配套文档整体流程清晰易于理解。压缩包共426个文件约39.35MB包含140个Python脚本和211个编译后的pyc文件、47个YOLO所需的yaml配置、预训练权重pt/pth、测试车牌图片以及sh脚本与md/txt说明文档目录结构清晰便于按模块查阅也能让初学者快速定位所需功能。测试图片涵盖蓝牌、绿牌、黄牌等常见类型这套高分项目已通过导师指导认可评审得分95分代码运行验证功能正常适合有基础者二次开发也可直接用于课程设计或毕业论文。目前已累计83人学习下载完整资料包对快速搭建车牌识别系统、完成毕设课设以及学习进阶都有参考价值。1. 车牌识别拆成 YOLOv8 检测加 CNN 识别不是绕路而是抄近道很多人第一次做车牌识别第一反应是用一个目标检测模型把七个字符全部框出来再按从左到右拼成车牌字符串。真正落地时你会发现这条路很累字符之间存在遮挡、反光和形变检测框稍微偏一点拼出来的字符串就错一个字母。常见做法是把任务拆成两段——先用 YOLOv8 定位车牌区域再用 CNN 对切分后的字符做分类。这个拆分不是绕路而是让每个模型只解决一个维度的问题检测模型只管“车牌在哪”分类模型只管“这个字符是什么”。两段模型的训练样本可以分别优化检测错误和识别错误也能单独定位。这篇文章会从数据流、检测训练、CNN 识别到部署量化把整套方案讲成你能直接照着写的步骤适合正在做毕设、练手项目或者在做园区门禁、停车场道闸的工程师参考。2. 两段式车牌识别的整体数据流与字符类别设计2.1 数据流检测框、透视校正、字符切分、字符分类整套系统的输入是一张包含车辆的图片输出是一个字符串比如“粤B12345”。中间的数据流我在实际项目里一般固定为四步YOLOv8 检测出车牌区域输出一个水平矩形框附带置信度。对框内的车牌图像做预处理灰度化、二值化、透视校正。把校正后的车牌按字符位置切成 7 个小图。每个小图送入 CNN得到 65 类字符的概率分布按位置拼接成最终车牌号。第 2 步的透视校正不是可选项。YOLOv8 默认输出的是水平矩形如果车辆停在斜坡上或者摄像头安装角度偏车牌在图像里是平行四边形。这种情况下直接切字符CNN 拿到的是歪斜的字符图识别准确率会明显下降。常见处理是拿到检测框后在框内用边缘检测找车牌的四条边计算透视变换矩阵把四边形拉正成矩形。第 3 步的切分方式分两种固定等分和垂直投影切分。固定等分适用于标准蓝牌字符间距均匀垂直投影切分则先统计每一列的像素和波谷就是字符间隙适应性更强。后面第 5 章会给出一个可用的垂直投影切分函数。2.2 字符集的构成65 类而不是 70 类车牌识别里的字符分类和通用 OCR 不同。中国民用汽车牌照由省份简称、字母、数字组成标准蓝牌是 7 位排列是“汉字 字母 5 位字母数字混合”。字符类别数需要提前定死因为它直接决定 CNN 最后一层全连接的输出维度。类别数量说明省份简称31京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼字母2426 个英文字母去掉 I 和 O避免与数字 1 和 0 混淆数字100 到 9合计65CNN 输出层神经元数量注意新能源车牌是 8 位排列规则不同本文先不展开。如果你在数据集里混入新能源车牌字符切分逻辑要重写因为第 8 位字符与前面的间距不一致。我一般建议第一个版本只做蓝牌跑通后再扩展绿牌。2.3 数据集标注检测标注和识别标注分开做两段式模型需要两份标注。检测部分用 LabelImg 或 Labelme 标注车牌外框通常只需要标矩形框不需要标字符级位置。识别部分有两种做法省钱做法是只标注整张车牌对应的字符串然后把字符切分后的图自动存入对应类别的文件夹稳妥做法是直接标字符级框。前者适合快速迭代后者更适合正式项目。def crop_characters(plate_img, label, output_dir, char_i): 根据整牌标注切出单个字符并保存 plate_img: 透视校正后的车牌灰度图 label: 车牌字符串如 粤B12345 output_dir: 字符样本输出目录 char_i: 期望在检测框内切出的字符总数 _, binary cv2.threshold(plate_img, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) # 按列统计像素值判断字符间隙 col_sum binary.sum(axis0) char_positions find_char_boundaries(col_sum) # 返回每个字符的左右边界 # 如果检测到切分点数量不等于7则回退到等分切分 if len(char_positions) ! char_i: width plate_img.shape[1] // char_i for i in range(char_i): char plate_img[:, i*width:(i1)*width] cv2.imwrite(f{output_dir}/{label[i]}_{i}.jpg, char) else: for i, (left, right) in enumerate(char_positions): char plate_img[:, left:right] dst cv2.resize(char, (32, 64)) cv2.imwrite(f{output_dir}/{label[i]}_{i}.jpg, dst)这段代码的切分依据是二值化后的列像素和但注意find_char_boundaries需要你自己根据数据集统计合适的最小间隙宽度。参数char_i用来兜底当投影法找不到 7 个字符时说明二值化被噪声干扰回退到等分切分能保证样本不丢。实际上这一步生成的字符样本还需要人工抽检把切歪的图删掉否则 CNN 会用错样本学出错误特征。3. YOLOv8 车牌检测模型选型、训练参数与后处理3.1 为什么选 YOLOv8 而不是 YOLOv5anchor-free 与 C2f 结构车牌在整张车辆图里属于小目标通常占图像面积的 1% 到 3%。YOLOv8 相比 v5 的核心变化是换成了 anchor-free 检测头不再需要预设一组候选框尺寸。车牌的长宽比虽然有规律但不同省份、不同材质车牌的字符间距略有差异anchor-free 让模型自己回归“中心点 宽高”省掉了调 anchor 尺寸这一步。结构上 YOLOv8 把 v5 的 C3 模块换成了 C2f。C2f 在 Bottleneck 之间增加了 split 操作让梯度回传路径更丰富对车牌的边缘纹理和字符细节能提取到更细的特征。实际表现在同等参数下C2f 对 640 分辨率下的小目标召回率会比 v5 高二到三个百分点。这里提一个容易踩的坑很多人直接拿 GitHub 上 YOLOv8 默认的 COCO 预训练权重做车牌推理这是行不通的。COCO 数据集里没有车牌这个类别必须用预训练权重在自己的车牌数据集上微调。3.2 训练自己的车牌数据集data.yaml 与关键参数先准备数据集目录结构然后写data.yamltrain: ./datasets/plate_det/train/images val: ./datasets/plate_det/val/images nc: 1 names: [plate]训练命令我一般这么跑yolo detect train \ datadata.yaml \ modelyolov8s.pt \ epochs150 \ imgsz640 \ batch16 \ optimizerSGD \ lr00.01 \ cos_lrTrue \ patience20 \ project./runs/detect参数说明modelyolov8s.pt是起点如果你的部署设备是 CPU 或者嵌入式板子换yolov8n.pt速度优先有更好的 GPU 就用yolov8m.pt边界框定位精度更高。imgsz640是训练输入尺寸车牌小目标可以把 imgsz 提到 960代价是训练显存增加。我之前用 GTX 1660 Ti 6G 显存跑yolov8s imgsz640 batch8刚好不爆显存如果你只有 4G 显存batch 必须降到 4 或者换yolov8n。patience20表示 20 个 epoch 内验证集 mAP 没有提升就早停避免过拟合。训练完成后验证一下yolo detect val \ modelruns/detect/train/weights/best.pt \ datadata.yaml重点关注验证输出里的mAP50车牌检测属于单类检测mAP50 能达到 0.95 以上才算合格。如果只有 0.9 左右优先检查标注框是否把车牌边缘包含进去标注多一圈背景通常比少标字符角更影响精度。3.3 检测框后处理宽高比过滤与置信度阈值YOLOv8 在车辆图上还可能把前挡风玻璃、车灯、反光条误检成车牌。一个简单有效的后处理是结合车牌的几何先验做过滤。def filter_plate_boxes(pred_boxes, conf_thres0.6): 过滤 YOLOv8 检测结果中的非车牌框 pred_boxes: 模型输出每个元素为 [x1, y1, x2, y2, conf, cls] 返回过滤后的框列表 valid_boxes [] for box in pred_boxes: x1, y1, x2, y2, conf, cls box if conf conf_thres: continue w x2 - x1 h y2 - y1 if h 0: continue aspect_ratio w / h # 标准车牌宽高比约为 3:1允许 2.2 到 4.5 的浮动范围 if 2.2 aspect_ratio 4.5: valid_boxes.append(box) return valid_boxes车牌宽高比的浮动范围不能收得太紧。普通蓝牌是 440 x 140比例约 3.14但近距离拍摄时由于透视变形比例可能到 2.5 左右。范围设 2.2 到 4.5 已经能滤掉绝大多数误检。置信度阈值conf_thres在白天场景设 0.6夜晚或者车牌反光严重时降到 0.4否则真车牌会被滤掉。4. CNN 字符识别把切分图变成 65 分类任务4.1 为什么识别部分不用检测模型而用 CNN 分类有人会问既然 YOLOv8 能检测车牌直接让 YOLOv8 再检测字符行不行可以做但不划算。字符是密集排列的 7 个小目标相邻字符间距极小YOLOv8 的 NMS 后处理很容易把两个字符合并成一个框或者把同一个字符重复检测两次。而切分后的字符图是标准的图像分类问题CNN 分类网络结构简单、推理速度快、参数少字符位置已经在切分阶段固定了不需要回归坐标。4.2 一个可复现的 CNN 分类网络结构这里给出一个能在 CPU 上跑到毫秒级的小网络结构参考了经典 LeNet 的堆叠思路但针对 32x64 的字符图做了调整。import torch import torch.nn as nn class CharCNN(nn.Module): def __init__(self, num_classes65): super().__init__() self.features nn.Sequential( nn.Conv2d(1, 32, kernel_size3, padding1), nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), # 输出 16x32 nn.Conv2d(32, 64, kernel_size3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), # 输出 8x16 nn.Conv2d(64, 128, kernel_size3, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), # 输出 4x8 ) self.classifier nn.Sequential( nn.AdaptiveAvgPool2d((1, 1)), nn.Flatten(), nn.Linear(128, 128), nn.ReLU(inplaceTrue), nn.Dropout(0.3), nn.Linear(128, num_classes), ) def forward(self, x): return self.classifier(self.features(x))网络结构说明输入是 1 通道的灰度字符图尺寸 32x64。三层卷积的通道数从 32 翻倍到 128每层卷积后接 BatchNorm 和 ReLU再经过 MaxPooling 降采样。最后的AdaptiveAvgPool2d((1, 1))把特征图压成 1x1好处是不管输入字符图是 32x64 还是 28x56全连接层的输入维度都是 128。这里用 Dropout 0.3 防止字符样本少时过拟合。4.3 训练循环与推理拼接训练这个网络用交叉熵损失优化器用 Adam学习率 1e-3 起步。训练时注意字符样本的类别均衡如果把整牌切分后的字符直接拿去训练出现频率高的省份汉字会占大头CNN 学到的是“蒙”字出现在第一位而不是“蒙”字长这样。常见做法是每个类别的样本数量随机采样到同一数量级。接到 YOLOv8 后面的推理流程可以这样写def recognize_plate(plate_img, yolo_result): 端到端完成车牌识别 plate_img: 检测框截取的车牌图 yolo_result: YOLOv8 返回的检测框坐标 warped perspective_correct(plate_img) # 透视校正 char_images crop_characters(warped, char_i7) # 字符切分 chars [] for char in char_images: tensor torch.from_numpy(char).float().unsqueeze(0).unsqueeze(0) tensor tensor / 255.0 with torch.no_grad(): logits model_cnn(tensor) pred int(torch.argmax(logits, dim1).item()) chars.append(class_to_char[pred]) return .join(chars)这段代码里class_to_char是训练时建立的类别索引映射表。有一个容易忽略的点字符图在送入 CNN 前要统一归一化到 0 到 1 之间且训练时做了同样的预处理否则推理时输入分布和训练分布不一致准确率会断崖式下降。4.4 最容易混淆的字符对字符分类的错误不是均匀分布的主要集中在几组形近字符上。下面这组对照表在训练和验证时值得重点看混淆对原因缓解手段0 与 O车牌字体中 O 的外形和 0 几乎一样数据增强时对 O 做轻微拉伸对 0 不做1 与 I衬线字体下 I 有横杠非衬线下两者极像统一裁剪字符上下边缘消除字体差异2 与 Z部分车牌字体底部有圆角切分后对字符做 3 度内的随机旋转B 与 8下部的半圆缺口容易闭合加形态学腐蚀增强突出缺口我在实验中发现一个规律省份汉字的识别准确率天然低于字母和数字因为汉字笔画多32x64 的分辨率下细节丢失严重。处理方式是把汉字送入 CNN 前改用 48x96 分辨率同时把网络的池化层次数调低保留更多细节。5. 部署到嵌入式设备时的量化技巧与端到端验证训练完成的模型直接跑推理时YOLOv8s 在 CPU 上大概需要 150 毫秒一帧CNN 字符识别需要 20 毫秒左右加起来将近 200 毫秒勉强够用。如果目标是 RK3588 这类边缘设备或者要做到 30 FPS 以上需要做量化和导出。PyTorch 自带的动态量化对 CNN 分类网络效果明显import torch.quantization as quant model_cnn.eval() model_cnn.qconfig quant.get_default_qconfig(fbgemm) quant.prepare(model_cnn, inplaceTrue) quant.convert(model_cnn, inplaceTrue) # 导出为 ONNX方便部署 dummy_input torch.randn(1, 1, 32, 64) torch.onnx.export(model_cnn, dummy_input, char_cnn.onnx)量化后的 CNN 模型体积能从原来的 4MB 缩到 1MB 左右推理速度提升三倍。YOLOv8 导出 ONNX 时记得加halfTrue参数用 FP16 精度推理检测精度几乎不掉。端到端验证要按“检测正确、识别正确、整牌正确”三个层面统计建议在验证集上记录三组数字车牌检测框与标注框的 IoU 大于 0.5 记为检测正确在检测正确的前提下7 位字符全部预测正确记为识别正确。我在实际数据上通常得到的结果是检测 mAP 0.97字符分类准确率 0.985整体整牌正确率约 0.93差距主要来自切分错误而不是分类错误。最后留一个技巧垂直投影切分时min_gap参数不要固定先把投影数组做高斯平滑再用局部极小值找切分点遇到边框反光和铆钉干扰时比直接找波谷稳健得多。本文还有配套的精品资源点击获取
返回列表