☰
YOLOv5小目标交通工具检测实战:从漏检近半到mAP 0.86
2026/10/17 12:11:28 网站建设 项目流程

如果你只是在通用公开数据集上跑过YOLOv5的demo,大概率不会觉得“检测轿车、卡车、公交车”是什么难事——这三个类别的目标都够大、够常见,拿预训练权重随便跑都能出框。真正让项目变麻烦的是标题里那三个字:小目标。我去年做这个交通工具检测系统时,第一批测试视频跑出来,远端车道上的小轿车漏检率接近一半,公交车的尾部被识别成卡车,那一刻我才意识到,小目标检测不是“把输入分辨率调大一点”那么简单,它牵扯到数据标注、anchor设计、训练策略、部署逻辑一整条链路。

这篇文章就把这个项目的完整过程拆开讲一遍,从数据集怎么准备、YOLOv5怎么改配置、训练中踩了什么坑,到最后的部署文档怎么组织。适合两类人看:一类是自己手里有类似交通检测需求、正用YOLOv5做方案的开发者;另一类是准备拿目标检测方向做项目、需要一份完整交付物(源码、配套文档、部署说明)的同学。这个项目最后把mAP@0.5从baseline的0.63提到了0.86,算是走过了一条完整的从能用——到好用——再到能交付的路,下面都是实际操作记录。

1. 为什么小目标交通工具检测这么难:问题的本质

1.1 小目标检测的核心矛盾:特征太少与感受野的博弈

先聊一个最基础的认知:YOLOv5默认有三个检测头,分别在8倍、16倍、32倍下采样的特征图上做预测。一个20×20像素的小轿车,经过32倍下采样后连一个像素都占不满,模型在深层特征图里基本看不到它。虽然P3检测头(8倍下采样)保留了更多空间细节,但它的anchor预设和感受野是跟着通用公开数据集中中大型目标的尺寸统计出来的,小目标很难拿到足够多的正样本匹配。

这里有一个很多人忽略的定义问题。在目标检测的通用标准里,面积小于32×32像素的目标被归为小目标。但实际交通摄像头场景中,一辆距离路口80米的轿车在1080P画面里可能只有15×15像素,这已经完全掉出了常规训练分布。所以“看起来不难”的任务,落地时反而最棘手。

损失函数层面还有一个陷阱:小目标即便匹配成功,它在总损失里的占比极低。训练时大目标的回归误差和分类误差会把小目标的梯度信号淹没掉。这就是小目标检测的经典困境——不是模型“看不到”,而是看到了也学不动。

1.2 轿车、卡车、公交车三类目标带来的差异化挑战

三个类别放在一起检测,难点不全是“小”。我梳理了一下实际遇到的情况:

类别常见像素尺寸主要干扰因素最典型失败模式
轿车10×10 到 60×30体积小、密集、易被前车遮挡远端小轿车漏检
卡车40×20 到 200×60车身长、轮廓不规则尾部被识别为公交车
公交车60×20 到 250×80车体图案多、车窗反光与卡车混淆

最让我头疼的是卡车和公交车的类间相似性。从正前方看,两者的轮廓几乎一样,唯一稳定的差异在车头造型、车厢长度和车窗布局。这类细粒度差异对浅层纹理特征依赖很强,模型如果只靠深层语义信息,很容易把公交车尾部粗壮的立柱识别成卡车的货箱边缘。我在实验中发现,这种混淆在低分辨率输入下特别严重,说明小目标检测不仅考验“找得到”,还考验“分得清”。

1.3 真实交通场景里的额外干扰因素

除了目标本身的原因,部署环境的干扰是第二个大坑。我自采视频里有白天强光、傍晚逆光、夜间路灯、雨天水渍反光等情况,每种光照条件对模型的影响都不同。逆光时轿车轮廓和路面融为一体,雨夜时车灯会造成大面积的过曝光斑,这些都给检测器制造了虚假目标。

