ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于YOLO的车型识别与计数系统全链路实战指南

基于YOLO的车型识别与计数系统全链路实战指南

1. 从零做一个车型识别与计数系统,到底要解决哪些问题

两年前我第一次拿到类似的需求,客户说得很简单:“帮我统计一下每天经过路口的车,轿车多少、卡车多少。”但真正动起手来才发现,一个“车型识别与计数系统”背后至少牵扯出三件麻烦事:光有识别根本不够,你要区分同一辆车在不同帧里没被重复计数;车型分类不是按品牌而是按车辆长宽高和结构特征去划分;还有最烦人的——部署环境算力有限,模型既要快又要准。

如果你正好也有“识别一个物体并数数量”的需求,不管是对着停车场、高速公路卡口还是园区出入口,这套基于YOLO系列算法的思路基本可以复用。它会涵盖从数据准备、模型选型到训练推理、计数逻辑落地的全链路,也会回答我踩过的坑里最典型的几个问题。这篇文章更适合那种“已经跑通过YOLO官方demo、但不知道怎么把它做成一个真正能交付的功能模块”的人,如果你是纯新手,建议先花两小时把YOLOv5或者YOLOv8的检测demo跑一遍再回来。

对于这类视觉任务,核心点其实不是“用什么模型”,而是“你的类别怎么定义”“计数怎么防止重复”“你的设备跑不跑得动”。与其一上来就追求刷榜精度,不如先把这几个环节理清,后面你走得会顺很多。

2. 项目整体设计与方案选型:不是一味追新版本就完事

2.1 为什么偏偏用YOLO系列

选YOLO系列,本质上是在“精度”和“速度”之间找一个平衡点。早期我用过快两阶段的检测模型,精度确实高,但那是对着服务器显卡来玩的。到了实际路侧项目,一个NVIDIA Jetson设备甚至一台普通工控机的GPU都很弱,实时性直接压垮多阶段模型。而YOLO把“候选区域生成”和“分类回归”合二为一,单次前向就能输出目标框、类别和置信度,在边缘设备上做到25~30FPS完全现实。

YOLO系列发展到现在有很多版本,v5、v6、v8、v9、v11甚至还有各种改进分支。我个人的选型经验是:

  • 如果团队熟悉PyTorch生态、资料多、部署坑少,选YOLOv5或YOLOv8;
  • 如果项目刚起步、目标类别单一、数据量不大,YOLOv8的默认配置能帮你少调很多东西;
  • 对极端小目标(比如远距离的摩托车)有需求时,改进版模型可能更稳,但训练成本和部署复杂度也会上去。

用YOLOv8举个例子,它把C2f模块和Anchor-Free解耦头结合起来,对中等尺寸的车辆目标非常友好。车辆在监控画面里通常不是超小目标,也不是极端长条形,这种目标分布恰好落在YOLOv8最擅长的区间。

2.2 类别设计:先想清楚你真的需要分几种

很多新手一上来就把类别定得非常细:轿车、SUV、MPV、皮卡、面包车、厢式货车、挂车……听着专业,实际上会把自己坑惨。原因有两个:一是标注成本爆炸,二是相邻类别的边界模糊,比如SUV和MPV从正上方俯视角度下根本分不清。

我建议按“车长轴距+结构”粗分类,先把最容易区分的类别做扎实:

  • 轿车(car)
  • 公交车/客车(bus)
  • 卡车(truck)
  • 摩托车/电动车(motorcycle)
  • 自行车/三轮车(bicycle)如果在园区内部,可能还要加“行人”和“非机动车”;如果只是做车流统计,上述四类车辆通常够了。

不要小看这个“分类粒度”的决策。两个项目如果分类粒度不同,数据标注工作量至少差三倍,模型误判率也完全不一样。粗分类下的模型,鲁棒性会好得多,而且对光照、拍摄角度的容忍度高,那是实打实能落地的优势。

2.3 硬件约束反过来决定模型要多大

项目部署平台的算力,必须在选模型之前就定死。我自己遇到过一种情况:模型在服务器上测的时候精度很高,用户一看效果说“可以”,结果到了现场发现设备只支持TensorRT且显存只有4GB,原版的YOLOv8x根本放不进去,最后只能换轻量级模型重新训练,白折腾了两周。

所以选型流程应该是这样:

部署设备类型推荐模型规模期望帧率备注
普通工控机集成显卡或CPUYOLOv5s / YOLOv8n5~15 FPS需要量化或TensorRT加速
Jetson NX / 独显边缘设备YOLOv8s / YOLOv8m20~30 FPS半精度FP16可跑
服务器GPU(常见推理卡)YOLOv8l / YOLOv8x30 FPS以上可同时处理多路视频流

从我自己跑通的项目来看,YOLOv8s是性价比非常高的档位。mAP50一般能到0.9左右,模型文件大小20MB上下,TensorRT加速后延迟能控制在20ms内。如果你的车流量没有极端密集,YOLOv8s足够撑起大部分场景。

3. 核心细节解析与实操要点:数据、标注、训练

3.1 数据集构建:公开数据集打底,自有数据微调

很多人会问:“我是不是找几万张网上图片就能直接训练?”理论可行,但实际效果很差。网络图片拍摄角度五花八门,很多是平视角度的整车照片,而监控场景是俯视或斜视的,视角差异会让模型在真实现场产生很多漏检。我采用的做法是两条腿走路:

第一,用公开数据集做预训练或初始化。像UA-DETRAC、BDD100K、Cityscapes这些数据集都包含车辆类别,虽然有的不是按轿车/卡车/客车细分,但你完全可以把它们当作“车辆检测”的预训练语料来使用,让模型先具备通用的车辆感知能力。

第二,录制自己目标场景的视频,抽帧标注。现场视频才是最有价值的数据,因为它包含了你的相机高度、角度、光线、背景干扰。抽帧频率不要每帧都标,5到10帧抽一帧就行,保证了目标形态多样性又不会太过冗余。最终我整理下来,一个“四类车辆、摄像头位置固定”的项目,大约需要3000到5000张标注图像,每张图平均1.5个目标,这已经能训练出一个实用的模型。

3.2 标注规范:类别边界不一致是模型犯糊涂的根源

标注工作看似简单,其实是决定模型上限的关键环节。

具体到我的项目,我规定:

  • “卡车”是指有独立货箱且长度明显大于家用车的车辆,包括厢式货车、栏板货车;
  • “客车”指车身长度超过6米、载客为主的车辆;
  • “轿车”包含普通三厢轿车和两厢轿车,不包含SUV;
  • SUV单独归为“SUV”还是并入“轿车”,取决于你的分类目标。如果只统计“小型车 vs 大型车”,SUV可以并进去;如果要做精细化交通流分析,SUV就单列。

这里有个血泪教训:标注人员如果对“面包车”和“轻客”的标准不统一,模型训练出来预测结果会非常不稳定。同一个对象在不同标注人员手里一会儿是卡车一会儿是客车,损失函数会无所适从。所以我建议在建标规范时写清楚每一类的示例图和边界情况。宁可少一个类别,也不要边界模糊。

3.3 训练配置与参数调整:按场景动态做策略

用YOLOv8做训练时,有几个参数我基本是固定下发的。输入分辨率在车辆场景会选640,已经有足够信息量了。如果目标特别小(远距离拍摄),把分辨率调到1280做部分滑窗推理也行,但整体计算开销明显变大。

训练epoch数要看数据量。3000张图,我一般跑100~150个epoch,早停机制开着,在验证集loss连续30轮不降就停。batch size受显存约束,8到16都可以接受。数据增强里,我特别推荐开启mosaic和random perspective,这两个增强对车辆场景帮助很大,因为车在画面里会有不同角度、不同尺度;不过要小心不要开太强的hsv扰动,夜间项目里颜色失真会影响车灯检测(如果你不依赖颜色,则还好)。

优化器直接用AdamW或SGD都可以,我的经验是nano和small模型用SGD配合warmup更容易稳定,而m以上的模型AdamW收敛更快。这只是经验,最终以你的验证集结果说话。

3.4 从训练到部署的“最后一公里”:导出与加速

训练结束以后,pyTorch的权重文件是不能直接进生产环境的。我通常会先转成ONNX,再从ONNX转TensorRT Engine,或者直接用YOLOv8自带的导出功能一步到位。

几个容易忽略的细节:

  • 导出时务必固定输入shape,动态shape推理在TensorRT上偶尔会触发隐式优化失败,耽误排查时间。
  • 开启FP16精度,对于车辆识别这种粗粒度任务完全没有问题,精度损失几乎不可见。
  • 如果你部署的平台是Jetson系列,自带TensorRT版本较老时,建议在相同的JetPack环境下导出,避免版本不兼容报错。
  • CPU部署请用OpenVINO或ONNX Runtime,不要硬扛PyTorch原版推理。

4. 实操过程与核心环节实现:完整跑通一个计数系统

4.1 整体代码架构

