做小目标检测和做大目标检测完全是两码事。前一阵帮A同学调试一个基于YOLOv5的小目标交通工具检测系统,类别只有三类:轿车、卡车和公交车。一开始以为套个预训练模型,标一批数据,训练出来就能跑;实际推上去才发现,远处车辆只有几十个像素大小,轮廓模糊,特征被背景吞掉,直接用默认参数根本不够用。折腾了数据增强、模型结构、输入分辨率和推理阶段之后,才把漏检率压下来。这篇文章把项目从需求拆解、数据准备、模型调整、训练评估、部署,再到配套论文和讲解文档的组织思路完整梳理一遍,希望能给正在做类似毕业设计、课程设计或交通场景落地开发的朋友提供一份能直接参考的经验。
1. 项目需求拆解与整体方案设计
1.1 用户真正要的是什么样的系统
这种带“源码+论文+部署文档+讲解”的交付型项目,要求往往比单纯写一段代码更复杂。硬性功能很明确:输入一张道路监控图或视频帧,输出三类车辆的边界框、类别和置信度,类别是轿车、卡车、公交车。看起来只要训练一个检测器就行,但实际交付时要考虑的维度更多。
我会先把这个项目拆成四个层面来看。第一是检测效果,小目标召回率不能太低,尤其是远处车辆;第二是代码可读性,工程要结构清晰,别人拿到之后能快速跑通;第三是扩展性,后续要换数据类别、调推理接口,不能伤筋动骨;第四是文档完整性,论文、部署文档、讲解材料要覆盖项目全生命周期。
很多时候用户并不真的需要一个“最先进”的模型,而是需要一个“能跑、能讲、能改、能交付”的完整案例。所以系统的核心设计目标不只有精确率,还包括稳定性和可复现性。数据集划分、随机种子、训练参数、评估指标都必须能复盘,论文里的每一张图都来自真实实验记录,而不是事后编造。
1.2 为什么选YOLOv5而不是更新的模型
YOLOv5在这类项目里的地位很稳,虽然新版本不断出现,但它仍然是资料最全、社区问题沉淀最充分的目标检测框架之一。选择它有几个非常现实的原因。
首先是代码结构清晰。模型定义用配置文件描述,训练评估入口固定,对刚接触目标检测的人比较友好。论文里要写技术路线,YOLOv5的CSPDarknet骨干网络、PANet特征融合结构都有对应公开资料,讲解起来容易形成完整逻辑链。
其次是部署链路短。训练得到的PyTorch权重可以轻松导出为ONNX或TensorRT格式,CPU场景还能用OpenCV的DNN模块加载。这个特性对“部署文档”交付特别重要,因为我不会希望为了一个简单接口引入一堆重型依赖。第三个原因是硬件门槛适中。YOLOv5s版本在主流显卡上训练成本可控,比起更大规模的模型,更符合学生项目和中小型工程场景的实际情况。
1.3 系统模块怎么划分
整个系统我习惯拆成四个独立模块,模块之间只通过目录和接口通信。
数据模块负责整理图片、标签、类别映射关系,输出标准YOLO格式的训练集和验证集。训练模块读取数据配置,执行训练、验证、可视化,输出best.pt权重和训练日志。推理模块负责加载权重,对图片、视频或视频流做检测,绘制结果并统计数量。服务模块把推理能力封装成HTTP接口,方便前端调用或接入到更大的业务系统中。
这种拆分的好处是调试时能快速定位。推理结果不对,先检查数据标注和预处理,再检查模型权重和后处理,不用在混乱的代码里来回翻。论文里的架构图也直接按这四个模块去画,逻辑清楚,答辩时也容易讲。
2. 小目标车辆检测的难点与选型分析
2.1 小目标到底难在哪里
先说一个基本标准。在目标检测社区里,像素面积小于32x32的目标通常被视为小目标。道路监控画面中,一辆位于画面远端或边缘的轿车,宽度往往只有20到30个像素,这就是很典型的小目标。
小目标难检的核心原因有两个。第一是信息量不足:车辆缩小之后变成一团模糊色块,车牌、车灯、车窗等判别特征全部丢失。第二是背景干扰:一个模糊色块可能是车,也可能只是路面的阴影、井盖、隔离墩或者绿化带的一部分。深度学习模型本质上是靠特征做决策,当目标特征本身已经很少,模型就需要更强的浅层细节和上下文信息来辅助判断。
常规目标检测能轻松识别的大卡车,在版画边缘可能变成一小条深色区域,很容易被当成背景忽略。所以项目能不能成,很大程度上取决于对小目标问题的重视程度,而不是单纯比谁的网络更强。
2.2 YOLOv5里的组件对小目标检测的影响
YOLOv5的网络结构由骨干网络、特征融合和检测头三部分组成。骨干网络负责提取不同尺度的特征;特征融合层把深层语义信息和浅层位置信息结合起来;检测头在三个特征层上输出预测结果。
默认配置会带来一个问题:小目标主要依赖浅层特征,但浅层特征经过多层下采样后,位置细节被逐步压缩。YOLOv5的输出层通常是8倍、16倍、32倍下采样,分别对应80、40、20的网格尺寸。大目标在低分辨率网格上效果很好,小目标则更需要80×80这层特征。如果输入分辨率只有640,再经过32倍下采样,一个20像素的车在深层特征图上可能只剩1个点,检测头基本无计可施。
针对这个结构,常见的解决办法是提高输入分辨率,让目标在下采样后仍然保持足够像素;或者增加更浅层的检测头,让模型直接在4倍下采样特征上预测。这两个方向我都会在后面的优化章节里详细展开。
2.3 轿车、卡车、公交车三类的特征差异
三类车的长宽比和外表有明显区别。轿车通常扁而长,车顶轮廓低,尾部接近地面;卡车车身高大方正,很多时候是矩形轮廓,货箱尺寸大;公交车也很方正,但整体长度比卡车更长,车窗分布规律,侧面线条更连续。
但在实际监控视角中,如果拍到的是正后方视角,卡车和公交车会特别容易混淆。两者都是高大的矩形,只有长度、高度比例和细节上的差别。轿车的问题更多是漏检,而卡车与公交车的问题则主要是互相误判。
所以我在标注阶段就会约定清楚:集装箱半挂类、货箱类统一标成卡车;车身长、有明显车窗排布的客运车辆标成公交车;普通家用车标成轿车。边界框要贴合每类车辆的可见轮廓,被遮挡时框出可见主体,不要框出半个车壳再加一截背景。
3. 数据准备、标注与增强
3.1 数据来源与数据量规划
数据来源主要采用公开道路数据加自己采集的监控视频帧截图。这里不推荐直接用网上下载的混杂图片,因为容易出现大量重复帧、标注格式混乱、场景单一的问题。
在数据量规划上,我建议三类合计至少5000到10000张图片,最低不要低于5000张。5000张情况下还要特别关注类别均衡,轿车样本多、公交车样本少时,模型很容易对公交车视而不见。数量不足时,宁可每类各1500张,也不要轿车4000张加公交车500张的组合。
划分比例用8:1:1比较稳妥。训练集负责学习特征,验证集用来早停和挑选最优权重,测试集在最后做一次公正评估。要注意验证集和测试集中的视频片段不能和训练集来自同一段连续画面,否则相同场景几乎一样,评估出来的指标会虚高,真实场景一跑就露馅。
3.2 标注工具与小目标标注要点
标注工具方面,可以选用支持本地部署的开源标注工具,例如带自动跟踪和预标注能力的那类。车辆目标比较规整,半自动标注能显著提高效率。但自动标注结果一定要二次检查,尤其是小目标,边界框差几个像素都会影响训练质量。
小目标标注有一条实战经验:放大图像到200%再框,不要盯着缩略图随意拉框。如果目标像素小于10×10,认真判断是否保留这条标注;比如确实能看出车顶轮廓就标,只剩一团无法分辨的色斑就宁可删掉这张图。标签里面混入大量噪声框,会让模型对“什么是目标”产生极端错误的理解。
YOLO格式的标签相对简单,每行是“类别 中心点x 中心点y 宽度 高度”,坐标值都除以图像宽高做了归一化。检查标签时重点看宽高是否为0、坐标是否越界、类别编号是否和配置文件对应。这些低级错误会直接导致训练损失异常。
3.3 面向小目标的数据增强策略
YOLOv5自带的Mosaic增强对小目标不完全友好。Mosaic会把四张图拼接后随机裁剪,如果裁剪区域避开小目标,那么这几张小目标实际上就被丢弃了。我的做法是降低Mosaic概率,或把小目标占比高的图像排除在Mosaic之外。
复制粘贴增强是一个非常有效的手段。把标注好的车辆区域从原图中抠出来,随机旋转、缩放后粘贴到其他背景图上,同时自动生成新的标签。这样既增加了小目标数量,又增加了目标出现的环境多样性。要注意粘贴时不要让车辆悬空在明显不可能的位置,例如跨到天空或完全遮挡在树冠里。
多尺度训练也属于增强。训练过程中每固定轮数切换一次输入分辨率,比如从640切到960再切到1280,模型对不同大小目标的变化会更鲁棒。不过这对显存有一定要求,后续会提到怎么降低训练开销。
4. 环境准备与YOLOv5训练全流程
4.1 环境配置与依赖安装
我习惯用独立虚拟环境进行训练,避免把系统Python环境搞乱。推荐使用Python 3.9或3.10,配套安装PyTorch 2.x版本的稳定版,显卡驱动已适配的机器直接用CUDA版本。
创建环境的基本命令如下:
conda create -n yolo python=3.9 -y conda activate yolo pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install -r requirements.txt安装完成后不要急着训练,先用一张图片跑通前向推理,确认CUDA设备能被PyTorch正常访问。很多问题不是出在模型,而是显存驱动不匹配、依赖版本冲突或者路径错误。花十分钟验证链路,能省下后面一整天的排查时间。
4.2 数据集配置与目录检查
数据目录建议按官方默认结构组织:
datasets/vehicle/ images/ train/ val/ labels/ train/ val/labels目录里每张jpg对应一个同名txt文件。图片放在images的train目录下,标签放在labels的train目录下,YOLOv5训练时会自动匹配文件名。
在datasets/vehicle目录下新建一份vehicle.yaml文件:
path: datasets/vehicle train: images/train val: images/val nc: 3 names: ['car', 'truck', 'bus']类别顺序必须严格和标注文件里的编号一致。训练之前写一个小脚本统计每类目标数量和标签格式,发现某个类别数量异常低或标签文件为空,就要先处理数据,而不是直接启动训练。
4.3 训练命令与关键参数解释
训练入口是YOLOv5的train.py,我的基础命令如下:
python train.py \ --data data/vehicle.yaml \ --weights yolov5s.pt \ --img 1280 \ --batch 8 \ --epochs 120 \ --device 0 \ --name vehicle_small \ --hyp data/hyps/hyp.scratch-low.yaml这里几个参数对小目标影响很大。--img 1280意味着输入分辨率从默认的640提高到了1280,图像被放大后,小目标的像素信息显著增加。代价是显存占用上升,所以batch只能从16降到8。
--weights yolov5s.pt用到预训练权重。迁移学习让模型从一开始就具备基础视觉特征,比从零训练收敛快很多。如果显存特别紧张,可以选yolov5n.pt或者yolov5tiny,检测精度会略有下降,但节省显存效果明显。
--epochs不需要一直写很高。模型通常在几十轮达到平台期,后面容易过拟合。配合早停参数观察验证集指标即可。
4.4 训练过程监控与结果评估
训练启动后要重点看三个曲线:训练损失是否下降、验证集mAP是否上升、分类损失是否异常集中。YOLOv5会自动保存best.pt和last.pt,best.pt根据验证集mAP挑选。
训练结束后执行一次完整评估:
python val.py \ --data data/vehicle.yaml \ --weights runs/train/vehicle_small/weights/best.pt \ --img 1280评估输出会包括mAP0.5和mAP0.5:0.95。mAP0.5是IoU阈值0.5下的平均精度,适合快速判断模型是不是“能用”;mAP0.5:0.95对边界框定位要求更高,更贴近严格工程场景。如果mAP0.5已经不错,但全值区间偏低,说明检测框整体偏大或偏小,需要检查数据标注质量和后处理参数。
5. 针对小目标检测的优化组合拳
5.1 提高输入分辨率是最直接的提升
从640提高到1280,是见效最快、成本也最直观的优化。分辨率翻倍后,远处车辆占用的像素区域变大,模型能观察到车灯、车窗结构,召回率提升明显。我用同一份数据做过对比,1280输入在mAP0.5上的涨幅通常比960输入高2到5个百分点。
代价是显存占用接近翻倍,推理速度下降也明显。工程化的折中方案是分两阶段训练:先用640或960把模型收敛,再用1280做一次小fine-tune。这样既享受高分辨率的收益,又避免从头到尾都用高分辨率导致训练时间过长。而推理阶段可以根据业务需求选择960或1280,不必要的话就用960。
5.2 增加浅层检测头的判断标准
默认YOLOv5有三个检测头,最小步长是8倍下采样。面对超小目标,可以在骨干网络浅层再增加一个4倍下采样的检测头,让模型直接在更高分辨率的特征图上做预测。
修改思路是在模型配置里加入一个来自骨干网络早期层的新输出分支,并调整检测头输出的网格尺寸。实现时要注意,特征融合部分的通道数要和新增层匹配,训练时正样本分配逻辑也要支持多检测头。
这里给一个清晰的提醒:增加检测头不是万灵药。参数量和推理时间会上升,有时小目标召回率提升不明显,反而大目标指标轻微下降。我的建议是先试输入分辨率和数据增强,如果小目标还是大量漏检,再考虑加检测头。实际上很多道路场景用1280分辨率加切片推理已经能解决问题,不一定非改网络结构。
5.3 切片推理是部署阶段的大杀器
切片推理的原理很简单:把大图切块,每一块放大来检测,最后合并结果。前文说的“大目标小目标”问题直接分摊到每张切片里,一个小目标原本可能只有20像素,切成4块之后在单块里可能变成40像素以上,检测能力立刻提高。
具体操作是把输入按例如640×640切片,切片之间保留一定重叠,确保目标不会被切面分割成残块。每块独立推理后用NMS对重叠区域去重。代价是推理时间成倍增加,所以适合离线分析、卡口抓拍、监控区段回放等实时性要求不高的场景。
如果项目要做成实时视频流,建议在采集端做ROI裁剪或仅在关键区域启用切片,其他区域用普通单帧推理。这样既保留了小目标检测能力,又不会让GPU占用率直接爆表。
5.4 锚框、阈值与后处理调节
YOLOv5的默认锚框来自通用目标集合,直接用在车辆场景不一定最优。训练时开启锚框自适应,程序会对标注数据做聚类,生成更适合当前数据集的三组锚框。如果改过检测头结构,锚框数量必须和网格输出匹配,否则训练会直接报错。
推理阶段的置信度阈值默认是0.25,这个值对小目标偏高。小目标因为特征弱,置信度天然低于大目标,所以把阈值放到0.15甚至0.1能明显减少漏检。代价是误检数量上升,所以要和NMS阈值配合调整。NMS的IoU阈值我建议保持0.5左右,太高会把多个真实目标合并成一个框,太低会把同一个车切碎成多个框。
下面这张表是我在项目中实际摸索出来的优化手段对比:
| 优化手段 | 收益 | 成本 | 适合场景 |
|---|---|---|---|
| 提高输入分辨率 | 高 | 显存和耗时上升 | 所有小目标场景 |
| 增加浅层检测头 | 中 | 网络复杂,训练变慢 | 超小目标且分辨率已经到头 |
| 切片推理 | 高 | 推理时间成倍增加 | 离线分析、监控录像回查 |
| 降低置信度阈值 | 中 | 误检增加,需配合NMS | 漏检问题远大于误检问题 |
6. 部署与API封装
6.1 模型导出为ONNX或TensorRT
训练结束后的PyTorch权重不能直接投到生产环境,因为部署服务器的Python环境不一定装PyTorch复现。统一做法是导出为标准格式。导出命令:
python export.py \ --weights runs/train/vehicle_small/weights/best.pt \ --include onnx engine \ --img 640应该先用640输入导出做通道验证,导出1280输入会增大模型体积并拖慢推理速度。如果GPU是NVIDIA且追求低延迟,TensorRT引擎格式在推理性能上优势明显。导出后建议用网络可视化工具检查输入和输出维度,确保和后面写的后处理代码一致。
6.2 使用ONNX Runtime做推理
我把推理独立成一个类,避免业务代码和模型加载耦合在一起。简单的加载方式如下:
import onnxruntime as ort import numpy as np import cv2 class YOLOv5ONNX: def __init__(self, onnx_path): self.session = ort.InferenceSession( onnx_path, providers=["CUDAExecutionProvider", "CPUExecutionProvider"] ) self.input_name = self.session.get_inputs()[0].name def preprocess(self, img, size=640): h, w = img.shape[:2] r = size / max(h, w) new_w, new_h = int(w * r), int(h * r) resized = cv2.resize(img, (new_w, new_h)) canvas = np.full((size, size, 3), 114, dtype=np.uint8) canvas[:new_h, :new_w] = resized blob = cv2.cvtColor(canvas, cv2.COLOR_BGR2RGB) blob = blob.astype(np.float32) / 255.0 blob = np.transpose(blob, (2, 0, 1))[None] return blob预处理时注意保持宽高比,用灰色填充。省略缩放步骤会让检测框坐标完全错位。后处理需要解析输出张量,完成置信度过滤和NMS后再映射回原始图像坐标。
6.3 封装成HTTP接口
为了让部署文档更完整,我通常会再包一层HTTP服务。上传图片后返回JSON响应,包括类别、置信度和边界框坐标;也可以返回画好检测框的图片。
服务启动时一次性加载ONNX模型到内存,避免每次请求都重新初始化。并发不高的场景用同步请求处理就够了。并发高时要把推理放到独立线程池,避免阻塞Web服务的事件循环。接口响应里除了检测框,我还会带上统计结果,比如当前帧轿车数量、卡车数量、公交车数量,这样接业务系统时少一次二次开发。
这一步对论文的“系统实现”章节很有帮助。架构图里可以画成“图片输入→HTTP服务→模型推理→结果返回”的完整链路,答辩演示也更直观。
7. 常见问题与避坑记录
7.1 小目标漏检严重怎么办
首选检查输入分辨率。默认640跑漏检明显的情况下,换成1280经常有立竿见影的效果,这是最优先的debug步骤。分辨率提高后还是漏,就把置信度阈值调低,开启后处理合并,试切片推理。
还有一个容易被忽略的点,夜间或逆光场景下画面整体偏暗,细胞车辆对比度极低。这时候在网络之外加一个对比度拉伸或白平衡预处理,可以有效提升小目标可见度。不要指望模型在这种数据下还保持很强的表现,预处理增强才是性价比最高的办法。
7.2 卡车与公交车互相误判
这套系统里最容易出的“工程病”。卡车和公交车在正后方视角下高度相似,很多标注样本里货箱式卡车和城市公交车长度接近,模型分不清很正常。
解决思路有三步。第一步保证训练集里公交车样本充足,至少和卡车样本量在一个数量级。第二步是增加难例挖掘,去数据里找那些特别容易被混淆的图片,单独圈出来参与训练,让模型学习细微差异。第三步是后处理规则修正,比如长宽比明显偏长的矩形更可能是公交车,但这只能做辅助,不能当核心逻辑。
7.3 训练损失不降或过拟合
训练损失不降先检查数据路径。标签全部为空或路径写错时,模型等于在一片空白数据上学习,损失自然下不去。可以先在训练开始时打印一个batch的标签统计,确认里面有真实目标。
学习率设置太高也会震荡。YOLOv5默认学习率在0.01附近,可以改成0.001看是否变得稳定。过拟合的表现是训练损失持续下降、验证指标不动或下降,解决办法是增加数据增强、删除脏样本、降低epoch数,或者换更小尺寸的模型。不要一上来就加正则硬扛,数据层面的修正更本质。
7.4 显存和推理速度不够
显存不足优先减小batch,而不是减小输入分辨率,因为分辨率是影响小目标的核心变量。batch降到1之后还爆显存,就打开梯度累积参数,让多个小batch的梯度累计后再更新。
推理速度不够时优先考虑TensorRT和量化。INT8量化之前一定要用代表性训练数据做校准,否则检测框精度会明显下降。对部署场景来说,有时候CPU上跑半精度ONNX也能接受,关键是保证小目标不被后处理过滤掉。
8. 配套论文、部署文档与讲解内容的组织思路
8.1 配套论文主体怎么写
这类项目交付时通常会要求一份配套设计论文,核心不是堆技术名词,而是把“题目、方案、实现、实验、结论”讲清楚。论文骨架一般分成绪论、相关技术介绍、系统设计、实验与结果分析、总结与展望。
系统设计章节放总体架构图,并逐一说明数据、训练、推理、服务四个模块。实验章节必须放训练损失曲线、PR曲线、混淆矩阵和不同优化方案的对比表。答辩最看重的就是实验有没有完整记录,所以在训练阶段就应当固定好对照实验,保存每个对照组的权重和日志。到写论文时无非是把这些真实记录转成图表。
8.2 部署文档怎么组织
部署文档的目标是让另一个人照着做就能跑起来。开头先放项目目录结构,把源码、权重、数据、文档的位置在树状目录中说明。然后写环境安装步骤,包括Python版本、依赖库、CUDA版本和安装命令。随后写数据准备说明,重点写目录格式、标签格式和data.yaml配置。
训练和评估部分放目标命令即可。每个命令后面可加一到两行参数说明,让读者明白这些参数在控制什么。最后写接口调用示例,包括请求格式和响应格式。如果能配一张实际测试效果图,文档会比干巴巴的文字有说服力得多。
8.3 讲解视频或现场演示怎么准备
讲解时不要从第一行代码开始念,而是要围绕“小目标为什么难”和“我做了哪些优化”这两条主线来展开。演示流程可以固定为:启动服务、传入一张图片、展示检测结果、再跑一段视频流。
要提前准备一张问题清单,常见问题包括“为什么选YOLOv5”“mAP怎么解释”“增加检测头为什么没效果”“漏检怎么办”等。A同学第一次试讲时就被问到“增加检测层之后为什么mAP没涨”,当场愣了几秒。后来我让他把实验记录打印出来,对着对照组表格回答,效果立刻好很多。所以建议在正式讲解之前,自己先做一次完整的问答模拟,把每一条实验结果的意义提前理解清楚。
最后再分享一点个人体会:这类小目标项目最容易出成果的顺序,是先把训练和评估流程跑通,再决定要不要改模型结构。很多人一上来就修改配置文件,结果输入输出对不上,排错排了一整天。我实际做下来最有效的组合是“干净的数据集+高一点的分辨率+锚框自适配+后处理阈值下调”,三件套就能把几十像素车辆的识别率拉到很可用的水平;浅层检测头这类结构改动,只有前面手段都试完还达不到要求时才值得考虑。希望这篇经验整理能帮你在自己的项目里少走弯路。