简介:这是基于YOLOv5构建的车型识别系统完整工程包,包含可运行的源码和基于PyQt5开发的图形操作界面。系统支持轿车、SUV、商务车三种车型以及奥迪、宝马、大众、奔驰、丰田五种品牌识别,并集成摄像头实时识别、历史记录保存与识别目标数量统计功能;同时提供按用户图片训练自定义模型的脚本,适合深度学习初学者、计算机视觉方向学生及有车辆识别需求的开发者参考学习。资源共34364个文件,压缩包约546.93MB,其中以py源码、pyc编译文件、C++头文件、qml界面文件及dll动态库为主,也包含模型权重pth、训练日志events、配置yaml和说明文档等,目录结构较完整,便于按模块查阅。目前已有4715人学习下载。除GitHub公开的YOLOv5基础源码外,系统界面与业务逻辑均为作者原创,可作为课程设计、毕业设计或实际项目二次开发的完整参考方案,也可从中提取界面设计、训练流程与数据标注思路。
1. 车型识别为什么绕不开YOLOv5
停车场收费、出入口闸机、二手车估价、物流园区货车分类……这些场景都有一个共同的硬需求:在摄像头画面里把车辆所属的型号类别认出来,而且必须实时完成。车型识别不是目标检测的简单换皮,它要求模型兼顾俯视、侧视、夜间补光等复杂视角,又能在算力有限的工控机上保持流畅帧率。YOLOv5恰好是这套需求里性价比最高的选择:它比两阶段检测器更快,比YOLOv8更轻量,PyTorch生态下源码结构清晰、资料齐全,最利于做二次开发和界面集成。下面直接讲这套方案怎么落地:从网络结构理解、数据准备、训练自己的数据集,到源码组织与推理封装、操作界面实现,最后给出部署压测时可以调的参数与验证技巧。
2. YOLOv5网络结构与车型识别模型训练
2.1 先看懂YOLOv5网络结构再动手
做车型识别之前,先把YOLOv5网络结构理清。YOLOv5整体由backbone、neck、head三部分组成,常见版本是v5s和v5m。backbone采用CSPDarknet,核心是C3模块——通过跨阶段局部连接减少重复梯度计算,同时用Focus或6x6卷积做下采样。neck采用PANet,在FPN基础上增加自底向上的路径聚合,让高层语义信息与低层空间细节充分融合。head沿用anchor-based检测头,输出三种尺度检测框,分别对应大中小目标。
车型识别中车辆往往占据画面较大面积,但车灯、进气格栅这类细粒度特征能否被有效保留,取决于neck的融合能力。实际使用时我会把默认anchor换成与车辆长宽比更接近的尺寸,这一步往往能让mAP提升1到3个百分点。常见做法是用源码里的k-means脚本对训练集重新聚类anchor,再写回模型yaml配置。
2.2 车型识别数据集:分类粒度决定训练成本
车型识别一般有三种分类粒度。粗粒度只区分轿车、SUV、货车、客车,每类几十张就能出效果。中粒度区分品牌,如大众、丰田、本田,每类需要两三百张。细粒度区分具体型号年份,比如帕萨特2019款与2020款,每类至少上千张,且视角必须覆盖车头、车尾、侧身。粒度选得越高,训练成本和标注成本都会成倍上升,动手前先跟需求方确认现场到底需要认到哪一层。
| 分类粒度 | 类别数示例 | 每类最少样本 | 适合场景 |
|---|---|---|---|
| 粗粒度 | 4~8类 | 50~100 | 出入口道闸、车位管理 |
| 中粒度 | 10~30类 | 200~500 | 停车场品牌统计 |
| 细粒度 | 50类以上 | 1000+ | 二手车估值、保险定损 |
如果项目预算有限,常见做法是先用开源数据集如BIT-Vehicle、CompCars子集做预训练,再用现场摄像头截图补充微调。标注工具推荐LabelImg,导出PASCAL VOC格式后转成YOLOv5要求的txt格式。每张图中车辆标注框应尽量贴合车身外沿,不要包含大面积背景,否则会引入噪声。
2.3 训练自己的数据集:从环境到命令
2.3.1 环境配置与验证
YOLOv5源码对环境依赖并不苛刻,建议Python 3.9以上、CUDA 11.x、PyTorch 1.10以上。安装命令如下:
git clone https://github.com/ultralytics/yolov5.git cd yolov5 pip install -r requirements.txtrequirements.txt会列出torch、opencv-python、matplotlib、pyyaml、pandas、tqdm等依赖。GPU版本要注意先单独安装匹配CUDA的PyTorch,再执行上面的pip install,否则可能装上CPU版,训练速度慢一个数量级。安装完成后验证:
python -c "import torch; print(torch.cuda.is_available())"返回True说明GPU可用。接下来准备数据目录,YOLOv5需要images和labels两个目录,txt标注每行格式为 class x_center y_center width height,坐标归一化到0~1。目录结构如下:
vehicle_data/ ├── images/ │ ├── train/ │ └── val/ └── labels/ ├── train/ └── val/2.3.2 数据目录与标注格式
在yolov5根目录下创建数据集配置yaml文件:
# vehicle.yaml train: vehicle_data/images/train val: vehicle_data/images/val nc: 6 names: ['sedan', 'suv', 'truck', 'bus', 'van', 'pickup']nc是类别总数,names与标注文件里class id的顺序一一对应,顺序颠倒会导致训练后预测结果张冠李戴。我通常会写一个脚本检查labels和images的文件名一一对应,避免出现悬空标注。
2.3.3 训练命令与超参数修改
训练命令:
python train.py \ --data vehicle.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 100 \ --device 0weights指定预训练权重,推荐yolov5s.pt做初步验证;img为输入分辨率,车辆检测用640x640即可;batch受显存限制,显存小于8G时改成8;epochs在车型这类中等复杂度任务上100轮足够收敛。--patience用于早停,当验证集loss连续指定轮数不降时自动停止;--cos-lr配合学习率预热使用能让后期收敛更平稳。
YOLOv5的超参数存放在data/hyps/hyp.scratch-low.yaml中,最常改的是lr0(初始学习率)、mosaic(马赛克增强概率)以及hsv_h、hsv_s颜色扰动。车型识别尤其是侧面车型图片,mosaic增强对提升泛化能力有明显作用,但如果现场采集图片数量少,反而建议关闭mosaic,避免模型看到太多拼接噪声。我的经验是:样本量低于500时关闭mosaic,并把hsv_h、hsv_s调低,防止颜色扰动让细粒度分类失效。
提示:训练时在命令行加--project与--name,把每次实验的权重和曲线分开存放,回滚对比时不用重新跑一遍。
3. 源码项目结构与推理核心代码
3.1 读懂源码目录再改代码
拿到一套基于YOLOv5的车型识别源码,先不要急着跑预测脚本,先看目录结构。常见的组织方式如下:
carbrand_system/ ├── models/ │ ├── common.py │ ├── experimental.py │ └── yolo.py ├── utils/ │ ├── datasets.py │ ├── general.py │ └── torch_utils.py ├── weights/ │ └── best.pt ├── ui/ │ ├── main_window.py │ └── camera_thread.py ├── core/ │ ├── detector.py │ └── recognizer.py ├── data/ │ └── vehicle.yaml └── main.py其中core/detector.py是核心推理封装,ui/下面是操作界面逻辑。网上常见的免费Python源码项目里,车型识别项目结构大同小异,核心区别在于是直接调用detect.py还是封装成类。直接调用detect.py的缺点是每次推理都要重新解析命令行参数、重新加载模型;封装成类则可以长期驻留权重,界面切换视频源时不用重复加载。
3.2 封装一个推理核心Detector类
下面这段代码是车型识别系统最常用的推理封装写法:
import torch import cv2 import numpy as np from models.experimental import attempt_load from utils.general import non_max_suppression class VehicleDetector: def __init__(self, weights_path, device='cpu', img_size=640, conf_thres=0.3): self.device = torch.device(device) self.model = attempt_load(weights_path, map_location=self.device) self.model.eval() self.img_size = img_size self.conf_thres = conf_thres self.names = self.model.names def preprocess(self, src_img): img = cv2.cvtColor(src_img, cv2.COLOR_BGR2RGB) img = cv2.resize(img, (self.img_size, self.img_size)) img = img.transpose(2, 0, 1) # HWC -> CHW img = torch.from_numpy(img).float() / 255.0 return img.unsqueeze(0).to(self.device) def detect(self, frame): x = self.preprocess(frame) with torch.no_grad(): pred = self.model(x)[0] det = non_max_suppression(pred, self.conf_thres, 0.45) results = [] if det[0] is not None: for *box, score, cls_id in det[0]: x1, y1, x2, y2 = [int(v) for v in box] results.append({ 'bbox': (x1, y1, x2, y2), 'score': float(score), 'class_id': int(cls_id), 'class_name': self.names[int(cls_id)] }) return results代码逻辑说明:attempt_load是YOLOv5源码中加载模型的标准入口,能自动处理不同版本权重文件。preprocess把OpenCV读入的BGR图像转成RGB、缩放到640并归一化到0~1。detect方法里non_max_suppression第一个参数是网络原始输出,第二个是置信度阈值,第三个是NMS的IoU阈值。返回的结果是字典列表,界面层直接读取class_name和bbox即可绘制。
这里两个参数容易被忽略:conf_thres设在0.1会导致大量误检框,设在0.6会导致严重漏检,车辆场景推荐0.3到0.35。NMS的IoU阈值0.45在车辆密集时段可以下调到0.4,减少重叠框。另外如果preprocess时缩放了图像,框坐标要按比例还原到原图,常见做法是用utils.general里的scale_coords,我这里直接按原图推理省去还原步骤。
| 格式 | 体积 | 加载方式 | 适用场景 |
|---|---|---|---|
| .pt | 14~40MB | torch.load/attempt_load | 开发调试、PyTorch推理 |
| .onnx | 14~40MB | onnxruntime | 跨平台、CPU部署 |
| .engine | 8~20MB | TensorRT | Jetson/GPU边缘端 |
3.3 视频流与模型推理的同步
操作界面里实时视频流和推理不能放在同一个线程,常见做法是单独开一个线程读摄像头,用队列缓冲帧,界面主线程只负责取帧和推理。代码示例如下:
import threading import queue class CameraThread(threading.Thread): def __init__(self, src=0, max_queue=5): super().__init__() self.cap = cv2.VideoCapture(src) self.q = queue.Queue(maxsize=max_queue) self.running = True def run(self): while self.running and self.cap.isOpened(): ret, frame = self.cap.read() if not ret: break if not self.q.full(): self.q.put(frame) def stop(self): self.running = False self.cap.release()max_queue设为5是有讲究的:队列太长画面会滞后好几秒,停车场道闸场景会出现车已通过、画面还停在上一辆。队列满时直接丢新帧比阻塞读取更合理——实时侦测场景丢帧比延迟更可接受。摄像头断线时read会持续返回False,要在run里加重连计数,超过阈值后尝试重新打开设备。
4. 操作界面:PyQt5与YOLOv5的集成
4.1 界面需要哪些功能区
操作界面是源码项目里最影响交付观感的部分。基于PyQt5做的车型识别操作界面,功能区一般包括四个:左侧画面显示区、右侧识别结果列表、下方视频源控制栏、底部状态栏。识别结果列表用QTableWidget展示车辆类别、置信度、时间戳和截图路径,方便事后按时间检索。控制栏提供打开本地视频、打开摄像头、停止识别、保存截图四个按钮。
PyQt5的UI线程与推理线程必须分离。常见做法是主线程跑事件循环,推理放到独立线程,画面刷新交给QTimer定时器。如果直接在槽函数里调用detect方法,界面会随着推理耗时出现明显卡顿,鼠标操作都响应不过来。
4.2 主窗口核心代码
下面是一段可直接套用的主窗口初始化代码,覆盖加载模型、启动摄像头线程、定时刷新画面:
from PyQt5.QtWidgets import QMainWindow, QLabel from PyQt5.QtCore import QTimer from core.detector import VehicleDetector from ui.camera_thread import CameraThread class MainWindow(QMainWindow): def __init__(self): super().__init__() self.detector = VehicleDetector( 'weights/best.pt', device='cuda', conf_thres=0.3) self.video_label = QLabel(self) self.video_label.setMinimumSize(960, 540) self.camera = CameraThread(src=0, max_queue=5) self.camera.start() self.timer = QTimer(self) self.timer.timeout.connect(self.update_frame) self.timer.start(30) def update_frame(self): if self.camera.q.empty(): return frame = self.camera.q.get() results = self.detector.detect(frame) frame = self.draw_boxes(frame, results) self.show_frame_on_label(frame) self.refresh_result_table(results)代码逻辑说明:QTimer每30毫秒触发一次update_frame,即目标帧率约33FPS。如果推理耗时超过30毫秒,timer会积压调用,导致画面抖动。常见解法是在update_frame入口加一个“上一帧是否还在处理”的布尔标志,正在处理就直接return,丢帧而不是排队。窗口关闭时记得在closeEvent里调用camera.stop(),否则子线程不退出,程序无法正常结束。
4.3 绘制检测框与结果输出
绘制检测框的代码要跟推理解耦,单独写一个方法便于复用:
def draw_boxes(self, frame, results): color_map = { 'sedan': (0, 255, 0), 'truck': (0, 165, 255), 'bus': (255, 0, 0), } for r in results: x1, y1, x2, y2 = r['bbox'] color = color_map.get(r['class_name'], (200, 200, 200)) cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2) label = f"{r['class_name']} {r['score']:.2f}" cv2.putText(frame, label, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) return framecv2.rectangle的坐标用的是原图坐标,因为前面detect方法对原图直接推理,没有做缩放。如果改用了resize预处理,坐标必须乘回缩放比例,否则框会明显偏位。标签文本里置信度保留两位小数,界面观感上比全精度数字干净得多。
4.4 如何通过界面操作完成数据集的自动标注
界面集成自动标注功能,能大幅压低人工标注成本。常见做法是在界面上增加“自动标注当前帧”按钮:点击后模型跑一次检测,检测框直接写入YOLOv5格式的txt标注文件,人工复核修正后再存入正式训练集。写入代码要保持归一化格式:
def export_auto_labels(self, img_path, results, out_txt): img = cv2.imread(img_path) if img is None: return h, w = img.shape[:2] lines = [] for r in results: x1, y1, x2, y2 = r['bbox'] cx = ((x1 + x2) / 2) / w cy = ((y1 + y2) / 2) / h bw = (x2 - x1) / w bh = (y2 - y1) / h lines.append( f"{r['class_id']} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}") with open(out_txt, 'w') as f: f.write('\n'.join(lines))这段代码把界面上的检测结果直接落盘成训练标注。注意只有置信度高于0.5的框才应写入自动标注集,低于阈值的框多半是误检,写进去会污染训练集。实践中我会把自动标注输出目录与手工复核目录分开,界面显示“待复核数”计数,复核通过的文件再移动到train/labels。
5. YOLOv5车型识别系统的压测与部署调优
5.1 模型导出与体积优化
训练结束后的best.pt在YOLOv5s下约14MB,YOLOv5m约40MB。界面程序直接加载pt没问题,但需要提升吞吐时建议导出为ONNX或TensorRT引擎。导出命令:
python export.py --weights weights/best.pt --include engine --device 0engine格式在Jetson上能直接用TensorRT加速,FP16精度下体积减半、单帧速度提升两倍以上。如果现场只有CPU,导出onnx后用onnxruntime加载,比PyTorch推理快20%到40%。
5.2 边缘端算力平台的选择
很多毕设选题会用STM32做边缘端车辆检测,但YOLOv5s在STM32这类MCU上跑一次前向需要数秒,实时性根本达不到。更合理的路线是选带GPU或NPU的开发板,Jetson Nano配合TensorRT推理引擎,FP16精度下单帧约30ms,带车位管理或道闸控制绰绰有余。选型时先确认算力还是功耗优先,再做量化方案,不要一开始就上INT8。
5.3 验证方法与动态阈值技巧
部署完成后不要只看平均帧率,要统计逐帧延迟分布。用benchmark脚本记录每帧推理耗时,观察P99延迟比平均值更有参考意义。参考数值:Jetson Nano FP16下P99应小于50ms,超过时优先检查CPU与GPU之间数据拷贝的频次。
还要重点测试无车辆帧的误检率。把一段空画面循环播放,统计连续出现彩色框的帧数,误检会表现为短暂闪烁,体验很差。我的做法是让conf_thres动态自适应:连续N帧没有高置信度检测时自动提高阈值,检测到车后再降回0.3。把这段逻辑写进主循环的帧后处理里,对比开关前后的误检日志,通常能减少四成以上的空目标闪烁。
本文还有配套的精品资源,点击获取