我习惯把一个计数系统拆成四个模块,这样便于排错和后续维护:

  1. 视频流读取模块:负责打开本地视频或RTSP流,做解码;
  2. 目标检测模块:加载模型,对每一帧做推理,输出检测框、类别、置信度;
  3. 目标跟踪模块:给同一个目标分配唯一ID;
  4. 计数逻辑模块:根据ID和位置判断是否跨线或进入区域,进行计数。

这些模块解耦开之后,如果你想把YOLOv8换成其他检测器,比如RT-DETR或者YOLO11,就只需要替换“目标检测模块”,其他模块不用动。

4.2 检测模块核心代码示例

如果用的是YOLOv8官方库,检测侧代码很短:

from ultralytics import YOLO model = YOLO("best.pt") results = model(frame, conf=0.35, iou=0.5, imgsz=640, device=0) for r in results: boxes = r.boxes for box in boxes: cls_id = int(box.cls[0]) conf = float(box.conf[0]) xyxy = box.xyxy[0].tolist() # 自己决定是否保留 if conf < 0.35: continue

注意这里conf阈值我设了0.35,不算高。如果现场误检多发,可以压到0.5甚至0.6,但召回率会下降。这个数字和你的数据集质量、类别数量强相关,我建议做一次阈值扫描(0.25到0.6之间)再确定。

4.3 跟踪模块:解决“重复计数”的关键

现在请把思维从“识别”切换到“跟踪”。没有跟踪模块,你没法知道“这一帧的轿车”和“上一帧的轿车”是不是同一辆,计数就完全废掉。

我用的跟踪方案很简单:ByteTrack。它处理遮挡和短暂消失的能力比单纯用IoU匹配的DeepSORT要稳得多,而且不需要额外的ReID模型。喂进去的是检测框和置信度,输出的就是稳定的目标ID。在密集车流场景下,ByteTrack配合高帧率视频,ID切换率可以压到5%以下。

核心逻辑大致是:

  • 检测框作为观测值;
  • 卡尔曼滤波预测每个目标在当前帧的位置;
  • 匈牙利算法做检测框和轨迹的最优匹配;
  • 高置信度检测框优先匹配,低置信度框掉帧保留,防止漏跟。

如果你没有接触过这些概念,可以把它理解为“每一帧给每个目标贴了一张带编号的名帖,贴住之后哪怕它走到画面边缘被挡了一下,回来时号码还认得”。

4.4 计数逻辑:虚拟线还是虚拟区域

计数模块设计取决于你的业务场景。最常见的有两种:

  • 虚拟线计数:在画面中画一条线,目标ID第一次跨线时计数一次。适合计“通过量”,比如道路卡口。
  • 虚拟区域计数:在画面中划定某个停车区域或者统计区域,每隔一段时间统计该区域内ID数量,适合停车场余位管理。

具体到跨线判断,核心是判断“目标ID的状态机”:如果目标上一帧在线的A侧、当前帧在B侧,则计数值加一;一旦ID丢失或者反向穿越,要配合“已计数ID集合”避免重复计数。我通常会维护一个已计数的ID哈希集合,只有新ID才允许计数。

这样写可以保证计数不会因为检测抖动而重复。如果你懒得自己写状态机,也可以直接用现成跟踪库里的触发逻辑。

4.5 结构化输出与业务对接

计数结果需要给到用户界面、数据库或者平台端。一般做法是把每一帧检测到的目标信息都攒成结构化数据,后端定时聚合:

  • 每条检测记录包括:时间戳、目标ID、类别、置信度、边界框坐标、经过方向;
  • 后端每隔1分钟做一次聚合,把“这个时间段内新增的ID、每个类别数量”写入数据库;
  • 前端展示曲线图或者大屏数字。

从我的体会来看,与其把这些统计逻辑全写在视觉进程里,不如让识别进程“纯粹负责识别”,把结果丢到消息队列里,由另一个服务负责聚合和存储。这样视觉进程哪怕崩溃重启,不会丢掉已经统计的计数。

5. 常见问题与排查技巧实录:这些坑我真的踩过

5.1 夜间与强光照场景效果崩了怎么办

白天跑得好好的模型,到了夜间可能出现漏检暴增。原因通常是数据集里夜晚图像太少。我给的整改方案有三个方向:

  • 采集夜间视频,用图像增强方式扩充数据,比如随机调暗、加噪,但更管用的是“真的去录夜晚现场”;
  • 尝试用自适应直方图均衡化或gamma校正做预处理,把暗部拉起来再进行推理;
  • 如果现场有大灯眩光,检测框抖动的概率会明显增加,此时适当提高conf阈值虽然会漏掉一些真目标,但整体稳定性反而更好。

