尧图精选

基于YOLOv8+LPRNet的车牌识别源码实战:从CCPD训练到部署

🕒 发布时间:2026/10/1 16:40:30 📁 来源:尧图网络
简介这套基于深度学习的车牌识别源码出自一个评审分98分的高分毕设项目面向计算机相关专业正在完成大作业、毕业设计的学生以及需要项目实战练习的开发者适用于车牌检测与字符识别全流程的学习、复现和二次开发。项目难度适中内容经导师与助教审定源码均已完成本地编译并通过严格调试确保可稳定运行。包内文件共2000个以1943张车牌jpg图像为主要数据集另有png图片、xml标注文件、Python脚本、说明文档及gitignore、iml等工程配置压缩包整体265.63MB目录结构清晰便于按功能模块查找与阅读。目前已有250人学习/下载。通过该项目读者可以直观理解深度学习车牌识别的数据准备、标注组织、模型训练与结果输出等关键环节同时能借助丰富的真实车牌图像样本扩展训练集、调整参数或直接借鉴其工程结构完成课程设计、毕业设计展示与答辩准备是一套兼具教学与实战价值的完整项目。1. 为什么“基于深度学习的车牌识别源码”还能成为高分毕设先看清这个项目的真实工作量很多人在选题时对“车牌识别”有刻板印象觉得这是已经被做烂的方向再拿出来当毕业设计答辩时容易被评委追问“你的创新点在哪里”。但实际情况是如果把车牌识别当成一个单纯的OCR问题确实廉价可一旦把它拆成“复杂场景下的目标检测 不定长字符序列识别 模型轻量化部署”三个子问题这仍然是一个能打的高分毕设选题。深度学习车牌识别源码的热度始终没降不是因为它简单而是因为它在学术上覆盖CNN、注意力机制、序列建模等知识点在工程上又牵涉数据增强、模型剪枝、推理加速等落地技能恰好是一项“既有论文可写、又有系统可演示”的题目。这篇笔记的读者应该是正在选毕设题目、或者拿到一套源码但不知道怎么改成自己项目的学生。我会从检测到识别到训练部署拆出一个最小可行方案用YOLOv8做车牌区域检测用LPRNet或CRNN做字符序列识别把两段模型串成一条完整流水线。我会把每一条命令、每一个关键参数的来龙去脉讲清楚也会把数据集构建、训练时长、显存占用这些真实约束交代出来让你拿到的不是一段“能跑”的代码而是一个你能讲明白、能改、能复现、能通过答辩的源码项目。2. 车牌识别到底在识别什么两类主流技术路线与中文车牌的特殊之处2.1 端到端识别与两段式识别的选型逻辑车牌识别源码的实现方案业界大概分成两类一类是端到端模型输入整张图片直接输出车牌字符串典型代表是LPRNet和某些基于Transformer的方法另一类是两段式方案先用目标检测模型找到车牌区域再对裁剪出来的车牌小图做字符识别。两者没有绝对的优劣只有适不适合你手头的资源。端到端模型的最大优点是部署简单模型数量少推理链路短但训练难度大。因为模型需要同时学会“找出车牌在哪”和“读出车牌是什么”这两件事的损失函数耦合在一起数据量不够时模型往往会偏向其中一个任务导致整体精度上不去。两段式方案把问题解耦检测和识别各自训练、各自调优出问题时也更容易定位。对毕设来说两段式是更稳的选择——你可以在答辩时分别展示两个模型的精度曲线讲清楚瓶颈在检测还是识别这本身就是一篇论文的结构。从工程实现上看两段式方案里的检测模型几乎必选YOLO系列。之前的YOLOv5版本生态非常成熟能搜到大量车牌检测的预训练权重现在的YOLOv8在C2f结构、Anchor-Free解码头上有明显升级对小目标的召回率更好而且ultralytics库的训练接口统一了数据加载和评估流程。对于车牌这种宽度不一、角度多变的目标YOLOv8的多尺度预测能力是够用的。2.2 中文车牌字符集合与解码规则这是最容易理解错的地方大多数开源的国外车牌识别项目只处理数字和大写字母字符集合只有36类。国内标准燃油车牌要加上省份简称汉字一共是31个省份简称加汉字“学”“警”等特殊字符数字和字母共24个I和O在民用号牌中一般不用整体字符类别在65到70之间。新能源车牌是8位字符传统蓝牌是7位字符检测和识别模型都得同时兼容这两种长度。这个字符集合的特殊性直接影响两个设计决策其一识别模型的输出维度必须覆盖全部中文字符不能用国外预训练模型直接迁移其二车牌字符是定长或准定长的序列如果使用CRNN路线需要在解码时处理可变长度输出而LPRNet这类模型干脆把字符序列长度固定为最大长度用CTC损失来对齐标签。CTC解码时的贪心搜索和Beam Search在车牌这种短序列场景下差距不大贪心就够。2.3 国内公开数据集现状CCPD是首选但背景纯净度需要自己补训练车牌识别模型绕不开数据集这个话题。国内最常用的公开数据集是CCPDChinese City Parking Dataset它采集自合肥市的停车场场景提供了超过20万张带车牌的车辆图像标注信息包含车牌的四角坐标和字符标签。CCPD的车牌类型覆盖蓝牌、黄牌、绿牌新能源、白牌场景包含不同光照、不同角度、不同距离这对训练检测模型非常有利。但CCPD有一个隐含问题它的负样本比较少也就是“画面里有类似车牌纹理的区域但实际不是车牌”的样本不够多。停车场环境下广告牌、空调外机、车灯轮廓都容易产生误检。用CCPD训练出来的检测模型在路边随机场景测试时误检率可能会偏高。我的处理习惯是用CCPD做基础训练再额外采集一些包含干扰物的图片做负样本微调这会让最终源码的鲁棒性明显提升。3. 搭建车牌识别源码的最小可运行方案从数据集准备到检测模型训练3.1 数据准备环节把CCPD转成YOLO格式的目录结构拿到CCPD数据集后第一步不是训练而是把标注格式转换成YOLO需要的txt标签格式。CCPD的标注信息直接编码在文件名里形如“025-95_113-154383_386473-386473_177454_154383_363154-0_0_22_27_27_33_28-70-99.jpg”其中第二个数字段是车牌四个角点的像素坐标。你需要写脚本把这些坐标解析出来换算成归一化的中心点坐标和宽高。以下是一个完整的转换脚本我保留了关键的解析逻辑和边界处理import os import cv2 from pathlib import Path def parse_ccpd_filename(filename): 解析CCPD文件名返回车牌角点坐标和字符标签 parts filename.split(-) # 第二部分是四个角点坐标格式为 x1_y1_x2_y2_x3_y3_x4_y4 points parts[1].split(_) pts [] for i in range(0, 8, 2): x int(points[i]) y int(points[i1]) pts.append((x, y)) return pts def points_to_yolo_bbox(pts, img_w, img_h): 把四角点坐标转成最小外接矩形的YOLO格式 xs [p[0] for p in pts] ys [p[1] for p in pts] x_min, x_max min(xs), max(xs) y_min, y_max min(ys), max(ys) x_center (x_min x_max) / 2.0 / img_w y_center (y_min y_max) / 2.0 / img_h width (x_max - x_min) / img_w height (y_max - y_min) / img_h return f0 {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f} def convert_ccpd_to_yolo(ccpd_dir, output_dir): 批量转换CCPD数据集为YOLO格式 img_dir Path(output_dir) / images lbl_dir Path(output_dir) / labels img_dir.mkdir(parentsTrue, exist_okTrue) lbl_dir.mkdir(parentsTrue, exist_okTrue) for img_path in Path(ccpd_dir).glob(*.jpg): img cv2.imread(str(img_path)) img_h, img_w img.shape[:2] pts parse_ccpd_filename(img_path.stem) yolo_line points_to_yolo_bbox(pts, img_w, img_h) # 复制图片到目标目录 new_img_path img_dir / img_path.name cv2.imwrite(str(new_img_path), img) # 写对应的标签文件 lbl_path lbl_dir / (img_path.stem .txt) lbl_path.write_text(yolo_line)这个脚本的核心处理是points_to_yolo_bbox函数。CCPD给的是车牌的四个角点不是标准正矩形而YOLO的标注格式要求水平矩形框所以这里取四个点坐标的极值构造最小外接矩形。需要注意的是这个转换会丢失车牌的角度信息如果车牌倾斜超过45度矩形框会框进大量背景。对于这种情况我建议在训练检测模型时加入旋转数据增强来弥补ultralytics库自带这个功能在augment参数中设置degrees10即可。3.2 训练YOLOv8检测模型的完整命令与超参数说明数据准备好之后建立一个data.yaml文件声明训练集和验证集路径。然后就可以用ultralytics库训练了。以下是训练命令yolo detect train \ datadatasets/plate/data.yaml \ modelyolov8n.pt \ epochs100 \ imgsz640 \ batch16 \ device0 \ projectruns/plate_detect \ nameexp_ccpd \ pretrainedTrue \ degrees10.0 \ translate0.1 \ scale0.5我在这里选用了yolov8n作为基础模型因为车牌检测属于单类目标检测任务类别少、目标相对固定用轻量骨架就能达到不错的精度。imgsz640是速度和精度的平衡点如果你发现小角度车牌漏检严重可以改成imgsz896但显存占用会从大约3G涨到5.5G。degrees10.0允许模型在训练时随机旋转最多10度这能模拟车辆变道时的姿态变化。scale0.5是缩放增强范围模拟摄像头距离的变化。训练过程中的关键监控指标是Recall和mAP50-95。车牌检测要求高召回因为漏检一个车牌就意味着后面识别环节完全失效。一般训练80个epoch后mAP50能到95%以上此时如果Recall还低于90%优先检查数据标注质量而不是盲目加大模型。训练完成后用yolo detect predict批量跑验证集图片把预测框可视化输出用肉眼检查误检和漏检案例。3.3 检测模型的输出后处理坐标裁剪与角度校正检测模型输出的是矩形框但实际场景中车牌往往是倾斜的。识别模型对倾斜字符的鲁棒性有限所以在把裁剪区域送入识别模型之前最好做一个角度校正。常见做法是检测车牌的四个角点而不是矩形框但YOLO默认只输出水平框。变通方案是对裁剪出来的车牌区域做透视校正前提是你知道车牌在图像中的大致角度。在毕设源码中我的做法是先用YOLO水平框裁剪然后在这个小图上用OpenCV的轮廓查找找到车牌区域的边缘再做透视变换。车牌区域有高对比度的白色边框或铆钉特征这些都能辅助定位。透视变换后的车牌图像会统一缩放到宽240像素、高80像素的比例作为识别模型的输入。这个预处理步骤虽然增加了一点点推理耗时但对识别精度的提升非常明显倾斜超过30度的车牌也能准确识别。4. 车牌字符识别模块序列模型如何从裁剪图中读出字符串4.1 为什么用LPRNet而不是CRNN一个参数更少、更适合中文车牌的方案中文车牌识别模块常见的两个选择是CRNN和LPRNet。CRNN是通用的场景文字识别模型由CNN特征提取层、RNN序列建模层和CTC解码层组成它在长文本识别上表现很好但对车牌这种短序列、字符数不超过8个的任务来说RNN层的收益不大反而带来更大的模型体积和推理延迟。LPRNet是为车牌场景专门设计的轻量化网络。它的核心思路是用CNN直接输出字符序列的概率分布不引入RNN而是通过一个空间变换层和对齐模块来建模字符位置。LPRNet在保持高精度的同时参数量比CRNN小一个数量级CPU上推理一张车牌图大约只要几毫秒。对于毕设源码来说如果你后续要部署到嵌入式设备或者展示实时识别效果LPRNet是更合适的选择。如果你更熟悉CRNN的原理和代码结构用CRNN也完全可行只是需要在论文里说明你选择它而不是LPRNet的理由。4.2 LPRNet源码中的数据加载与标签编码字符映射表是第一个坑LPRNet的实现细节里最容易被忽略的是字符映射表。中文车牌的字符集合有70多个每个字符需要映射成一个整数索引模型输出的概率分布维度就是字符集合的大小。这个映射表的顺序必须固定训练和推理时使用同一份文件否则会出现“模型输出明明正确解码结果却是乱码”的问题。以下是数据加载和标签编码的核心代码展示了如何把车牌字符串转换成CTC训练所需的标签序列import torch from torch.utils.data import Dataset import cv2 CHARS 京沪津渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领学警ABCDEFGHJKLMNPQRSTUVWXYZ0123456789 class PlateDataset(Dataset): def __init__(self, img_dir, label_file, img_width240, img_height80): self.img_dir img_dir self.img_width img_width self.img_height img_height self.samples self._parse_label_file(label_file) def _parse_label_file(self, label_file): samples [] with open(label_file, r, encodingutf-8) as f: for line in f: img_name, plate_text line.strip().split() samples.append((img_name, plate_text)) return samples def _encode_label(self, text): 把字符串转成CTC标签索引序列 label [] for char in text: if char not in CHARS: raise ValueError(f字符 {char} 不在字符集合中) label.append(CHARS.index(char)) return label def __getitem__(self, idx): img_name, plate_text self.samples[idx] img_path f{self.img_dir}/{img_name} img cv2.imread(img_path) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img cv2.resize(img, (self.img_width, self.img_height)) img img.astype(float32) / 255.0 img torch.from_numpy(img).permute(2, 0, 1) label self._encode_label(plate_text) label_len torch.tensor(len(label), dtypetorch.long) label_tensor torch.tensor(label, dtypetorch.long) return img, label_tensor, label_len这段代码里值得留意的是_encode_label函数中ValueError抛出的位置。训练数据里很容易混入特殊字符比如教练车牌照上的“学”字如果不在字符集合里训练时就会报错。我的建议是字符映射表一次就定义完整的集合宁可多定义几个占位字符也不要训练到一半才发现漏字。另一个细节是图像归一化使用float32和/255.0这在飞书文档、GitHub项目的readme里经常被省略但漏掉归一化会导致模型收敛极慢甚至不收敛。4.3 训练识别模型的损失函数与超参数配置LPRNet使用CTC损失函数。CTC的核心思想是允许模型在不同时间步输出重复字符和空白符解码时再通过去重和去空白得到最终字符串。PyTorch中直接使用torch.nn.CTCLoss即可但需要注意它的输入格式——模型的输出是(时间步, 批次, 类别数)目标标签是(批次, 标签长度)这个维度顺序和大多数模型输出相反。训练命令的超参数配置如下import torch.nn as nn import torch.optim as optim criterion nn.CTCLoss(blanklen(CHARS), zero_infinityTrue) optimizer optim.Adam(model.parameters(), lr0.001) scheduler optim.lr_scheduler.ReduceLROnPlateau(optimizer, modemin, factor0.5, patience5) # 训练循环中计算CTC损失的关键步骤 for images, labels, label_lengths in dataloader: log_probs model(images) # 输出形状: (T, N, C) input_lengths torch.full((batch_size,), log_probs.size(0), dtypetorch.long) loss criterion(log_probs, labels, input_lengths, label_lengths) optimizer.zero_grad() loss.backward() optimizer.step()blanklen(CHARS)的含义是把字符集合最后一个索引作为CTC的空白符。zero_infinityTrue的作用是当某个batch的计算结果出现无穷大的CTC损失时将其置零防止梯度爆炸。学习率从0.001开始使用ReduceLROnPlateau等验证集准确率停止上升时把学习率减半。车牌文字识别任务的收敛速度很快训练到20个epoch左右就能看到接近90%的字符准确率只是中文省份简称偶尔会混。如果想再压榨精度可以加入标签平滑label smoothing但对CTC损失来说还需要看pytorch版本是否支持。4.4 识别模型的解码逻辑贪心搜索与序列去重训练完成后推理时的解码逻辑如下模型每个时间步输出一个概率分布取概率最大的类别作为该时间步的预测字符然后连续相同的字符去重再删除空白符。这个过程叫贪心解码。由于车牌序列很短贪心解码已经足够不需要实现复杂的Beam Search。下面这段代码就是推理解码逻辑非常短但坑很多def decode_plate(model_output): model_output形状: (T, C)T为时间步数C为字符类别数 返回解码出的车牌字符串 # 取每个时间步概率最大的类别索引 pred_indices torch.argmax(model_output, dim1).cpu().numpy() # CTC解码去除重复字符和空白符 blank_idx len(CHARS) decoded [] prev_char None for idx in pred_indices: if idx ! blank_idx and idx ! prev_char: decoded.append(CHARS[idx]) prev_char idx return .join(decoded)这里去重的逻辑是idx ! prev_char但要注意如果两个相同字符之间没有空白符隔开比如车牌中连续两个字母“AA”在解码时会被合并成一个“A”。这是CTC机制本身带来的限制对标准车牌影响不大因为车牌字符中不允许出现连续重复的字母。如果你的数据集包含特殊牌照比如使馆车牌有连续字母就需要考虑引入更复杂的解码策略。还有一个常见踩坑是在解码前忘了对模型输出做log_softmax训练时CTCLoss内部会做归一化但推理时模型输出的原始logits不做softmax直接argmax也能得到相同结果只是概率值没有意义这一点只要在代码注释里写清楚即可。5. 完整训练流水线的串联与源码整合从CCPD到可答辩系统5.1 两段模型的训练先后顺序与数据隔离策略训练顺序上我建议先训练检测模型再训练识别模型。原因是识别模型需要大量裁剪后的车牌图像而用检测模型自动裁剪可以省去手工标注的成本。这里要特别注意数据隔离用于检测模型训练的图片不能直接用于识别模型的训练集因为检测模型在这批图上表现最好裁剪出来的车牌图都是正面、清晰、无遮挡的识别模型在这样的数据上学不到遮挡和模糊条件下的特征泛化能力会退化。我在实际项目中把CCPD按9比1划分成检测训练集和检测验证集然后用训练好的检测模型去跑一个更大的未标注图片集自动裁剪出车牌区域人工筛掉低置信度的样本后作为识别模型的训练集。这个过程看起来绕了一圈但能显著提升识别模型在“检测模型实际输出分布”上的表现毕竟检测模型裁剪出来的图和手工裁剪的图在边缘留白、倾斜角度、模糊程度上都有差异。跑通整个流水线后源码的目录结构建议按功能模块来组织不要把所有代码堆在一个文件里plate_recognition/ ├── config/ │ ├── data.yaml # 检测数据集配置 │ └── charset.py # 字符映射表 ├── data_preprocess/ │ ├── convert_ccpd.py # CCPD转YOLO格式 │ └── crop_plate.py # 检测结果裁剪 ├── detection/ │ ├── train_detect.py # YOLOv8训练脚本 │ └── predict_plate.py # 检测推理 ├── recognition/ │ ├── model.py # LPRNet模型定义 │ ├── dataset.py # 数据加载与标签编码 │ ├── train_recog.py # 识别模型训练脚本 │ └── decode.py # CTC解码 ├── inference/ │ └── pipeline.py # 端到端推理流水线 └── requirements.txt每个子模块保持独立inference/pipeline.py是整个系统的门面答辩演示时只需要调用这个文件。5.2 超参数速查表与基线期望值训练开始前我建议把关键超参数和对应效果记录在实验表格里这既是论文的实验章节素材也是答辩时回答评审提问的依据。模块关键超参数推荐值影响与效果检测imgsz640或896尺寸越大对小目标越友好显存翻倍检测epochs80-120超过100轮后mAP增益很小检测degrees10旋转增强倾斜车牌必调检测batch168G显存显存不足时优先降batch而非降imgsz识别img_size240x80车牌字符宽高比的还原识别learning_rate0.001Adam大于0.003时训练震荡识别epochs30-50字符准确率95%以上不需要再训识别blank_index字符表长度必须等于模型输出头C维度的偏移量在普通单卡NVIDIA GeForce RTX 3060级别上检测模型训练约2小时识别模型训练约30分钟。整体代码跑通一次的时间预算在半天左右这在毕设项目中属于性价比较高的配置。6. 避坑车牌识别训练中最常见的五个翻车点6.1 现象检测模型训练损失正常下降但mAP一直为零原因标签文件中的类别ID错误。YOLO格式的txt文件中第一列是类别ID如果从0开始编号没有错但CCPD转换脚本中容易把类别ID写成1而数据集配置data.yaml里定义了nc: 1类别名为0两者不匹配时mAP恒为0。解决打印一张训练样本的标签可视化图用yolo detect train配合plotsTrue参数在生成的labels.jpg中确认标签框是否落在车牌上。6.2 现象识别模型训练时loss下降到某个值后震荡不收敛原因训练数据里存在标签错误特别是“0”和“O”、“1”和“L”混用。CCPD标注的字符偶尔有错在训练时模型会反复在学习正确特征和错误标注之间摇摆。解决在训练前做一次标签清洗把字符置信度过低的样本人工检查一遍或者直接删除包含易混淆字符对0/O、1/I的样本。6.3 现象推理时检测框定位很准但识别结果中汉字大概率错误原因识别模型的训练数据中省份简称的分布不均衡。CCPD来自安徽合肥“皖”字出现频率远高于其他省份模型对低频汉字的特征学习不足。解决做数据采样平衡对低频省份的样本进行过采样或复制增强保证每个省份简称在训练集中至少出现200次。6.4 现象摄像头实时识别时FPS正常但每隔几秒有一帧完全识别失败原因检测模型对运动模糊和过曝帧的鲁棒性不够。训练数据里静态图片多动态模糊样本少。解决在检测训练数据增强中开启motion_blur和gaussian_blur随机概率设为0.1即可。6.5 现象把模型部署到CPU上后识别速度慢到不可用原因模型没有做半精度转换或INT8量化。训练时用的FP32模型参数在CPU推理时计算量过大。解决用onnxruntime导出ONNX模型开启optimization_level99效果明显。如果启动时遇到不匹配修改model类的forward函数使用torch.onnx.export重新导出。7. 一条把毕设做成可演示系统的进阶技巧用ONNX完成检测识别模型的本地推理串联到这里源码已经能跑通训练和测试但距离“答辩现场流畅演示”还差一步用一套纯推理代码把检测和识别串起来并且脱离PyTorch训练框架减少现场环境依赖。import cv2 import numpy as np import onnxruntime as ort # 初始化ONNX会话关闭训练框架依赖 detect_session ort.InferenceSession(yolov8n_plate.onnx, providers[CPUExecutionProvider]) recog_session ort.InferenceSession(lprnet.onnx, providers[CPUExecutionProvider])这段代码说明了两点第一ONNX模型一旦导出推理时就不需要torch了现场部署只依赖onnxruntime和opencv两个库环境安装成本低第二ONNX支持的CPU加速对毕设演示足够。验证方法也简单准备一组没有参与训练的手机随手拍的照片覆盖白天、夜间、地下车库、雨天四个场景统计端到端识别准确率。这个数字比训练集的高分更有说服力评委看重的恰恰是泛化能力而不是训练集上的拟合。以我的经验来看如果室外白天场景端到端准确率能达到90%以上夜间和雨天场景在80%左右这个项目的完成度就已经超过大多数同类毕设了。我在做这类项目时养成的一个习惯是在代码注释里记录每次训练实验的时间、数据和最终指标答辩时展示给评委说服力比空口谈“效果很好”强得多。另一个经验是演示前先跑十张图看看排查依赖库版本问题别到答辩时现场翻车。希望这篇拆解能帮你在这个方向上少走一段弯路。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →