前阵子做模拟项目X的时候,我需要一个能自动识别车辆类型的模块,用在停车场的出入口道闸:车来了,不仅要判断“有没有车”,还要认出这是小轿车、SUV、面包车、卡车还是公交车。这套需求听起来简单,真做起来牵扯的东西不少,数据要从头整理,模型要反复调,最后还得给使用者一个能点按钮的界面。最后我选的是YOLOv10深度学习方案,训练了一个车辆类型识别检测模型,配了一套Python写的UI界面,整个项目源码和模型都能直接拿去做二次开发。这篇文章就把整个过程的思路、参数和踩过的坑完整写一遍,给要搭车辆检测系统的同学一个参考。
先说明一下,这里说的车辆类型识别,本质上是一个目标检测任务,而不是单纯的目标分类。因为在实际场景里,车辆在画面里的位置是变化的,我们必须先“框出”车的位置,再判断它是哪一类。YOLOv10这类检测网络天然适合这个场景,一次推理同时输出位置框和类别,不需要额外接分类网络。下面我从方案选型、数据处理、训练、模型导出、UI集成和实测调优六个部分来展开。
1. 从项目需求到方案选型:为什么车辆类型识别用YOLOv10
1.1 先想清楚要识别的车辆类型
不少人在项目一开始就容易犯一个毛病:类别还没定清楚,就急着下载模型跑demo。车辆类型识别看起来只有“车辆”两个字,但细分方式可以差很多。
我这次按停车和道闸场景的需求,把类别定为:
- 小轿车(car):包括两厢、三厢轿车,车身高度相对较低。
- SUV(suv):包括城市SUV、越野车,比轿车高、体量大。
- 面包车(van):车头短、箱体方正,常见于货运和客运。
- 卡车(truck):包括轻卡、重卡,车身长,货斗明显。
- 公交车(bus):车体明显比面包车大,车窗线条平直。
- 摩托车(motorcycle):两轮车辆,目标小但必须单独归一类。
这里有个经验:类别别一上来就分太多,比如把“货车”“半挂”“厢式货”“皮卡”全部拆开,会导致样本量不够,模型反而学不好。我最初只定了5类,后面根据反馈又加了摩托车,效果比较稳。如果你做的系统是用于高速卡口的,可能还需要把“客车”和“货车”按座位数/轴数再细分,那就是另一套标注逻辑了。
1.2 YOLOv10相对前代模型的几个关键变化
为什么选YOLOv10而不是直接沿用YOLOv8或者更老的一代?我当时的判断主要基于部署成本。
YOLOv10最大的一点改进是去掉了NMS(非极大值抑制)后处理。传统检测模型在输出时会生成大量重叠框,必须用NMS把多余的框去掉,而YOLOv10通过一致匹配策略在训练阶段就解决了这个问题,推理时直接输出最终结果。这带来的直接好处是:推理链更短,延迟更低,代码也更简单。
另一个变化是模型结构上的优化。YOLOv10采用了更高效的梯度流传递方式,在保持精度的前提下减少了计算量,对小模型版本尤其友好。我比较了几种模型的体积和速度:
| 模型版本 | 参数量(约) | COCO精度(约) | 推理速度倾向 |
|---|---|---|---|
| YOLOv10n | 2.3M | 中等 | 极快,适合边缘设备 |
| YOLOv10s | 7.2M | 较好 | 快,适合普通CPU/核显 |
| YOLOv10m | 15.4M | 好 | 中等,适合消费级GPU |
| YOLOv10l | 24.4M | 更好 | 较慢,适合服务器 |
| YOLOv10x | 29.5M | 最好 | 慢,适合高算力场景 |
我最终用的是YOLOv10m。原因很简单:我们场景里需要对摄像头画面做实时推理,一张普通消费级显卡能跑到几十毫秒一帧,精度又比小模型高不少,属于均衡选择。如果你的目标平台是嵌入式设备,比如树莓派或者工控机,建议走YOLOv10n或者YOLOv10s路线。
1.3 系统整体架构设计
这个系统不是只训练一个模型就完事,我把它拆成了五块:
- 数据准备:收集车辆图片、清洗、标注、划分训练验证集。
- 模型训练:基于预训练权重做迁移学习,得到车辆类型识别专用权重。
- 模型导出:把PyTorch权重导出成部署友好的格式,方便用推理引擎加载。
- 推理封装:写一个统一的检测函数,输入图片或帧,输出类别、置信度和坐标框。
- UI界面:用Python搭一个桌面界面,让非技术使用者能选择图片、视频或摄像头画面,直接看到检测结果和统计数据。
这里我想强调一个容易被忽略的点:模型训练和推理部署最好是分开的。训练阶段依赖完整的深度学习框架,而实际交付给使用者时,尽量把模型转换成轻量格式,UI界面只负责调用推理接口,这样整个系统不会因为训练环境缺失而跑不起来。
2. 车辆数据集的收集、清洗与标注
2.1 原始图片怎么收集和筛选
模型是“吃数据”的,车辆类型识别系统的效果上限,很大程度上由数据集决定。我这次整理的数据包含大约1.2万张有效图片,每类样本量尽量保持在2000张以上。
图片来源主要有两个:
- 公开的车辆图像数据:网上有不少开放下载的高速公路监控、停车场、交通场景数据集,选那些带自然光变化、角度多样化的图片。
- 实地拍摄补充:我在模拟场地附近蹲了几次点,用手机和普通摄像头拍不同时间段的车流,专门补充夜间、逆光和雨天场景。
这里要特别强调清洗流程。原始图片里经常混着大量废图:完全模糊的、车辆占画面比例极小的、被栏杆/树木遮挡超过一半的、以及同一辆车连续几十帧的高度重复图。这些图片放进训练集只会浪费算力,甚至拉低精度。我清洗之后直接删掉了大约15%的原始图片,宁可少而精,不要多而杂。
2.2 标注与YOLO格式转换
标注用的图形标注工具比较常规:打开图片,给每一辆车画一个尽量贴合车身的矩形框,然后给框指定类别。矩形框最好贴着车的外轮廓,不要把背景大片框进去,也不要切掉车灯或后视镜,那种“差不多就行”的框会让模型训练时反复纠结边界。
标注完成后,要转换成YOLO训练使用的txt格式。YOLO格式每一行代表一个目标:
class_id center_x center_y width height其中center_x、center_y、width、height都是相对图片宽高的归一化值。举个例子,一张1920x1080的图片里,某辆车的框左上角是(960, 200),右下角是(1200, 700),那么框宽是240,高是500,中心点是(1080, 450),转换后就是:
0 0.5625 0.4167 0.125 0.4630类别id从0开始编号,对应我在配置文件里写的names顺序。标注完成后,图片和txt文件要存放在同一目录下,并且保持同名,这样训练框架才能正确匹配。
2.3 训练/验证集划分与数据增强
数据集的目录结构我建议按下面的方式组织,这是训练框架约定的常见结构:
datasets/vehicle/ ├── images/ │ ├── train/ │ └── val/ └── labels/ ├── train/ └── val/我按9:1的比例划分训练集和验证集,没有单独设测试集,因为验证集的指标已经足够反映模型真实表现。划分的时候要按“图片组”切,而不是把所有图片随机打乱后一张一张分,否则同一辆车在不同帧出现时会被同时分进训练和验证集,造成数据泄漏,验证精度虚高。
数据增强方面,我自己保留了几个常用组合:水平翻转、亮度/饱和度微调、马赛克增强和随机缩放裁剪。马赛克增强对提高小目标检测能力很有帮助,因为它把四张不同的图片拼在一起,相当于让模型在更复杂的背景中学习。但这类增强在训练后期要适当减弱,避免目标被拼得太过“抽象”,导致模型在真实场景下适应不良。
3. 训练阶段:参数怎么定,坑怎么躲
3.1 环境准备与预训练权重
训练环境方面,我用的是Python 3.10,配了一张普通消费级显卡,显存12GB。安装深度学习框架的时候要留意版本匹配,具体就不展开了。YOLOv10的源码包解压到工作目录后,先安装依赖库,再确认torch和cuda版本能够正常调用GPU,用一个小推理demo验证环境通了再开始训练。
权重文件建议直接用官方发布的预训练模型作为起点,我用的是yolov10m.pt。这个权重在通用目标检测任务上已经学过大量基础特征,我们只需要在其基础上做微调,让模型学会区分车辆细分类别。从零开始训练不是不行,但需要的数据量和训练时间都会成倍增加,一般人没必要这么折磨自己。
3.2 数据配置文件怎么写
训练前需要写一个数据配置文件,告诉训练框架数据集在哪里、有哪些类别。我的vehicle.yaml大概长这样:
path: /path/to/datasets/vehicle train: images/train val: images/val names: 0: car 1: suv 2: van 3: truck 4: bus 5: motorcycle有几个细节容易踩坑:
path一定写绝对路径或相对训练命令运行位置的正确路径,写错的话训练启动后会提示图片加载不到。names里的类别顺序必须和标注txt里的class_id完全一致,否则训练出来的模型预测结果会张冠李戴。- 不要忘了检查
labels/train和images/train下的文件数量是否一致,漏标的那部分图片虽然不会报错,但会影响训练稳定性。
3.3 训练参数这样设置更稳
训练命令我用的是类似下面的写法:
yolo detect train data=vehicle.yaml model=yolov10m.pt epochs=300 imgsz=640 batch=16 patience=50各参数的具体选择逻辑说一下:
imgsz=640:YOLO系列默认训练尺寸,边长640x640。如果目标车辆在画面里普遍偏小,可以试试896甚至1280,显存够的话能明显提高小目标召回率。epochs=300:训练轮数。我实际训练时没有跑满300轮,因为框架会自动判断模型不再提升时提前停止。patience=50:如果连续50轮验证集指标不再提升,训练会提前结束,这个机制能省下大量时间。batch=16:显存12GB时跑yolov10m比较稳。如果训练时显存溢出,不要硬开大batch,先把batch降到8,或者把cache关掉。optimizer=AdamW:微调阶段我用AdamW比较多,前期loss下降更快。如果你更想要稳定收敛,可以用带动量的SGD,学习率设置成0.01左右。
3.4 训练过程盯住哪些指标
训练过程中不要只看loss曲线,更要关注验证集上的mAP50和mAP50-95。mAP50是预测框与真实框交并比超过0.5时算出的平均精度,mAP50-95则在0.5到0.95之间取多个IoU阈值再平均,后者更严格,差距更容易反映模型的真实泛化能力。
我习惯这样判断模型状态:
- 如果训练loss在降,但验证
mAP逐渐下降,说明过拟合了,优先减少训练轮数、增加增强力度或降低模型复杂度。 - 如果训练loss和验证loss都比较高,并且整体精度低,先怀疑数据标注有问题,而不是急着调参。
- 如果某个类别的精度明显低于其他类别,去检查这个类别的样本量和标注质量,一般情况下是“摩托车”这类小目标样本不够或者框太大。
我这边的结果比较理想,训练到大概180轮时mAP50稳定在了93%左右,mAP50-95在70%上下。对于车辆类型识别这种粗分类任务来说,已经足够用了。
4. 模型推理与导出:把模型变成能用的服务
4.1 用训练好的权重做一次推理
训练完成后,模型权重文件是PyTorch格式,我把它单独放在weights/vehicle_type.pt路径下。推理脚本的调用方式非常直观:
# 以你使用的YOLO工具库为准,通常是通过 YOLO('权重路径') 创建模型对象 model = YOLO('weights/vehicle_type.pt') results = model.predict('test_car.jpg', conf=0.4, verbose=False) for result in results: for box in result.boxes: cls_id = int(box.cls[0]) conf = float(box.conf[0]) xyxy = [round(x, 2) for x in box.xyxy[0].tolist()] print(f"类别: {names[cls_id]}, 置信度: {conf:.2f}, 坐标: {xyxy}")这里的conf=0.4是置信度阈值。默认的0.25在车辆场景下偏松,经常出现把路牌、垃圾桶误判成车的情况。我实测调成0.4之后,误检明显减少,召回也没有明显下降。
4.2 导出ONNX格式方便部署
训练好的PyTorch权重直接交付给使用方有一个问题:对方必须装一个带GPU依赖的深度学习环境,这对很多人来说是劝退级别的要求。我的做法是导出成ONNX格式,这样后续可以用轻量推理引擎去加载模型,CPU也能跑流畅。
导出很简单,一行命令:
yolo detect export model=weights/vehicle_type.pt format=onnx imgsz=640导出后你会在同目录下看到vehicle_type.onnx文件。ONNX的好处是模型结构固化了,不依赖原始训练代码,同时推理引擎做了大量算子优化。实测同一张图片在同样硬件条件下,ONNX格式推理速度比原始PyTorch格式快大概20%-30%。
使用ONNX格式时,输入要做标准化预处理:把图像缩放到640x640,像素归一化到0到1,然后按照模型要求的通道顺序送入。输出是一个二维数组,每一行对应一个检测框,包含类别id、置信度和框坐标。虽然手写后处理麻烦一点,但终端部署的可控性更强。
4.3 检测结果的后处理与车辆统计
在实际业务里,单纯把框画出来还不够,系统需要知道“进来一辆什么车”。我在推理封装层里加了一个统计逻辑:
- 对每一帧图片执行检测。
- 遍历所有检测框,过滤置信度小于阈值的框。
- 按类别进行数量统计,同时用跟踪算法对目标ID去重,避免同一辆车在视频连续帧里被反复计数。
去重这个点特别重要。如果只是对单张图统计,那叫“画面内车辆计数”;如果想要“车流量计数”,就必须跟踪同一辆车跨帧移动。我用的方案是在检测结果基础上接了一个轻量跟踪器,用检测框中心点和框尺寸做匹配。更复杂的场景还可以接ByteTrack这类跟踪算法,让车辆在遮挡后仍能保持同一个ID。
统计结果用一个字典保存:
vehicle_count = {"car": 0, "suv": 0, "van": 0, "truck": 0, "bus": 0, "motorcycle": 0}每次确认有新目标出现时,对应类别加一。UI界面上能实时看到各类型车辆的累计数量。
5. UI界面集成:让非技术用户也能操作
5.1 界面技术怎么选
模型跑通了,但整个系统不能只停留在脚本层面。项目交付时,使用方最关心的不是模型精度,而是界面好不好用:能不能选一张图就看出来是什么车,能不能打开摄像头实时识别,数据统计能不能看清楚。
界面技术路线我对比过几种:
| 技术路线 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|
| Qt的Python绑定 | 桌面端体验好,支持复杂布局,打包方便 | 开发量稍大,界面样式需要调 | 交付给固定客户使用的桌面工具 |
| Tkinter | 零额外依赖,上手快 | 界面简陋,视频刷新体验一般 | 内部小工具、演示demo |
| Streamlit | Web端展示方便,交互组件丰富 | 实时视频流支持较弱,并发处理麻烦 | 快速原型、远程查看 |
| Gradio | 极简代码生成交互界面 | 定制能力有限 | 算法验证、临时演示 |
我最终选了Qt的Python绑定来搭桌面界面。原因是我们需要在界面上同时展示原始画面和检测结果画布,还要支持图片、视频、摄像头三种输入,Qt的布局管理和信号槽机制比较顺手。
5.2 核心界面代码
界面整体结构分成两块:左侧是操作按钮和类别统计信息,右侧是检测结果展示区域。核心逻辑用一个MainWindow类封装。
import cv2 from PyQt5.QtWidgets import (QMainWindow, QFileDialog, QLabel, QPushButton, QVBoxLayout, QWidget) from PyQt5.QtGui import QImage, QPixmap class MainWindow(QMainWindow): def __init__(self, model_path): super().__init__() self.model = load_model(model_path) self.setWindowTitle("车辆类型识别检测系统") self.resize(960, 640) self.label = QLabel("请选择图片或视频", self) self.btn_image = QPushButton("选择图片", self) self.btn_video = QPushButton("选择视频", self) self.btn_camera = QPushButton("打开摄像头", self) self.btn_image.clicked.connect(self.select_image) self.btn_video.clicked.connect(self.select_video) self.btn_camera.clicked.connect(self.open_camera) layout = QVBoxLayout() layout.addWidget(self.btn_image) layout.addWidget(self.btn_video) layout.addWidget(self.btn_camera) layout.addWidget(self.label) container = QWidget() container.setLayout(layout) self.setCentralWidget(container) def select_image(self): path, _ = QFileDialog.getOpenFileName( self, "选择图片", "", "Images (*.jpg *.png *.bmp)" ) if not path: return frame = cv2.imread(path) annotated = self.run_detect(frame) self.show_frame(annotated)实际项目里,按钮的样式、字体大小、画面自适应缩放这些细节都要花时间调。别小看这部分工作,使用方看到的第一眼就是界面,一个干净专业的界面能省掉大量沟通成本。
5.3 图片、视频、摄像头三种输入的统一处理
为了让界面代码不随着输入源不同而疯狂膨胀,我把检测逻辑统一封装成一个函数:
def run_detect(self, frame): results = self.model.predict(frame, conf=0.4, verbose=False) annotated = frame.copy() for result in results: annotated = result.plot(line_width=2, labels=True) return annotated图片和视频调用过程的区别只在于“帧从哪来”:图片读一次,视频循环读。这里要注意,视频逐帧推理时不能直接在UI线程里跑,否则画面会卡到没法看。我用QTimer定时从视频里取帧,把推理放到工作线程里,推理完成后再通过信号回到主线程刷新UI。
摄像头输入同理。打开摄像头后,循环读取画面、推理、显示。因为实时性要求高,我建议把输入分辨率控制在640x640以内,并且对画面做适当裁剪或缩放,让每帧推理耗时稳定在50毫秒以内,体验才跟得上。
6. 实测效果与后续升级思路
6.1 不同场景下的实测表现
系统做完后,我拿了一批真实场景的视频做压力测试,结果如下:
| 场景 | 表现情况 | 说明 |
|---|---|---|
| 白天正常光照 | 很好,几乎无漏检 | 车辆轮廓清晰,类别区分稳定 |
| 夜间有路灯 | 尚可,小目标易漏 | 摩托车和远处轿车容易搜不到 |
| 雨天玻璃反光 | 中等,误检率上升 | 车窗反光会形成类似车身的特征 |
| 逆光场景 | 中等,部分车辆发黑 | 阴影区域对比度低,需要预处理增强 |
| 车辆密集拥堵 | 较好,但帧间ID易跳变 | 检测能出框,统计计数需要跟踪配合 |
夜间是一个绕不开的痛点,单纯调模型效果有限。我后面在推理前加了一个自适应直方图均衡化预处理,对夜间低对比度画面有明显改善,代价是每帧增加了大约两三毫秒处理时间,可以接受。
6.2 踩过的坑与排查过程
这里写几个我印象比较深的坑。
第一个坑是标注框不贴合的问题。有段时间模型在验证集上表现不错,但一上真实场景就疯狂乱框。后来查了上百张图,发现不少标注框把后视镜切了一半,还有的把相邻两辆车框在一个矩形里。这类问题不会让训练报错,只会让模型学得“模棱两可”。解决方法是删掉这些坏标注,重新校准一遍所有类别的框,精度立刻上升了几个点。
第二个坑是训练后期小目标丢失。在拥堵场景里,摩托车和远处的轿车经常检测不到。排查时发现训练框架在最后的epochs会关闭马赛克增强,而我的摩托车样本又偏少,导致模型对这类小目标学得不到位。解决办法是补充了摩托车夜间样本,同时适当降低置信度阈值到0.3,召回率上来了。
第三个坑是视频推理卡顿。最早我是直接在主线程里做逐帧推理,界面像幻灯片一样。后来改成双线程方案:主线程负责UI响应,工作线程负责取帧和推理,再用队列缓冲,界面立刻流畅。
6.3 后续可扩展的方向
这套系统现在还只是完成了“是什么车”这一层判断。我后续已经在计划做几个扩展方向:
- 加入车辆轨迹跟踪和进出口匹配,实现真正的“驶入驶出”统计。
- 增加车牌识别模块,把车型信息和车牌绑定,形成一条完整的车辆通行记录。
- 按颜色细分,比如同时识别车身颜色和车型,在找车场景下很有价值。
- 把模型量化到更低精度,再换成更轻量的部署方式,让系统可以在不带GPU的工控机上跑。
做这个项目最深的体会是,车辆类型识别系统真正的难点往往不在模型本身,而在数据质量、前后处理和工程化交付。YOLOv10让检测模型的门槛降得很低,但要把一个demo变成别人也能上手的系统,需要花的心思一点都不会少。如果你也在做类似项目,建议先用小模型把整条链路跑通,再回来优化精度,这样至少能避免“模型还没训好、工程也写不下去”的尴尬局面。