最优解永远是“数据里有什么,模型就会什么”。训练前一定要先统计自己采集数据的时段分布,白天和夜晚的比例不能差太多。

5.2 计数会重复,同一个车被数了好几次

我排查过很多次这种问题,90%的情况是跟踪模块没有接好,对应的“已计数ID集合”没有维护好。比如在低帧率视频源(5FPS以下)中,两帧间隔太大,同一个车可能位移超出IoU匹配范围,导致ID切换,于是被当成新车再数一次。

处理办法有几个,按优先级排:

  • 升级相机帧率,至少做到10FPS以上;
  • 用更精细的运动预测模型,比如ByteTrack自带的卡尔曼滤波参数调优;
  • 允许目标短时间“消失又回来”后继承原ID,也就是增加最大丢失帧数。这个值设到10到15帧,对大部分场景都够。

另外,虚拟线的位置千万不要贴着画面边缘。边缘处的目标往往只有半个车身,检测不稳定,ID也容易飘。把线放在画面中部目标完整经过的地方,计数可靠性会明显提升。

5.3 模型在服务器上精度高,部署后精度掉得离谱

这种情况多半是量化环节出了问题。FP16和INT8量化会让小目标特征产生损失,尤其是摩托车、行人这种小框,降精度表现特别明显。

我的建议是:

  • 能上FP16就不强行INT8;
  • 如果设备实在算力太弱必须INT8,需要准备校准数据集,一般是几百张有代表性的图像,而不是随便挑一张图就让TensorRT自动校准;
  • 部署后做一个“A/B误差对比”,统计同一段视频在服务器float32和边缘端推理结果的mAP差异,差异在可接受范围内再放量。

5.4 推理速度达不到实时,怎么定位瓶颈

先用自带的profiler看时间损耗分布。实践中最常见的原因有:

  • 视频解码占了大头:很多工控机软解4K视频流会吃掉CPU,这里可以把输入缩放成720P再送进模型;
  • 后处理卡顿:YOLO的NMS和类别过滤如果写得不高效,也可能拖后腿。使用官方库的端到端推理模式就可以有效规避;
  • 多线程机制不对:读帧、推理、跟踪、计数要放在不同流水线阶段,用队列解耦。否则只要一帧处理慢,整个管线就卡住。

5.5 常见问题速查表

现象可能原因排查切入点解决建议
全部目标都检测不到权重文件错误检查模型路径、类别名重新导出模型或确认加载的ckpt
轿车和卡车混淆标注边界不清检查标注样本边界情况统一标注规范,补充混淆样本
近距离车辆重复计数ID Switch看跟踪ID是否跳变调ByteTrack参数,加大丢失容忍帧数
小目标完全漏掉输入分辨率过低查看检测框置信度分布提升推理分辨率或做分块推理
GPU利用率很低CPU预处理瓶颈检查解码与归一化是否在CPU用GPU解码或优化pipeline
部署后精度下降明显量化损失做FP16/INT8对比测试改FP16或重新校准数据集

6. 我再分享一点经验之谈:模型训练只是这个系统的一半

这个项目走到最后,我最深刻的体会是:别把“识别模型”当项目的全部。模型只是一个部件,真正的系统是“采集数据—标注—训练—跟踪—计数—输出”整条链路。很多东西做的时候觉得很琐碎,比如标注规范里写“挡风玻璃算不算车身范围”“车顶行李架算不算SUV特征”,但恰恰是这些细节,决定了模型在实际场景里是“够用”还是“三天两头出问题”。

另外一个经验是:无论用户一开始说需要多少类别,第一版都要砍到尽量少,先让主流程跑通,再逐步加类别。我自己见过太多项目,光标注就拖了两个月,结果算法工程师连调模型的机会都没有。最理想的状态是“第一版只分轿车、卡车、客车(非机动车看需求),第二天就能跑通demo给用户看,后面再按需扩展”。

如果你正准备在YOLO系列上做类似的项目,我建议你把大部分精力放在数据采集的“场景还原度”和跟踪计数逻辑的“业务一致性”上。好的数据需要贴近现场,好的计数逻辑需要贴近业务。至于模型那头,社区现成的预训练权重和调参经验,足够让你少走一大半弯路。

最后再补充一个小的实战细节:无论你是训练还是推理,都记得固定随机种子。你会发现,复现结果这件事,比调高0.01的mAP重要得多。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进