运动模糊也是一个容易被低估的问题。路口摄像头抓拍高速通过的车辆时,车速快、曝光时间稍长,画面里的车就是一道拖影,小目标的纹理彻底丢失。相机安装角度的影响同样很大——高位俯视镜头下,车辆呈现的是车顶特征;平视镜头下,车身侧面特征更显著。同一款车在两种机位下外观差异极大。如果训练集和部署机位不一致,验证集指标再高也是空中楼阁。

2. 数据集准备:整个系统的地基工程

2.1 数据来源与采集策略:自采数据才是关键

这个项目的数据主要靠自采。通用公开数据集可以拿来做预训练和交叉验证,但它和实际部署场景大概率不匹配——机位高度、分辨率、地域车辆外观都不一样。我最终保留了两部分数据:自采约9000张有效图像,从公开数据中筛选了约3000张补足夜间和雨天场景,总量约12000张。

自采的策略是分时段、分天气、分机位录制视频再抽帧。白天、傍晚、夜间各录一批;雨天单独录一批;机位覆盖高位杆装和路边平视两种;分辨率覆盖1080P和部分4K下采样。有一点要特别提醒:采集素材后,人脸和车牌必须做模糊处理,这是数据合规的底线,也是很多初学者容易漏掉的环节。数据划分也花了心思——训练集9500张,验证集1200张,测试集1300张,测试集从采集时间维度单独隔开,避免同一段视频抽出来的帧同时落在训练集和测试集里。

2.2 标注规范:小目标标注的隐藏陷阱

标注这一步决定了模型的上限,不能用“差不多”的心态来对待。我命令行里规定:矩形框必须紧贴目标外轮廓,四边溢出不超过目标本身尺寸的5%。对于小于8×8像素的目标,直接放弃标注——这个尺寸下的标注误差可能超过目标本身尺寸,模型学到的只有噪声。

截断目标的标注规则也要提前定死。被遮挡或出画面的目标,如果可见部分超过50%,按完整目标的大致范围标注;否则按可见部分标注。但这个“两套标准”会让模型在数据里看到矛盾的标签,容易困惑。这个项目我最终统一采用“按可见部分标注”的策略,并把高度遮挡样本单独放到一个测试子集里观察表现。

还有一个关键规则:卡车与公交车的类别判定标准必须写进标注文档。我定的规则是:封闭式货箱且长度明显大于高度的判定为卡车;载客车厢、侧窗连续排列的判定为公交车。这条规则如果不写清楚,多个标注员对同一辆车的标签就可能不一致,模型自然学不出稳定的边界。

2.3 针对小目标的增强策略:别让mosaic帮倒忙

做增强时我发现一个反直觉的现象:YOLOv5默认开启的mosaic增强对小目标不一定友好。mosaic把四张图缩放到同一画布,小目标在缩放过程中被进一步缩小,反而消失了。所以我把mosaic概率从默认的1.0降到了0.5,同时加大了对单张原始图像的增强比例。

对提升小目标召回最有效的增强手段是copy-paste——把小轿车和卡车的实例裁剪出来,随机粘贴到空旷路面区域。这相当于人工制造了大量“远端小目标”,对训练数据分布的修正立竿见影。另一个有效手段是tile切块:把1080P大图切成640×640或960×960的块来训练,等于变相提高输入分辨率。切块时要保留块间50像素左右的重叠,避免车辆恰好被切在块边界上导致标注信息丢失。

2.4 类别不平衡:轿车太多,卡车和公交车太少

统计训练集标签后发现,轿车占全部目标约70%,卡车约18%,公交车约12%。类别不平衡的直接后果是模型对卡车和公交车的召回明显偏低。我的处理分两步:第一,对卡车和公交车样本做随机过采样,生成增强副本混入训练集;第二,在损失函数里加入类别权重,让稀有类别的分类损失获得更高权重。

但这里要提醒一句:轿车数量多并不完全是一件需要“平衡掉”的事。小目标信号本就稀缺,轿车作为小目标的主要贡献者,需要大量样例才能让检测器在极小尺寸上稳定响应。真正的平衡应该在小目标数量占比上做,而不是机械地让三个类别数量接近。把训练集里小于20×20像素的目标数量占比统计出来——如果这个比例低于15%,小目标检测基本做不起来。

