那阵子需求方给过来的原始描述特别朴素:“门口要统计进出的车是小车、SUV还是大车,别再让保安一边抬杆一边拿本子记了。”等真把项目接下来我才发现,一个基于深度学习的车辆类型检测系统,远不止“训练一个模型”那么简单。数据整理、模型选型、训练调参、UI界面、部署兜底,整条链路每一项都在吃时间。这篇文章我把自己做YOLOv10车辆类型检测系统的完整过程写下来,包括数据集怎么收拾、训练参数为什么那样设、Web界面怎么搭、现场误判又怎么处理。如果你正打算做车辆检测方向的项目,或者只是想快速落地一个带界面的Python识别Demo,这篇应该能帮你省掉不少弯路。
1. 为什么选YOLOv10:从一次“识别车型”的需求说起
1.1 项目起点:不等于“做个检测就行”
最开始我以为这个项目就是接个摄像头、跑个模型、输出结果。等我把需求拆开才发现,需求方真正要的是“分类”,不只是“检测”。他们想知道开进来的是轿车、SUV、面包车、货车还是客车,数据要按时间段统计,现场还得有一个能看的界面,最好能上传一张图就看结果,也能接现场的视频流。
这样一拆,整个项目其实由四块组成:
- 一套干净、类别清晰的车辆类型数据集;
- 一个能同时完成“定位+分类”的检测模型;
- 一个方便非技术人员使用的UI界面;
- 一套能应对现场光线、遮挡、误判的兜底机制。
四块缺一块,现场都会出问题。很多新手项目死在第一块,数据集没收拾干净,后面再怎么调模型都白搭。所以我建议你也按这个顺序推进:先定类别和数据标准,再训模型,最后做界面和部署。
1.2 模型选型对比:为什么是YOLOv10
当时手头可以选的方向其实不少。传统两阶段检测器比如Faster R-CNN系列,精度确实能打,但速度和部署复杂度对一个小项目来说不太友好,现场那台机器跑起来会明显吃力。YOLOv8已经很成熟,社区资料多,但推理阶段还需要NMS(非极大值抑制)后处理,逻辑多一环。最后我选了YOLOv10,核心原因是它在检测头设计上做了个大改动:训练时用一对多标签分配来加速收敛,推理时用一对一标签分配直接输出最终框,整个模型变成端到端,不需要再做NMS。
可以把它理解成一次“排队回答问题的机制升级”。原来的检测器会产生一堆候选框,需要挨个比对这些框是不是指向同一个物体,留下最好的那个;而YOLOv10相当于提前约定好,推理阶段每个物体只出一个框,不再需要额外环节去“挑拣”。工程上少一个NMS,部署代码就少一块,迟延也更稳定。
我整理了一张对比表,代表我当时纠结的几个方向:
| 方案 | 速度 | 精度 | 部署复杂度 | 适合场景 |
|---|---|---|---|---|
| 两阶段检测器 | 偏慢 | 较高 | 较高 | 对实时性要求低的离线分析 |
| YOLOv8 | 快 | 高 | 中(需要NMS) | 通用检测、有成熟部署文档 |
| YOLOv10 | 快 | 高 | 低(端到端) | 实时系统、边缘设备、快速落地 |
选型结论很明确:项目是偏实用的系统,要在普通机器上跑,还需要被人快速接手,YOLOv10的工程收益最明显。
1.3 部署环境决定了模型尺寸
很多同学一上来就想跑最大的模型,觉得准确率一定最高。实际不是这样。项目现场没有专门的高性能显卡,大概率用CPU兜底,这种情况下模型体积直接决定系统能不能用。我最后锁定了两个尺寸做对比:一个轻量版s,一个中等版m。两者在640分辨率输入下,s的CPU推理大概能压到几十毫秒,m则要再慢一些,但小目标上的精度略好。
另外输入分辨率我也固定在640。场地出入口的车辆属于中大型目标,不需要动不动上1280,否则推一帧的耗时翻倍,现场人员根本等不起。模型尺寸和输入分辨率这两个变量要放到真实运行环境里去验证,不能只看官方指标,这是我这次项目里比较深的一个体会。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据集收拾:八成效果都藏在这里面
2.1 数据来源:公开数据加现场实拍
模型效果的上限基本由数据决定,这句话在车辆类型检测上特别明显。我拿到的数据主要分两块:
- 网上公开的驾驶场景检测数据集。这类数据量大、场景多样,但类别定义普遍比较粗,很多数据集只分“car”和“truck”,没有SUV、皮卡、面包车这些细分。
- 自己在现场架的摄像头拍的图像。固定机位,早上、中午、傍晚、夜间各拍一段,专门补充公开数据里缺少的进出闸口视角。
两份数据合起来,总共保留了大约12000张图片,标注框约有4.6万个。类别经过筛选后定了8个:轿车(car)、SUV、皮卡(pickup)、面包车(van)、货车(truck)、客车(bus)、摩托车(motorcycle)、自行车(bicycle)。三轮车、农用车样本太少,强加进去只会让模型困惑,干脆砍掉。
如果你做类似项目,我建议类别数量控制在8到12个之间。太少,业务方觉得不够用;太多,每个类别样本量都可能不够,训练出的模型很容易在易混淆类别之间乱跳。
2.2 清洗与合并:最容易被跳过的一步
这一步我建议无论如何不要跳。公开数据集的标签标准非常不统一,有的数据集把所有四轮小型车都归为car,另一个数据集又把SUV单独列出来,还出现同一个车被标成两个框的情况。
我写了一个简单的标签映射脚本,把所有来源的类别名称统一到上面那8个类。脚本逻辑不复杂,核心就是一张映射表:
python复制label_map = {
"car_hatchback": "car",
"car_sedan": "car",
"suv_cross": "suv",
"pickup_truck": "pickup",
"minibus": "van",
"truck_medium": "truck",
"bus_city": "bus",
"motorbike": "motorcycle",
"bicycle_any": "bicycle"
}
但映射只是第一步,后面还要做三件很琐碎的事:
- 去掉损坏图片、没有标注的图片、以及标签里明显越界的框;
- 过滤掉面积过小的框,比如小于32乘32像素的目标,对出入口场景来说基本是远处噪声,留着反而干扰训练;
- 复查标注明显错位的图片,比如把客车标成货车。
清完之后我统计了每个类别的框数量,发现摩托车和自行车占比很小。这种类别不均衡如果不处理,训练出来的模型会倾向于把一切不确定的都猜成轿车。解决办法是给样本少的类别过采样,同时给它们稍微高一点的损失权重。这些操作听起来不酷,但确实是把最终准确率从“看起来能用”拉到“真的能用”的关键。
2.3 数据增强的克制用法
车辆和通用物体不太一样,车的整体形态是有固定结构的,车头车尾、侧面轮廓都很敏感。所以增强策略我开得比较克制:马赛克(mosaic)、水平翻转、色域扰动、轻度缩放和旋转。马赛克增强我并没有全程开启,训练到后半程就关掉了,理由很简单,马赛克会拼出一些现实中不太可能出现的车辆组合,后期反而干扰收敛。
我没有开的增强包括大角度旋转、极端裁剪。车侧45度角换个角度可能就变成另一个类别,比如面包车和轻卡,旋转多了模型会分不清。数据增强这块,原则只有一条:模拟真实场景可能出现的变化,而不是创造出真实场景里不存在的情况。
3. 训练阶段的配方和坑:按这条路线跑能省一半时间
3.1 训练环境与版本锁定
训练环境这一关,好多项目栽过跟头。模型代码本身跑通不难,难的是环境组合。我这次固定下来的环境是:Python 3.10、一套主流的深度学习训练框架、CUDA 11.8,单卡显存12G。训练前先把核心依赖版本写死,不要随手“最新版”,否则不同版本的算子实现差异会导致同样的参数跑出完全不同的结果,甚至直接报错。
装环境的时候有个小经验:先建一个干净的虚拟环境,再按顺序安装框架核心库、图像处理库、模型仓库依赖。我装的过程中因为版本不匹配踩过一次坑,损失函数出现莫名NaN,后来把某个图像处理库的版本固定回指定版本才恢复。
3.2 关键训练参数为何这样设
YOLOv10自带的默认参数在通用数据集上表现不错,但拿到车辆类型检测这个具体场景,还是要按实际情况调整。我最后用的训练配置如下:
| 参数 | 设置 | 说明 |
|---|---|---|
| 输入尺寸 | 640 | 出入口场景下足够 |
| batch size | 16 | 12G显存刚好能跑 |
| epochs | 140 | 官方预训练权重基础上微调 |
| 优化器 | SGD | 配合余弦退火更稳 |
| 初始学习率 | 0.01 | 微调不算激进 |
| weight decay | 0.0005 | 抑制过拟合 |
| warmup epochs | 3.0 | 让训练平稳起步 |
这里最重要的选择是从官方预训练权重开始训练,而不是从头随机初始化。迁移学习的价值在于模型已经学会了通用的边缘、纹理、形状特征,我要做的只是让它更懂车辆类型这个垂直领域。如果数据量不够大,效果会非常明显,从头训一个目标检测模型需要的数据量至少是这次的好几倍。
优化器我选了SGD而不是ADAM系列。单看收敛速度ADAM确实快,但SGD配合余弦退火在检测任务上最终的mAP往往更稳。这个取舍其实是在“训练时间”和“最终精度”之间做权衡,我这次项目的业务方对训练时长没有硬性要求,所以选精度更稳的方案。
3.3 训练中一直接着观测什么
训练不是把命令丢进去就等结果。我每5个epoch存一次权重,同时记录loss曲线和验证集指标。这里分享一个判断经验:如果训练loss还在下降,但验证集mAP不再涨甚至下跌,说明模型开始过拟合了,此时继续跑意义不大,早停即可。
训练完成后,我在验证集上逐类看了mAP50指标,结果如下:
| 类别 | 验证图片数 | mAP50 |
|---|---|---|
| 轿车 | 2240 | 0.968 |
| SUV | 1530 | 0.931 |
| 皮卡 | 620 | 0.882 |
| 面包车 | 940 | 0.892 |
| 货车 | 1680 | 0.921 |
| 客车 | 720 | 0.902 |
| 摩托车 | 980 | 0.855 |
| 自行车 | 810 | 0.842 |
整体mAP50大概是0.93,对这类项目来说已经不错,但摩托车和自行车明显拖后腿。查了混淆矩阵后,主要问题集中在“轿车与SUV”以及“摩托车与自行车”这两对。前者是很多跨界SUV车身比例和轿车相差不大,后者是远景小目标特征不足。处理方法是补了摩托车、自行车的现场样本,同时把这两类的损失权重略微上调,第二轮训练后分别涨到了0.87和0.86。
4. 把模型做成能用的UI:Web界面、线程和推理引擎那些事
4.1 UI技术方案怎么定的
模型跑通只是第一步,业务方要看界面。我选的是Web方案,后端用Flask搭一个轻量服务,前端做一个简单的操作页面。理由很直接:浏览器访问,不用在每台电脑上装环境,需求方打开网址就能用。
之前我也想过做桌面应用,但一想到要处理不同操作系统的兼容问题,还要专门打包分发,就放弃了。Web方案虽然要处理一些并发问题,但只写一套逻辑,部署在局域网服务器上,所有现场电脑都能访问,省事非常多。
4.2 推理模块的工程化设计
这里有几个工程问题必须想清楚:模型不能每次请求都重新加载,一个模型文件几百MB,每次加载要好几秒,用户根本等不了。所以模型实例做成全局的,服务启动时加载一次,后续请求复用。
另外,现场如果多个人同时上传图片,纯单线程处理会把请求排队排到崩溃。我就用了一个线程池加任务队列,请求进来先排队,后台按顺序处理,处理完再返回结果。不会有多个请求同时把内存塞爆,也不会因为某个超大图片卡住整个服务。
推理模块的流程我简化了一下:
python复制def predict(image_bytes):
image = load_and_preprocess(image_bytes)
results = model.predict(image, conf=0.45)
boxes = parse_results(results)
return render_results(boxes)
逻辑上就是把图片读入、预处理、推理、解析框、返回结果串起来。这个函数是后端所有接口的公共基础。
4.3 模型导出与CPU推理加速
训练好的模型直接放到服务里跑,在CPU上性能一般,尤其并发上来之后。我选择把模型导出成ONNX格式,再用专门的推理库跑。ONNX的好处是推理过程做了算子优化,CPU上比原始PyTorch推理快不少,后续想换硬件加速也方便。
这里要特别提醒一个细节:导出的模型在做预处理时,必须和训练时保持一致。YOLO系列的预处理包括letterbox等比例缩放、归一化、通道顺序转换。很多人导出后直接在CPU上跑,发现检测框偏了,基本都是预处理不一致导致的。
我处理图片的代码大概是这样的逻辑:
python复制def letterbox(img, new_shape=640):
h, w = img.shape[:2]
ratio = min(new_shape / h, new_shape / w)
new_h, new_w = int(h * ratio), int(w * ratio)
resized = cv2.resize(img, (new_w, new_h))
# 补边到640x640
return resized
预处理对了,模型输出才会准。这条建议同样适用于所有YOLO系列模型部署。
4.4 UI核心功能模块和实际效果
界面本身不复杂,但覆盖了三个实际场景:
- 单张图片识别:上传图片,返回画好框、带类别和置信度的结果图,下面附一个识别明细表格;
- 视频流识别:输入现场摄像头的RTSP地址,后端抽帧送入模型,把检测结果持续叠加显示;
- 批量识别:选一个文件夹,自动处理里面的图片,最后导出一份CSV结果,方便后续做统计报表。
识别明细表格里每一行记录了一个目标:类别名称、置信度、框坐标。业务方拿去给保安看,或者导给统计系统,都很直观。
这些功能做完以后,整个项目才算真正“能交付”。模型再好,没有一个好用的皮囊,业务方也用不起来。这一点对做软件项目的人来说,可能比训练一个高分模型更重要。
5. 现场实测的教训:误判集中在哪里,后处理怎么兜底
5.1 光照和视角的真实考验
实验室里跑得再好,拿到现场往往还是会出问题。第一个考验就是光照:出入口有逆光时段,车头全黑,模型会犹豫;夜间虽然有灯光,但车牌区域过曝、车身边缘模糊;还有那种从侧后方开来的车,车身不完整,模型容易误判。
我把置信度阈值从默认的0.25调到了0.45。阈值太低会放进来一堆误检,阈值太高又可能漏掉光照不佳的目标。0.45在实际场景里平衡点比较好,配合后面的多帧投票机制,整体识别反而更稳。
5.2 最容易混淆的几个类型
根据现场一个月的识别记录,最容易出错的是这几对:
- 轿车和SUV。这是最大的一对混淆源,很多跨界车型车身高度差异很小,边框比例几乎一样。
- 面包车和轻卡。车头扁平,侧面轮廓接近,模型经常把两者搞混。
- 客车和重型货车。车身体量接近,如果拍摄角度是从正前方过来,容易误分。
搞清这些混淆后,我就把重点从“继续加数据集”转向了“规则兜底”。加数据当然有用,但现场场景千变万化,不可能每个角度都覆盖。用规则在业务逻辑层修正,成本更低,见效更快。
5.3 多帧投票与业务规则兜底
最简单的兜底策略是连续帧投票。视频流场景下,对同一个目标连续识别5帧,统计5次结果中哪个类别出现次数最多,最终输出这个类别。单帧的偶然误判会被投票机制自然稀释掉。
再配合一个“未知”类别,当置信度低于阈值或者投票结果分散时,系统不强行判一个类型,而是输出“未知”,提醒值守人员人工确认。现场记录下来,这套机制把最终准确率从单帧的88%左右提到了96%上下,误报明显减少。
这个处理思路可以复制到其他检测系统里,规则不复杂,但很实用。
6. 这次项目给我的通用经验
每次做完一个项目,总会沉淀下几条能复用的心得。这次车辆类型检测系统,我最想说的是下面几条。
第一条,先把数据验收干净,再碰模型。模型跑不好,90%的锅在数据而不在算法。类别定义、样本分布、标注质量,这些没有确认清楚之前,训练完再回头看基本都是白费时间。
第二条,把训练脚本沉淀成模板。这次做完以后,我把数据清洗、训练、导出、后处理这套流程做成了一套固定脚本,下次接别的检测项目,换数据集和类别名就能直接开跑。建议你也维护一个这样的小仓库,前期多花一小时,后面能省好几天。
第三条,部署永远按最差环境设计。考虑现场机器只有CPU、没有显卡、还要被多人访问的情况,模型的选型、输入尺寸、并发策略都要提前留出余量,别等交付那天才去优化。
最后分享一个小技巧:UI服务跑在生产环境时,不要用开发服务器直接顶上去,应该用专为生产设计的WSGI容器来运行,并发能力会稳很多。这个小改动看着不起眼,但对长期运行的识别服务来说,差别非常明显。
这套系统的代码、模型、数据集处理脚本我都整理好了,按项目标题里的说明拿过去就能跑。希望这篇记录能帮你少踩几个坑,把更多时间花在真正有趣的事情上。
