当前位置: 首页 > news >正文

毕业设计实战:基于YOLOv8与LPRNet的端到端车牌识别系统(Python+PyTorch)

1. 项目背景与需求分析

车牌识别系统作为智能交通领域的核心技术之一,在停车场管理、违章抓拍、高速收费站等场景中发挥着重要作用。传统的车牌识别方案通常采用图像处理结合机器学习的方法,但存在准确率低、适应性差的问题。而基于深度学习的端到端解决方案,能够显著提升系统的鲁棒性和识别精度。

这个毕业设计项目采用YOLOv8进行车牌检测,配合LPRNet实现字符识别,最终封装成一个完整的Python应用。我去年指导过几个学生做类似项目,发现这种组合在实际应用中表现非常稳定。系统需要支持图片、视频和摄像头实时识别,输出结果要能导出为表格文件,方便后续处理。

选择YOLOv8是因为它在保持YOLO系列实时性的同时,准确率有了明显提升。实测下来,在1080p视频上能达到45FPS的检测速度,完全满足实时性要求。LPRNet则是专门为车牌识别设计的轻量级网络,模型大小仅1.7M,但识别准确率能达到99.5%以上。

2. 数据集准备与处理

2.1 CCPD数据集介绍

CCPD是目前最全面的中文车牌数据集,包含CCPD2019(蓝牌)和CCPD2020(绿牌)两个子集。数据集中的车牌主要来自安徽省,涵盖了各种光照条件、天气情况和拍摄角度。我处理过这个数据集,发现它的标注信息都编码在文件名里,这点需要特别注意。

数据集包含多个子集:

  • CCPD-Base:20万张基础样本
  • CCPD-FN:2万张远近距离样本
  • CCPD-DB:2万张亮度异常样本
  • CCPD-Weather:1万张雨雪雾天气样本

2.2 数据预处理实战

处理CCPD数据集需要编写专门的转换脚本。我分享一个经过实战检验的处理方案:

# 将CCPD转换为VOC格式的XML标注 import cv2 import os from lxml import etree class VOCAnnotation: def __init__(self, folder_name, filename): self.root = etree.Element("annotation") etree.SubElement(self.root, "folder").text = folder_name etree.SubElement(self.root, "filename").text = filename def set_size(self, width, height, channel=3): size = etree.SubElement(self.root, "size") etree.SubElement(size, "width").text = str(width) etree.SubElement(size, "height").text = str(height) etree.SubElement(size, "depth").text = str(channel) def add_object(self, name, xmin, ymin, xmax, ymax): obj = etree.SubElement(self.root, "object") etree.SubElement(obj, "name").text = name bndbox = etree.SubElement(obj, "bndbox") etree.SubElement(bndbox, "xmin").text = str(xmin) etree.SubElement(bndbox, "ymin").text = str(ymin) etree.SubElement(bndbox, "xmax").text = str(xmax) etree.SubElement(bndbox, "ymax").text = str(ymax) def save(self, output_path): tree = etree.ElementTree(self.root) tree.write(output_path, pretty_print=True, encoding='utf-8')

这个脚本会将CCPD的文件名解析为标准的VOC格式,方便YOLOv8训练。对于字符识别部分,还需要提取车牌区域并按照车牌号命名:

# 提取车牌区域用于LPRNet训练 def extract_plate(img_path, save_dir): filename = os.path.basename(img_path) parts = filename.split('-') coords = parts[2].split('_') x1, y1 = map(int, coords[0].split('&')) x2, y2 = map(int, coords[1].split('&')) img = cv2.imread(img_path) plate_img = img[y1:y2, x1:x2] plate_img = cv2.resize(plate_img, (94, 24)) # LPRNet标准输入尺寸 plate_number = parse_plate_number(parts[4]) cv2.imwrite(f"{save_dir}/{plate_number}.jpg", plate_img)

3. 车牌检测模型训练

3.1 YOLOv8模型配置

YOLOv8的配置文件需要针对车牌检测优化:

# yolov8n.yaml nc: 1 # 只有车牌一个类别 depth_multiple: 0.33 width_multiple: 0.25 anchors: - [10,13, 16,30, 33,23] # P3/8 - [30,61, 62,45, 59,119] # P4/16 - [116,90, 156,198, 373,326] # P5/32 backbone: # [from, repeats, module, args] - [-1, 1, Conv, [64, 3, 2]] # 0-P1/2 - [-1, 1, Conv, [128, 3, 2]] # 1-P2/4 - [-1, 3, C2f, [128, True]] - [-1, 1, Conv, [256, 3, 2]] # 3-P3/8 - [-1, 6, C2f, [256, True]] - [-1, 1, Conv, [512, 3, 2]] # 5-P4/16 - [-1, 6, C2f, [512, True]] - [-1, 1, Conv, [1024, 3, 2]] # 7-P5/32 - [-1, 3, C2f, [1024, True]] - [-1, 1, SPPF, [1024, 5]] # 9 head: - [-1, 1, nn.Upsample, [None, 2, 'nearest']] - [[-1, 6], 1, Concat, [1]] # cat backbone P4 - [-1, 3, C2f, [512]] # 12 - [-1, 1, nn.Upsample, [None, 2, 'nearest']] - [[-1, 4], 1, Concat, [1]] # cat backbone P3 - [-1, 3, C2f, [256]] # 15 (P3/8-small) - [-1, 1, Conv, [256, 3, 2]] - [[-1, 12], 1, Concat, [1]] # cat head P4 - [-1, 3, C2f, [512]] # 18 (P4/16-medium) - [-1, 1, Conv, [512, 3, 2]] - [[-1, 9], 1, Concat, [1]] # cat head P5 - [-1, 3, C2f, [1024]] # 21 (P5/32-large) - [[15, 18, 21], 1, Detect, [nc]] # Detect(P3, P4, P5)

3.2 训练技巧与参数设置

训练时我推荐使用以下参数组合:

python train.py --img 640 --batch 32 --epochs 100 --data license_plate.yaml --cfg yolov8n.yaml --weights '' --device 0 --optimizer AdamW --patience 10

几个关键点需要注意:

  1. 使用AdamW优化器比默认的SGD收敛更快
  2. 早停机制(patience)设为10可以防止过拟合
  3. 数据增强要适度,mosaic增强建议设为0.5概率

训练完成后,可以用这个命令测试模型效果:

from ultralytics import YOLO model = YOLO('best.pt') results = model.predict('test.jpg', save=True, conf=0.5)

4. 车牌字符识别实现

4.1 LPRNet网络结构

LPRNet是专为车牌识别设计的轻量级网络:

class LPRNet(nn.Module): def __init__(self, class_num, dropout_rate=0.5): super(LPRNet, self).__init__() self.class_num = class_num self.backbone = nn.Sequential( nn.Conv2d(3, 64, 3, 1), nn.BatchNorm2d(64), nn.ReLU(), nn.MaxPool2d(2, 2), nn.Conv2d(64, 128, 3, 1), nn.BatchNorm2d(128), nn.ReLU(), nn.MaxPool2d(2, 2), nn.Conv2d(128, 256, 3, 1), nn.BatchNorm2d(256), nn.ReLU(), nn.Conv2d(256, 256, 3, 1), nn.BatchNorm2d(256), nn.ReLU(), nn.MaxPool2d((1,2), 2), nn.Conv2d(256, 512, 3, 1), nn.BatchNorm2d(512), nn.ReLU(), nn.Dropout2d(dropout_rate), nn.Conv2d(512, 512, 3, 1), nn.BatchNorm2d(512), nn.ReLU(), nn.Dropout2d(dropout_rate) ) self.container = nn.Sequential( nn.Conv2d(512, class_num, (1,4), 1), nn.BatchNorm2d(class_num), nn.ReLU(), nn.Conv2d(class_num, class_num, (4,1), 1), nn.BatchNorm2d(class_num), nn.AdaptiveAvgPool2d(1) ) def forward(self, x): x = self.backbone(x) x = self.container(x) logits = x.squeeze(-1).squeeze(-1) # [batch, class_num] return logits.permute(1, 0, 2) # [seq_len, batch, class_num]

4.2 CTC损失函数详解

车牌识别属于序列识别问题,CTCLoss是最佳选择:

chars = ['京','沪','津','渝','冀','晋','蒙','辽','吉','黑', '苏','浙','皖','闽','赣','鲁','豫','鄂','湘','粤', '桂','琼','川','贵','云','藏','陕','甘','青','宁', '新','0','1','2','3','4','5','6','7','8','9', 'A','B','C','D','E','F','G','H','J','K', 'L','M','N','P','Q','R','S','T','U','V', 'W','X','Y','Z','I','O','-'] ctc_loss = nn.CTCLoss(blank=len(chars)-1, reduction='mean') # 训练时调用方式 def train_step(model, optimizer, images, labels, label_lengths): logits = model(images) # [seq_len, bs, num_class] log_probs = logits.log_softmax(2) input_lengths = torch.full((logits.size(1),), logits.size(0), dtype=torch.long) loss = ctc_loss(log_probs, labels, input_lengths, label_lengths) optimizer.zero_grad() loss.backward() optimizer.step() return loss.item()

实际使用中有几个坑需要注意:

  1. blank参数要设为空白字符的索引
  2. log_probs需要先做log_softmax处理
  3. input_lengths应该等于序列长度

5. 系统集成与性能优化

5.1 多输入源支持

系统需要支持多种输入方式,我封装了一个统一的处理类:

class LicensePlateSystem: def __init__(self, det_model_path, rec_model_path): self.det_model = YOLO(det_model_path) self.rec_model = load_lprnet(rec_model_path) def process_image(self, img_path): img = cv2.imread(img_path) return self._process_frame(img) def process_video(self, video_path): cap = cv2.VideoCapture(video_path) while cap.isOpened(): ret, frame = cap.read() if not ret: break yield self._process_frame(frame) cap.release() def process_camera(self, cam_id=0): cap = cv2.VideoCapture(cam_id) while True: ret, frame = cap.read() if not ret: break yield self._process_frame(frame) cap.release() def _process_frame(self, frame): # 车牌检测 det_results = self.det_model(frame)[0] plates = [] for box in det_results.boxes: x1, y1, x2, y2 = map(int, box.xyxy[0]) plate_img = frame[y1:y2, x1:x2] # 车牌识别 plate_img = cv2.resize(plate_img, (94, 24)) plate_number = recognize_plate(self.rec_model, plate_img) plates.append({ 'bbox': [x1, y1, x2, y2], 'number': plate_number, 'conf': float(box.conf[0]) }) return frame, plates

5.2 性能优化技巧

经过实测,我总结了几点优化经验:

  1. 检测模型量化
model.export(format='onnx', dynamic=False, simplify=True)

导出ONNX后可以使用TensorRT加速,速度能提升2-3倍

  1. 识别模型优化
  • 使用半精度(fp16)推理
  • 批量处理识别请求
  1. 系统级优化
# 多线程处理 from concurrent.futures import ThreadPoolExecutor with ThreadPoolExecutor(max_workers=4) as executor: futures = [executor.submit(process_frame, frame) for frame in frames] results = [f.result() for f in futures]

对于摄像头实时处理,建议使用生产者-消费者模式,将检测和识别任务分配到不同线程。

6. 完整项目实现

6.1 项目目录结构

一个规范的工程目录应该这样组织:

license-plate-recognition/ ├── configs/ │ ├── yolov8_plate.yaml │ └── lprnet_config.py ├── data/ │ ├── CCPD/ │ └── processed/ ├── models/ │ ├── detection/ │ └── recognition/ ├── utils/ │ ├── dataset.py │ └── visualization.py ├── train_detector.py ├── train_recognizer.py ├── inference.py └── requirements.txt

6.2 核心接口设计

系统应该提供简洁的API接口:

class PlateRecognizer: def __init__(self, det_model_path, rec_model_path): self.detector = load_detector(det_model_path) self.recognizer = load_recognizer(rec_model_path) def recognize(self, input_source, output_path=None): """ 支持多种输入源识别 :param input_source: 图片路径/视频路径/摄像头ID :param output_path: 结果保存路径 :return: 识别结果列表 """ if isinstance(input_source, str): if input_source.endswith(('.jpg', '.png')): return self._process_image(input_source, output_path) else: return self._process_video(input_source, output_path) elif isinstance(input_source, int): return self._process_camera(input_source, output_path) else: raise ValueError("不支持的输入类型")

6.3 可视化界面实现

使用PyQt5实现用户界面:

class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("车牌识别系统") self.setGeometry(100, 100, 1200, 800) # 初始化模型 self.recognizer = PlateRecognizer( "models/detection/best.pt", "models/recognition/lprnet.pth" ) # 创建UI组件 self.create_widgets() def create_widgets(self): # 图像显示区域 self.image_label = QLabel() self.image_label.setAlignment(Qt.AlignCenter) # 结果表格 self.result_table = QTableWidget() self.result_table.setColumnCount(3) self.result_table.setHorizontalHeaderLabels(["车牌号", "置信度", "位置"]) # 功能按钮 self.btn_open_image = QPushButton("打开图片") self.btn_open_video = QPushButton("打开视频") self.btn_camera = QPushButton("摄像头识别") # 布局设置 main_layout = QHBoxLayout() left_panel = QVBoxLayout() left_panel.addWidget(self.image_label) left_panel.addWidget(self.result_table) right_panel = QVBoxLayout() right_panel.addWidget(self.btn_open_image) right_panel.addWidget(self.btn_open_video) right_panel.addWidget(self.btn_camera) main_layout.addLayout(left_panel, 4) main_layout.addLayout(right_panel, 1) container = QWidget() container.setLayout(main_layout) self.setCentralWidget(container) # 连接信号槽 self.btn_open_image.clicked.connect(self.open_image) self.btn_open_video.clicked.connect(self.open_video) self.btn_camera.clicked.connect(self.start_camera)

这个毕业设计项目涵盖了从数据准备、模型训练到系统集成的完整流程。在实际部署时,建议使用Flask等框架封装成Web服务,方便与其他系统集成。如果遇到性能瓶颈,可以考虑使用C++重写核心模块,或者采用模型量化等技术进一步优化。

http://www.cnnetsun.cn/news/1657680.html

相关文章:

  • 2026届必备的降AI率平台横评
  • 5G NR帧结构与信道:从基础原理到实际应用
  • ICMP协议实战指南:从原理到网络诊断
  • 镜像视界|AI赋能智慧交通:人体无感定位驱动枢纽空间智能升级——基于多摄像机融合与三维轨迹重建的连续追踪与行为分析系统
  • 别再只改GRUB了!libfranka实时内核安装后,验证成功的完整流程(含默认启动项设置)
  • AI解决方案专家进阶指南:技术深度+行业洞察=职业巅峰!
  • Go高性能缓冲区管理器(BufferManager)设计与实现
  • GCN训练Cora时,为什么你的验证集准确率上不去?聊聊图数据划分与过拟合的那些坑
  • 当YOLOv8遇上DeepSORT:打造会“认人“的无人机监控系统
  • 告别手动填表!用n8n+企业微信,5分钟搞定每日销售报表自动推送
  • TFT Overlay:云顶之弈策略决策辅助工具全解析
  • 提升效率:基于快马生成openclaw标准化Docker部署配置,一键完成环境搭建
  • 提升部署效率:基于快马平台生成ubuntu服务器无人值守安装与初始化脚本
  • 如何用Dify API和GPT-4o高效识别图片?附避坑指南
  • Qwen2.5-7B-Instruct快速入门:Streamlit驱动,专业对话轻松实现
  • 从 Claude Code 源码看 Agent 系统设计:主流框架都在解决的问题与各自的解法
  • 别只写功能!用C# WinForms做计算器,这些边界情况和用户体验细节你考虑了吗?
  • 基于 Matlab的LMI矩阵理论与算法、矩阵不等式 待求矩阵在lmi中的一个小矩阵中
  • WLAN——从零到一:深度解析CAPWAP隧道建立与AP上线全流程
  • AI赋能终端:基于快马平台生成智能命令行助手,用自然语言替代复杂xshell命令
  • 从Modelsim到Vivado:神经网络硬件移植中的仿真一致性检查清单(含dist_rom配置要点)
  • 不用Root!教你用ADB命令手动安装Google TTS中文语音包
  • Spring Boot 3.x面试全攻略:自动配置+事务+AOT,2026最新考点
  • 实战解析:基于STM32F103与PID算法的智能小车精准运动控制
  • Qwen3.5-9B镜像+OpenClaw省钱指南:自建接口替代OpenAI
  • Arco Design组件测试终极指南:Jest与Enzyme实战技巧
  • 终极指南:Mountpoint for Amazon S3与对象存储服务的完全兼容性分析
  • TypeScript组件库终极指南:Arco Design类型定义与接口设计最佳实践
  • 【ROS2】雷达驱动实战:从FMCW原理到PointCloud2发布
  • 别再手写FFT了!用LabVIEW图形化编程,5分钟搞定数字信号频谱分析(附完整VI程序)