3. YOLOv5配置调整:为小目标改什么才有效

3.1 anchor重设计与匹配逻辑:默认配置不服务小目标

YOLOv5默认的anchor是根据通用公开数据集的目标尺寸聚类出来的,中位目标尺寸在几十到几百像素之间。我的数据集里大量目标只有10×10到40×40像素,默认anchor要么偏大、要么分布密度不足。YOLOv5自带autoanchor功能,基于k-means在训练集上重新聚类anchor。我把输入分辨率设为1280后重新计算,9组anchor整体向小尺寸方向偏移。

anchor组默认宽高(以640输入为参考)重聚类后(以1280输入为参考)用途
小尺度组10,13 / 16,30 / 33,238,9 / 13,17 / 22,29匹配小轿车远端目标
中尺度组30,61 / 62,45 / 59,11935,44 / 55,68 / 78,95匹配近端轿车和中小卡车
大尺度组116,90 / 156,198 / 373,326128,86 / 205,164 / 298,286匹配公交车和大型卡车

重新聚类只是第一步。anchor匹配阶段还有一个宽高比阈值参数,YOLO系列默认要求预测框与真实框的宽高比不超过4.0才判为正样本。小目标在8倍下采样特征图上与默认anchor的宽高比很容易超限,导致匹配失败。我把这个阈值调到了5.0,但要注意,阈值过大也会引入低质量匹配,需要配合后续的box loss权重来平衡。

3.2 输入分辨率与切图策略:对小目标最直接的控制项

如果说anchor决定了模型“往哪个方向学”,输入分辨率则决定了模型“能看清多小的东西”。我的实测数据很直观:640输入下,远端小轿车基本只占几个像素,检测器无能为力;提到1280后,同样目标的特征可辨识度大幅提升。训练阶段我最终用1280分辨率,推理阶段根据部署算力选择960或1280。

如果部署设备算力有限,还有一招是“训练用大图、推理用切块”。推理时把1080P图像切成960×960的块,每块之间重叠50像素,检测结果按原图坐标合并,跨块重复检测的目标用NMS去重。这个方案的显存占用和直接跑整图1280差不多,但切块推理的耗时更高,适合离线批量处理的场景。

3.3 训练超参数配置:从yaml到命令行参数的一次性清单

我把最终跑通的训练配置整理如下,供参考。注意参数不是越大越好,每一项都有取舍。

参数最终取值选择理由
img_size1280小目标分辨率需求,训练耗时约增加60%
batch_size16显存限制与梯度稳定性的折中
epochs150约120轮后val指标趋于平台期
optimizerSGD比AdamW在检测任务上收敛更稳
lr00.01(配合cosine衰减)用公开预训练权重时不需要过大学习率
mosaic0.5降低mosaic比例避免小目标被过度缩小
mixup0.2轻量使用,增强泛化但不破坏空间精度
multi_scale0.5 ~ 1.5多尺度训练提升尺度鲁棒性
amptrue半精度混合训练极大缓解显存压力
预训练权重YOLOv5mm版本是精度与速度的平衡点,s版本小目标特征太弱

迁移学习环节有个值得留意的操作:加载预训练权重后,前10个epoch冻结backbone底层,先把检测头训练起来,防止预训练特征被小样本数据破坏。10轮之后再解冻全部层统一微调,这个节奏比一开始全量训练更稳定。

4. 训练实战:从baseline到指标达标的完整记录

4.1 第一轮baseline:看到真实差距

第一轮我故意用最保守的配置跑:640输入、默认anchor、默认增强策略、YOLOv5m预训练权重。训练100轮后,验证集mAP@0.5只有0.63,mAP@0.5:0.95约0.34,测试集小尺寸目标AP更是只有0.32。

