ARTICLE DETAIL

资讯详情

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

基于深度学习与YOLO的车牌识别系统设计与实现

基于深度学习与YOLO的车牌识别系统设计与实现 简介这是一份面向计算机专业课程设计与期末大作业场景的车牌识别系统源码包项目基于Python与深度学习技术覆盖车牌图像处理、车牌定位、字符分割与识别等核心流程配有可运行主程序与说明文档适合正在完成大作业或希望进行项目实战的计算机相关专业学生参考。压缩包共18个文件体积约4.73MB其中5个Python脚本负责图像处理、特征提取与模型推理2个dat文件为SVM训练模型含中文车牌字符模型另有png/jpg图片素材用于测试同时附有车牌识别.pptx和README.md便于梳理项目结构与演示思路。已有483人学习下载作为一套获得过98分评价的成熟大作业项目源码整理完整、目录清晰可帮助读者快速理解车牌识别系统的工程实现方式也能在课程设计或期末答辩中直接作为演示与二次开发基础。1. 一个“车牌识别系统”到底在做什么从 OpenCV 边缘检测到深度学习为什么选这条路年关前后我接过好几个学生的咨询手里的“计算机大作业设计源码基于Python车牌识别系统深度学习项目源码.zip”几乎都是同一个套路下载、解压、跑通、换数据、截图、交作业。但真正动手时你会发现车牌识别没有教程里那么“智能”——摄像头稍微侧一点、车牌沾泥、路灯下反光识别结果就完全不可信。这个项目标题里最值钱的不是“源码”两个字而是它同时踩中了 Python、深度学习、车牌识别三个需求它是一套完整的视觉流水线不是一段调用现成 OCR 的脚本。它的典型落地路径是先用目标检测模型把画面里的车牌“框”出来再用一个序列识别模型把框内的字符读出来。前者解决“车牌在哪”后者解决“牌上写了什么”。适合的人群很明确要做深度学习课程设计的学生、想入门目标检测加 OCR 的开发者以及需要快速做一版车牌识别 Demo 的工程新手。如果你只想调一个 API 完事这套源码反而是绕远路但凡你要在报告里写清楚“网络结构、训练策略、评价指标”它就比任何黑匣子接口都好用。2. 把车牌识别拆成两段检测与识别先想清楚再动手2.1 端到端模型 vs 两阶段流水线大作业为什么选后者市面上做车牌识别的深度方案大致分两类一类是端到端模型输入一张图直接输出车牌字符串另一类是两阶段流水线先检测车牌区域再对裁剪区域做字符识别。标题里的“车牌识别系统”更常见的组织方式是两阶段原因很实际端到端模型看起来省事但训练数据要求极高——同一个网络既要学“车牌在哪”又要学“字符是什么”两个任务的梯度互相拉扯小数据集很容易训出一个“什么都懂一点、什么都不精”的网络。两阶段的最大优势是中间产物可控。检测阶段输出的是坐标框,你可以画出来看、存下来调试识别阶段吃的是裁剪后的矩形图输入尺寸固定数据增强可以单独针对字符做。真出问题时你能明确知道是检测漏了还是识别错了而不是对着一整张图的输出发懵。另一个隐藏优势是模型复用。检测端训好的权重可以换到别的任务上——反过来识别端换一套字体或颜色也能单独重训。期末赶工时这样做可以只重训一个阶段省一半时间。从报告角度说两阶段也比端到端好写目标检测讲 YOLO 的边框回归和 NMS识别端讲 CTC Loss 和序列建模两个模块各成一章原理、实验、结果对应关系清清楚楚。2.2 识别端选型LPRNet 与 CRNN 的取舍识别端这个位置常见的备选是 LPRNet 和 CRNN。CRNN 是老牌选手用 CNN 提特征、LSTM 建模序列、CTC 对齐效果稳但它有两个不适合大作业的毛病一是模型体积大推理速度相对慢二是训练时需要把特征序列按时间步展开对输入尺寸比较敏感。LPRNet 的思路是“去序列化”——不依赖循环神经网络纯卷积网络直接输出字符序列的概率分布再用 CTC 做对齐。它的优势在于三点输入图像可以不是等宽的训练和推理时对车牌宽高比的变化更宽容模型非常轻CPU 上跑一帧也就几十毫秒结构简单论文和开源实现都多报告里画网络结构图容易。如果你的数据集里车牌角度比较正、字符间距规整两者差距不大。但考虑到大作业大概率在 CPU 上演示我一般建议用 LPRNet。它不需要 LSTM 的时间步概念反向传播更直接调参的玄学空间也小一些。2.3 一个可落地的项目目录结构拿到一个 zip 源码包第一件事不是跑pip install -r requirements.txt而是先看目录结构是否完整。一个能顺利复现的车牌识别项目文件组织大致是这样license_plate_recognition/ ├── configs/ │ ├── detect.yaml # 检测模型配置含类别名、输入尺寸 │ └── reco_config.yaml # 识别模型配置含字符表、图像宽高 ├── data/ │ ├── images/ # 原始图片含训练/验证/测试 │ ├── labels/ # YOLO 格式标注类别 cx cy w h │ └── char_mapping.txt # 字符索引表一行一个字符 ├── src/ │ ├── detect/ │ │ ├── train.py # 检测模型训练入口 │ │ └── infer.py # 检测推理输出车牌坐标框 │ ├── recognize/ │ │ ├── model.py # LPRNet 网络定义 │ │ ├── train.py # 识别模型训练入口 │ │ └── decode.py # CTC 解码去重与空字符过滤 │ └── pipeline.py # 检测识别串联端到端推理 ├── weights/ │ ├── detect_best.pt │ └── lprnet_best.pth └── requirements.txt我见过很多翻车案例问题不在模型而是char_mapping.txt字符顺序和训练时不一致推理结果全是乱的。所以拿到源码先做一件事核对字符表确保0-9、A-Z、省份简称汉字都在表里且索引从 0 开始连续递增。字符表和模型权重是绑定的换字符表必须重训识别模型。3. 用 YOLOv8 把车牌从画面里“抠”出来数据集、转换脚本与三个必调参数3.1 数据从哪来来CCPD 子集与手工标注的取舍检测阶段的开源数据集最常用的是 CCPD中国城市车牌数据集里面有近 30 万张带标注的车牌图。对大作业来说不用全量挑几千张就够。选数据时注意一个坑CCPD 按天气和场景分了ccpd_blur、ccpd_rain、ccpd_snow、ccpd_night等子集不要只挑大晴天正面的图否则一到演示现场灯光一暗就露馅。如果你要检测的是特定场景——比如校园门禁、地下车库——CCPD 可能不够贴合。常见做法是先用 CCPD 训一版再拿手机去目标场景拍 200 到 300 张用 LabelImg 手工标注把标注结果并进训练集做微调。手工标注车牌不算痛苦车牌是矩形目标框四个角就行但要注意把整个车牌含白边一起框进去不要只框字符区域。3.2 标注文件转成 YOLO 格式坐标归一化的四个细节CCPD 自带的标注是 JSON 里的vertices字段存的是四个角点的绝对坐标。而 YOLO 需要的是归一化后的中心点坐标和宽高(cx, cy, w, h)。转换脚本是关键一环四角坐标转中心点宽高的写法如下import json import os def ccpd_json_to_yolo(json_path, img_w, img_h, out_txt_path): with open(json_path, r, encodingutf-8) as f: data json.load(f) # CCPD 的 vertices 是四角点列表格式为 [[x1,y1],[x2,y2],[x3,y3],[x4,y4]] vertices data[vertices] xs [p[0] for p in vertices] ys [p[1] for p in vertices] x_min, x_max min(xs), max(xs) y_min, y_max min(ys), max(ys) # 用外接矩形计算中心点和宽高 box_w x_max - x_min box_h y_max - y_min cx (x_min x_max) / 2.0 cy (y_min y_max) / 2.0 # 归一化到 [0,1]注意除以的是图片宽高 cx / img_w cy / img_h box_w / img_w box_h / img_h # 单类别车牌class id 固定为 0 with open(out_txt_path, w, encodingutf-8) as f: f.write(f0 {cx:.6f} {cy:.6f} {box_w:.6f} {box_h:.6f}\n) if __name__ __main__: # 示例单张图片转换 ccpd_json_to_yolo( json_pathdata/raw/1.json, img_w1160, img_h720, out_txt_pathdata/labels/1.txt )逻辑上要盯住四个细节第一四角点转外接矩形时如果原图有旋转车牌外接矩形会把背景也框进来解决办法是先用最小外接矩形算子拿到角度再做仿射矫正而不是直接取min/max第二归一化的分母必须是原图尺寸不是标注图尺寸CCPD 的高清图下载后如果被压缩过这一步会直接错位第三类别 ID 从 0 开始在data.yaml里要写names: [plate]第四XML 或 JSON 里的坐标原点在左上角YOLO 的归一化坐标原点也在左上角别画蛇添足做翻转。3.3 训练与推理参数一组能直接跑的命令检测端我习惯用 ultralytics 的 YOLOv8代码侵入少训练和导出一条龙。训练前的data.yaml长这样path: ./data # 数据集根目录 train: images/train # 训练图片相对路径 val: images/val # 验证图片相对路径 nc: 1 # 类别数只有车牌一类 names: 0: plate训练命令不需要写太花哨关键是参数要贴合车牌这类小目标yolo detect train \ modelyolov8s.pt \ datadata.yaml \ epochs60 \ imgsz640 \ batch16 \ lr00.01 \ mosaic1.0 \ close_mosaic10 \ patience15参数说明imgsz640是速度和精度的折中车牌在整张图中占比通常很小低于 640 会直接把小目标“压没”mosaic1.0开启马赛克增强等于把四张图拼成一张再训练能显著提升小目标检测稳定性close_mosaic10表示最后 10 个 epoch 关闭马赛克增强让模型在接近真实分布的数据上收尾patience15控制早停验证集 mAP 连续 15 轮不涨就自动停。推理时不要直接拿训练脚本改单独写一个推理脚本更好控制输出from ultralytics import YOLO model YOLO(weights/detect_best.pt) results model.predict( sourcedata/test/street.jpg, imgsz640, conf0.35, # 置信度阈值低一点能减少漏检但误检会增加 iou0.5, # NMS 阈值车牌之间不会重叠0.5 足够 max_det10, # 单张图最多出 10 个框防止误检堆叠 save_txtFalse, save_cropTrue # 自动保存裁剪后的车牌图后续识别直接吃这些图 )这段代码里save_cropTrue是最实用的参数——它会自动把检测到的车牌区域裁剪成小图存到runs/detect/predict/crops/plate/下后面识别阶段直接从这读图就行不用自己再写裁剪逻辑。conf调到 0.35 以下时误检框会明显变多建议只在夜间场景下适当放宽。4. 车牌字符识别LPRNet CTC不分割单字符的做法与训练细节4.1 为什么不用单字符分割识别早期车牌识别喜欢走“分割再分类”路线先做垂直投影把字符一个个切出来再对每个字符做分类。这套方法在字体规整、间距均匀的蓝牌上表现尚可遇到新能源绿牌(多一位字符)、使馆黑牌、倾斜或遮挡场景就崩了——投影分割把字符切得七零八落后续分类再准也没用。深度学习方案里更稳的是把整张车牌图当成一个序列让网络自己学习字符间的时序关系。这就是 CTC 的用武之地网络输出一个比字符数长的概率序列CTC 通过“去重 去空白”把序列压缩成最终字符串不需要显式分割字符。推广到任意长度字符组合时这套方案几乎不需要改代码。4.2 LPRNet 模型要点与简化实现LPRNet 的输入是固定高度的灰度图宽度可以变化通常高度取 24 像素宽度取 94 像素。网络主体是一串卷积层逐步下采样得到特征图再通过一个全连接映射到字符概率分布。一个能直接跑的简化版模型定义如下import torch import torch.nn as nn class SmallLPRNet(nn.Module): def __init__(self, num_classes): super().__init__() # 输入: (batch, 1, 24, 94) 灰度车牌图 self.backbone nn.Sequential( nn.Conv2d(1, 64, 3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), # 尺寸 - 12 x 47 nn.Conv2d(64, 128, 3, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), # 尺寸 - 6 x 23 nn.Conv2d(128, 256, 3, padding1), nn.BatchNorm2d(256), nn.ReLU(inplaceTrue), ) # 把 6 x 23 的特征图按宽度方向展开成序列 # 后面的 1x1 卷积等价于对每个位置做分类 self.head nn.Sequential( nn.Conv2d(256, num_classes, 1), ) def forward(self, x): x self.backbone(x) # b, 256, 6, 23 x self.head(x) # b, num_classes, 6, 23 # CTC 需要的形状: (time_steps, batch, num_classes) # 每个宽度位置是一个时间步 b, c, h, w x.size() x x.squeeze(2) # 高度已压缩到 1直接去掉 x x.permute(2, 0, 1) # (w, b, c) - (23, b, num_classes) return x代码逻辑说明backbone三段卷积逐步降低空间尺寸、增加通道数最终把每张图压成6 x 23的特征网格head用 1x1 卷积把每个位置的 256 维特征映射到num_classes维相当于告诉模型“这个时间步属于哪个字符类别”。最后squeeze(2)是假设高度方向已经被压缩到 1如果特征图高度不是 1需要先用一个全局池化把高度方向压掉。参数说明输入高度固定 24 是为了配合两次MaxPool2d(2)的降采样链输出特征图高度从 24 降到 6所以网络设计上对高度有限定宽度从 94 降到 23对应 CTC 的 23 个时间步。num_classes必须在字符表基础上加 1多出来的一个是 CTC 的空白字符索引一般取最后一个。4.3 训练数据怎么低成本凑齐识别模型的训练数据和检测不同不需要背景复杂的实拍图要的是“车牌区域特写”——字符清晰、背景基本是车牌底色的矩形图。常规来源有两个一是从 CCPD 检测标注框里直接裁剪出车牌区域二是用开源工具合成车牌图。合成更适合用来补齐省份汉字和生僻字符因为实拍数据集里省份简称分布不均匀“京”“苏”“粤”特别多“藏”“青”很少。训练时输入处理有固定套路转灰度、缩放到1 x 24 x 94、做归一化。数据增强方面轻度仿射变换和随机亮度对比度最有用因为实拍车牌的倾斜和光照变化是主要干扰。import cv2 import torch from torch.utils.data import Dataset class PlateDataset(Dataset): def __init__(self, img_paths, labels, char_to_idx, img_w94, img_h24): self.img_paths img_paths self.labels labels self.char_to_idx char_to_idx self.img_w img_w self.img_h img_h def __getitem__(self, idx): img cv2.imread(self.img_paths[idx], cv2.IMREAD_GRAYSCALE) img cv2.resize(img, (self.img_w, self.img_h)) img img.astype(float32) / 255.0 img torch.from_numpy(img).unsqueeze(0) # (1, 24, 94) # 标签转成字符索引序列例如 京A12345 - [2, 5, 13, ...] label_indices [self.char_to_idx[ch] for ch in self.labels[idx]] label_indices torch.tensor(label_indices, dtypetorch.long) return img, label_indices这个 Dataset 有两个要点字符索引不能直接从 0 开始因为 0 通常留给 CTC 的 blank实际字符索引从 1 开始映射blank 留在最后一位标签不需要 padding 对齐PyTorch 的CTCLoss接受变长标签在这里返回原始序列即可。4.4 识别推理greedy decode 与去重规则推理时不需要做束搜索车牌字符短、类别明确用贪心解码就够了。核心逻辑是取每个时间步概率最大的字符再去掉连续重复和 blankdef greedy_decode(model_output, blank_idx): # model_output shape: (time_steps, batch, num_classes) probs model_output.permute(1, 0, 2) # (batch, time_steps, num_classes) preds probs.argmax(dim2) # (batch, time_steps) decoded [] for pred in preds: chars [] prev None for t in range(pred.size(0)): idx pred[t].item() if idx ! blank_idx and idx ! prev: chars.append(idx) prev idx decoded.append(chars) return decoded去重逻辑只有一条规则当前时间步的类别和上一个时间步相同就跳过同时遇到 blank 也跳过。这里有个容易忽略的细节——prev的更新必须在遇到 blank 时也执行否则“A A blank A”这种情况会把中间的 A 去掉后前后两个 A 又被合并成一个。如果你发现识别结果少了字符先检查这里。训练参数给一组参考起点batch_size64、学习率1e-3、Adam 优化器、Cosine 退火、训练 50 到 80 个 epoch。字符表大约 70 个类别时LPRNet 单卡 CPU 训练几十个 epoch 就能在验证集上达到 95% 左右的字符准确率这也是它适合大作业的原因——不需要等 GPU 排队。5. 常见问题排查我在这类项目上翻过的五个车5.1 检测框把整辆车框住车牌被漏掉现象推理结果里出现一个接近全图大小的框但 save_crop 裁剪出来的“车牌图”里根本没有车牌识别结果自然全错。原因训练数据里存在“整辆车”的标注噪声。CCPD 部分子集标注框偶尔会把车头整体框进去YOLO 学到的是“有车就有框”而不是“有车牌才有框”。解决训练前清洗数据是必须的写脚本筛出宽高比大于 4 或小于 1 的标注框直接删掉车牌宽高比通常在 2 到 4 之间另外把conf阈值提到 0.5 以上过滤低置信度的整框。这类问题靠调参是压不住的必须清数据。5.2 训练 loss 降了但识别出来全是“白板”现象LPRNet 训练时 loss 从十几降到 0.5 以下推理时输出却是空字符串或乱码。原因字符表和模型输出维度对不上。最常见的是num_classes算错了——字符表 70 个字符加了 blank 应该是 71但模型里写成了 70CTC 的 blank 索引越界另一种是解码时blank_idx写成了 0而 0 是真实字符所有空白被当成字符输出了。解决写一个自检脚本训练前打印num_classes、blank_idx、len(char_list)三个值确认num_classes len(char_list) 1且blank_idx len(char_list)。5.3 识别端把“京”识别成“冀”现象蓝底白字的车牌其它字符都对只有省份简称经常错一个字。原因汉字类别在数据集中占比严重不均。CCPD 里安徽、北京、广东的数据多青海、西藏几乎没有模型对低频汉字的置信度天然偏低。解决用合成数据补齐低频省份简称把“藏”“青”“宁”“新”等各合成几百张加入训练集如果嫌麻烦可以在后处理里加一个汉字候选词表让识别结果只从合法省份简称里选这个规则在落地时非常实用。5.4 GPU 显存不够batch 一调小 loss 就爆炸现象yolo 训练时batch16报 CUDA out of memory改成batch4后 loss 曲线震荡剧烈训练无法收敛。原因batch 太小导致 BN 层的均值和方差估计不稳学习率还按原来的大值设置梯度更新方向来回摆动。解决先降学习率lr0按 batch 缩小比例同步降低从 0.01 降到 0.002或者在data.yaml里把训练图统一缩到 512 尺寸显存占用减少约三分之一batch 就不用降太多。不要只调 batch 不动学习率这是新手最容易踩的坑。5.5 onnxruntime 推理结果和 PyTorch 对不上现象PyTorch 权重跑得好好的转成 ONNX 后再推理识别出来的车牌字符变乱。原因转 ONNX 时opset版本和 onnxruntime 版本不兼容导致某些算子的计算结果有细微差异更常见的是输入尺寸写死了94x24但导出时把动态轴漏了推理时输入的尺寸被强制改变。解决导出时固定opset12并把张量的宽高轴声明为动态轴另外 ONNX 的输入是NCHW格式车牌裁剪图是HWC的 OpenCV 顺序推理前记得transpose((2, 0, 1))再unsqueeze(0)。6. 用测试集量化“准不准”别拿两张图说效果要拿指标说事大作业答辩时最尴尬的场景是老师问“准确率多少”你支支吾吾说“感觉还行”。正确做法是准备一个没参与训练的测试集至少 500 张图跑通下面这个评估循环检测阶段统计 mAP识别阶段统计字符准确率最后把两个阶段串起来统计端到端准确率。评估脚本的核心片段可以这样写def evaluate_end_to_end(model_detect, model_reco, test_loader): total 0 correct 0 char_hit 0 char_total 0 for img, gt_text in test_loader: # 步骤1: 检测 dets model_detect.predict(img, conf0.4, imgsz640) if len(dets) 0: continue # 漏检或误检都算整体错误 crop dets[0].crop # 取最高置信度框 # 步骤2: 识别 reco_text model_reco.recognize(crop) total 1 correct (reco_text gt_text) # 字符级准确率 for c_gt, c_pred in zip(gt_text, reco_text): char_total 1 char_hit (c_gt c_pred) end_to_end_acc correct / total char_acc char_hit / char_total return end_to_end_acc, char_acc这里最关键的理解是端到端准确率从来不是检测准确率“乘”识别准确率那么简单而是两个误差叠加后的净结果。检测漏一张这张的识别无论多准都是错识别错一位字符这张牌也判错。所以评估时要分别统计三组数单独检测 mAP、单独识别字符准确率、端到端车牌准确率。某一项低了定位到具体阶段再修。我自己的习惯是跑完评估后把错误样本按类型归一下类——是夜间低照度导致的检测漏框还是倾斜过大导致的识别错位还是边缘车牌字符模糊。分类之后再决定要不要补数据、调增强、换模型而不是盲目重训。这是我做这类项目最大的教训车牌识别系统的瓶颈往往不在模型结构而在你对错误分布是否有清晰的认知。希望帮到你。本文还有配套的精品资源点击获取
返回列表