翻看bad case,问题非常集中:远端小轿车大面积漏检,个别近景卡车被识别成公交车,夜间场景几乎全灭。PR曲线显示小轿车类别的召回率极低,说明检测器的问题不是“检不准”而是“根本没检出来”。把漏检样本可视化后发现,这些目标的像素尺寸大多在10~20像素之间,在YOLOv5的P3检测头上只有一个模糊响应。这就是baseline给我的最大价值:明确了瓶颈在尺度适配而非模型容量。

4.2 针对性优化:每一步都对应一个瓶颈

第一轮优化把输入从640提到1280,mAP@0.5直接跳到0.74,小尺寸AP提升到0.51。这一步说明分辨率对小目标最敏感,但训练时间也涨了约60%。第二轮重新聚类anchor并开启多尺度训练,mAP@0.5到0.81,小尺寸AP到0.62。第三轮加入copy-paste增强和tile切块数据,mAP@0.5稳定到0.86,小尺寸AP到0.73。

实验阶段输入尺寸anchor策略关键增强mAP@0.5小目标AP
baseline640默认默认0.630.32
分辨率提升1280默认默认0.740.51
anchor重构1280重聚类多尺度0.810.62
增强与切图1280重聚类copy-paste + tile0.860.73

训练收敛后,我选择confidence阈值0.25、NMS IoU阈值0.45作为部署参数。实际部署时阈值不是固定的——如果业务方更怕漏报,就把confidence降到0.15;如果更怕误报,就提到0.35。这个权衡需要根据场景需求现场调,不能一套参数走天下。

4.3 指标解读:别只看mAP@0.5

很多同学习惯只看mAP@0.5这一个数,但小目标检测场景里这个指标有欺骗性。mAP@0.5只要求在IoU大于0.5时算命中,小目标边框由于本身面积小,位置偏差几个像素就可能让IoU跌到0.5以下。所以还要看mAP@0.5:0.95这种更苛刻的指标。我最终模型的mAP@0.5:0.95约0.45,在交通场景里已经够用,但如果追求学术上的高指标,还得继续在边框回归精度上下功夫。

更实用的做法是把目标按尺寸分组,分别查看小、中、大三档AP。小目标AP和中目标AP分开统计,能直接判断优化手段是否对症。如果小目标AP提升明显、中目标AP没有下滑,说明改动是有效的;如果中目标AP大幅下降,可能说明anchor或增强策略对小目标过于激进,损害了正常尺度目标的表达能力。

5. 部署落地:源码之外的交付物怎么组织

5.1 模型导出与加速:ONNX与TensorRT的取舍

训练完成,模型得从PyTorch环境里“解放”出来,落到部署设备上。我先后试了三种导出方案:PyTorch直接推理、导出ONNX用ONNX Runtime跑、再进一步用TensorRT做加速。

对比结果很直观:ONNX Runtime部署最简单,性能损失小;TensorRT的FP16加速明显,但INT8量化在小目标上掉点严重。原因也很好理解——小目标本身的特征响应幅度就弱,INT8量化把激活值压到低精度后,这些微弱信号更容易被截断。

推理方案单帧耗时(1280输入约)mAP损失适合场景
PyTorch45ms无原型验证
ONNX Runtime32ms约0.1%通用部署
TensorRT FP1621ms约0.2%端侧实时场景
TensorRT INT816ms1%~2%,小目标掉点明显对精度不敏感场景

这个项目最终选择了TensorRT FP16,实时性满足要求,精度损失可接受。

5.2 推理管线设计:从视频流到结构化结果

部署不只是“把模型跑起来”,还包括一套完整的推理管线。我的管线设计很简单:视频解码→抽帧→缩放或切块→推理→NMS→坐标还原→输出结构化结果(类别、置信度、边界框坐标)。

这里有个实践建议:用异步队列把视频解码和模型推理解耦。解码线程不断往队列塞帧,推理线程从队列取帧计算,两个操作重叠后,整体吞吐量能提升约25%。如果后续要做车流量统计,也不必一上来就上复杂的跟踪算法,先做一个简单的跨帧IOU匹配,把同一辆车的检测框关联起来,统计精度在多数场景下已经够用。

5.3 源码+文档+讲解的交付组织:一个能直接复用的模板

如果这个项目需要作为完整交付物交给下游开发方,或者用来写配套文档,我强烈建议从一开始就按规范组织目录。我最终交付的目录结构大致如下:

项目根目录 ├─ weights/ # 训练好的权重、导出模型 ├─ src/ │ ├─ train.py # 训练脚本 │ ├─ detect.py # 推理脚本 │ ├─ utils/ # 工具函数,坐标换算、可视化等 │ └─ configs/ # yaml配置文件 ├─ datasets/ # 数据存放与标注文件 ├─ docs/ │ ├─ 部署文档.md │ ├─ API接口说明.md │ └─ 训练说明.md └─ requirements.txt

部署文档我习惯只写五件事:环境版本清单(Python、CUDA、各依赖库的具体版本)、模型权重获取方式、接口调用示例、性能指标测试方法、常见报错对照表。把这五件事写清楚,接手的人基本能独立跑通。讲解类的PPT则按“问题—方法—实验”三线展开:先说明小目标检测难在哪,再讲怎么改数据、改配置,最后用实验对比收尾。这样组织,即使听的人没做过检测,也能跟上逻辑。

6. 踩坑实录:几条让我印象最深的排查链路

6.1 损失震荡与标注偏移:最隐蔽的数据问题

训练到第30轮左右,train loss开始在小数点后第三位反复震荡,val mAP停滞不升。我先后怀疑过anchor不合适、学习率过高,调整后都没有质变。最终在翻bad case时发现,一批标注框明显偏离车体边缘,偏移量大约在3到8个像素。对小轿车这种小目标来说,5像素的偏差已经让IoU大幅下降,模型在矛盾的标注上不断修正又不断冲突,自然震荡。

这次排查给的教训是:数据质量问题会伪装成训练策略问题。现在我在训练前都会做一次标注可视化抽查——随机抽100张图,把真实框叠加在原图上肉眼检查,远比直接相信标注文件可靠。

6.2 验证集mAP很高但实拍效果差:分布不一致的陷阱

模型在验证集上mAP@0.5已经到0.86,我满心欢喜去实测一段全是远端小车的视频,结果漏检近一半。冷静下来统计后发现,训练集里小于20×20像素的目标只占8%,而实测场景中这个比例超过40%。模型压根没见过这么极端的分布。

解决办法是重新补充机位更高的俯视数据,并把采集间隔从每10帧抽1帧改成每5帧抽1帧,多保留连续帧中的小目标样本。最终补了约2000张后,实测场景的召回率才追上来。这件事让我养成了一个习惯——统计训练集和测试集的小目标占比是每次实验的必要步骤。

6.3 显存不足与训练节奏:先跑通再优化

用单卡16G显存训练1280分辨率时,第一次直接报CUDA out of memory。我的处理顺序是:batch从16降到8,打开梯度累积补偿batch变小带来的梯度不稳定;开启AMP混合精度;再把数据加载的pin_memory打开。如果还想继续压显存,关闭mosaic增强是最后的手段,虽然对小目标不利,但至少能先把流程跑通。

这个项目最大的节奏经验是:不要一上来就追求全配置拉满。先用640输入、小batch把整套代码跑通,确认数据、模型、评估链路没问题,再逐步开启大分辨率、多尺度、高级增强。每开一个选项就评估一次,出问题立刻能定位是哪一步引入的。

写在最后的一点个人体会

做完整套系统后,我对“小目标交通工具检测”的评价是:模型选型反而不是成败关键,YOLOv5足够完成任务。真正的制胜要素有三件:数据里小目标占比够不够、anchor和输入分辨率有没有为小目标专门调整、训练集与实测场景的数据分布是否一致。三个要素缺一个,最终效果都会打折扣。

最后分享一个小技巧:把训练集和测试集中小目标的像素尺寸分布打印出来,对比一下两边的比例和范围。如果测试集小目标占比远高于训练集,模型效果大概率会崩,这条规律比任何mAP数字都直观。做检测项目时,我每次都会先跑这一步,能省下后面大量返工时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询