安全帽检测这个题目,在工业视觉圈子里算是"入门即实战"的经典场景。我最早接触这类需求是在一个工地出入口的闸机改造项目上,当时甲方给的要求很朴素——"工人不戴帽子别让进",但真做起来才发现,光照、遮挡、小目标、误报率,每一项都能把人磨到怀疑人生。这次我拿 YOLOv9 重新做一遍完整的安全帽检测流程,把从数据准备到模型跑通的每一步都摊开讲清楚,包括我踩过的坑和那些文档里不会写的细节。如果你手上正好有类似的安全防护检测需求,或者想拿一个真实场景把 YOLOv9 完整跑一遍,这篇内容应该能帮你省下不少试错时间。
1. 为什么安全帽检测值得单独拿出来做一遍
1.1 这个场景的真实难点在哪
很多人觉得安全帽检测就是"戴帽子"和"没戴帽子"两类目标,标注一下、训个模型就完事了。我一开始也这么想,直到把第一批现场图片拉回来才明白问题没那么简单。工地场景的复杂度远超实验室数据集:逆光下安全帽只剩一个轮廓、远处工人只占几十个像素、钢筋脚手架把人体切得七零八落、黄色安全帽和黄色挖掘机在颜色上几乎分不开。这些都不是靠调几个超参就能解决的,得从数据层面就想清楚。
从检测任务的角度拆,安全帽检测本质上是小目标检测 + 遮挡目标检测 + 类内差异大的多类别检测的叠加。安全帽在整张图里往往只占很小一块区域,尤其是塔吊、高处作业的远景镜头;而"戴帽"和"未戴帽"的区分点又集中在头部这一小块,模型必须对头部区域的细节足够敏感。YOLOv9 相比前代在特征提取和梯度信息保留上做了改进,这对小目标和遮挡场景是有实际帮助的,但前提是你得把数据喂对。
1.2 类别设计:两类还是三类,这是个决策点
我在动手前先纠结了一个问题:到底分几类。常见的做法有三种:
| 方案 | 类别定义 | 优点 | 缺点 |
|---|---|---|---|
| 两类方案 | helmet / head | 标注简单,模型收敛快 | 无法区分"戴了但没戴好" |
| 三类方案 | helmet / no_helmet / person | 能关联人体上下文 | 标注量大,类别不平衡严重 |
| 四类方案 | helmet / no_helmet / head / person | 信息最全 | 冗余标注多,训练易过拟合 |
我最终选了两类方案(helmet / head),理由很实际:这个项目的下游逻辑只需要判断"头部区域有没有被安全帽覆盖",不需要区分人的整体姿态。head 类代表"裸露的头部",helmet 类代表"被安全帽覆盖的头部",两者在空间上是互斥的,标注边界清晰,模型学起来也干净。三类、四类方案看着信息更全,但 person 类和 head 类在标注时经常打架——一个人的头部到底算 person 的一部分还是单独标 head,不同标注员的理解不一致,反而引入噪声。
提示:如果你的下游要做"人员计数"或者"是否进入危险区域"的判断,那 person 类是必要的;如果只做安全帽合规检测,两类方案足够,别为了"看起来专业"把类别堆复杂。
1.3 YOLOv9 在这个任务上的适配性判断
选 YOLOv9 不是跟风。这个场景对模型的核心诉求是小目标召回和遮挡下的稳定输出,YOLOv9 引入的可编程梯度信息(PGI)和广义高效层聚合网络(GELAN)在结构上就是冲着"信息不丢失"去的。简单说,传统网络在层层下采样的过程中,小目标的特征很容易被稀释掉,而 PGI 机制通过辅助监督让浅层特征里的细节信息能更完整地传到深层,这对几十像素的安全帽来说很关键。
当然,模型再好也得看数据量和硬件。我这次用的是中等规模的现场数据集,单卡显存 24G,选的是 YOLOv9c 这个量级——s 太小容易欠拟合,e 太大训练周期扛不住,c 是精度和速度比较平衡的档位。这个选型逻辑后面会展开讲。
2. 数据准备:决定成败的八成工作量
2.1 数据来源与采集策略
安全帽检测的数据,我建议不要只依赖公开数据集。公开数据集(比如一些学术竞赛放出来的工地安全数据)质量参差不齐,而且场景和你实际要部署的环境往往对不上。我这次的做法是"公开数据打底 + 现场采集补足":先用公开数据把模型的基本能力拉起来,再用现场图片做针对性微调。
现场采集有几个实操要点。第一,按时间段采,早中晚各采一批,把逆光、顺光、阴天、夜间补光这些光照条件都覆盖到;第二,按距离采,近景、中景、远景都要有,远景样本是提升小目标召回的关键;第三,按角度采,正面、侧面、俯视(高处摄像头)都得有。我第一批只采了正面近景,训出来的模型一到俯视远景就拉胯,返工重采花了整整两天。
采集数量上,我的经验是每类目标至少 2000 个实例起步,注意是实例数不是图片数。一张图里可能有五六个安全帽,那这张图贡献五六个实例。总量控制在 5000 到 8000 张图比较合适,再多边际收益就下降了,除非你的场景特别复杂。
2.2 标注规范:那些容易埋雷的细节
标注是安全帽检测里最容易埋雷的环节。我见过太多项目栽在标注一致性上,模型训出来指标挺好看,一上线就各种误报。这里列几条我踩过坑之后定下来的硬规矩:
- 头部边界框的判定标准要统一。helmet 类框的是安全帽的外轮廓,head 类框的是裸露头部的可见区域。关键是"戴了帽子但帽子歪了、只盖住半个头"这种情况怎么标——我的处理是只要安全帽覆盖了头顶主要区域就标 helmet,不要纠结边缘。
- 遮挡超过 70% 的目标直接不标。强行标注残缺目标会让模型学到错误的形状先验,反而拉低整体精度。
- 极小目标(小于 16×16 像素)单独标记。这类样本对训练贡献有限但会干扰损失计算,我一般先标出来,训练时通过数据过滤决定是否纳入。
- 同一张图里 helmet 和 head 不要重叠标注。物理上互斥,标注上也必须互斥,否则模型分类头会混乱。
标注工具我用的是常见的开源标注软件,导出 YOLO 格式的 txt 标签。这里有个细节:YOLO 格式的坐标是归一化的中心点加宽高,转换脚本一定要自己验一遍,我遇到过导出时坐标没归一化导致训练 loss 直接爆炸的情况。
2.3 数据增强:别把增强做成了噪声
数据增强这块,我的态度是克制。安全帽检测的增强要贴合真实场景的变化,而不是为了增强而增强。我实际开启的增强项和理由如下:
| 增强方式 | 参数设置 | 开启理由 |
|---|---|---|
| Mosaic | 概率 0.8 | 提升小目标和多目标场景的鲁棒性 |
| 随机缩放 | 0.5~1.5 | 模拟不同距离的成像尺度 |
| 随机平移 | 0.1 | 模拟目标位置偏移 |
| HSV 色相扰动 | 0.015 | 应对不同光照色温 |
| HSV 亮度扰动 | 0.4 | 应对逆光、阴影 |
| 水平翻转 | 概率 0.5 | 安全帽左右对称,翻转合理 |
我没有开垂直翻转和大幅旋转。原因很直接:工地摄像头基本是固定角度安装的,工人也不会倒立走路,垂直翻转会造出物理上不存在的样本,模型学了反而有害。随机旋转超过 15 度同理,会让安全帽的朝向先验失真。这个判断很多教程不会讲,但对实际效果影响很大。
注意:Mosaic 增强在训练后期建议关闭(YOLO 系列一般有 close_mosaic 参数,最后若干 epoch 关掉)。Mosaic 拼出来的图里目标尺度分布和真实场景有偏差,训练末期关掉能让模型更好地收敛到真实分布上。
3. 环境搭建与训练配置的取舍逻辑
3.1 环境准备:版本对齐比装最新版重要
环境这块我吃过亏,所以现在特别强调版本对齐。YOLOv9 的官方实现依赖特定版本的 PyTorch 和 CUDA,盲目装最新版大概率会遇到算子不兼容或者编译报错。我这次的环境组合是:Python 3.9、PyTorch 2.0.x、CUDA 11.8,显卡驱动对应版本提前确认好。这套组合不是最新的,但胜在稳定,社区里踩过的坑都有现成答案。
安装步骤大致是这样,我按顺序列出来:
# 创建独立环境,避免污染系统 Python conda create -n helmet_yolov9 python=3.9 -y conda activate helmet_yolov9 # 安装对应 CUDA 版本的 PyTorch pip install torch==2.0.1 torchvision==0.15.2 --index-url https://download.pytorch.org/whl/cu118 # 克隆 YOLOv9 官方仓库并安装依赖 git clone <官方仓库地址> cd yolov9 pip install -r requirements.txt装完之后一定要跑一遍环境自检,确认torch.cuda.is_available()返回 True,并且打印一下 CUDA 版本和显卡型号。我见过有人装完以为好了,结果训练时才发现跑在 CPU 上,一个 epoch 跑了三个小时。
3.2 数据集目录结构:按 YOLO 规范组织
YOLO 系列对数据目录结构有固定要求,我按标准组织如下:
helmet_dataset/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── data.yamldata.yaml是数据集描述文件,内容大致是:
train: ./images/train val: ./images/val nc: 2 names: ['helmet', 'head']这里有个容易忽略的点:训练集和验证集的划分要按场景分,不能随机分。如果随机划分,同一段视频的相邻帧可能同时出现在训练集和验证集里,验证指标会虚高。我的做法是按采集批次划分,比如上午采的做训练、下午采的做验证,这样验证集才能真正反映模型在"没见过的场景"上的表现。
3.3 训练参数:每个数字背后的理由
训练参数我调了几轮才定下来,下面这张表是我最终用的配置和对应的思考:
| 参数 | 取值 | 选择理由 |
|---|---|---|
| epochs | 150 | 数据量中等,150 轮足够收敛,再多易过拟合 |
| batch size | 16 | 24G 显存下 c 模型能跑的最大稳定值 |
| img size | 640 | 兼顾小目标细节和显存占用 |
| 初始学习率 | 0.01 | YOLO 系列默认值,配合 warmup 稳定 |
| 优化器 | SGD | 实测比 Adam 泛化更好,收敛曲线更平滑 |
| 权重衰减 | 0.0005 | 抑制过拟合 |
| 学习率调度 | 余弦退火 | 后期精细收敛 |
关于 img size 我想多说一句。640 是默认值,但安全帽是小目标,理论上更大的输入尺寸(比如 960)能保留更多细节。我实测过 960,小目标召回确实有提升,但显存占用翻倍、训练时间也翻倍,而且提升幅度没有想象中大(大概 2 个点左右的 mAP)。如果你的硬件允许且小目标问题特别突出,可以上 960;否则 640 是性价比最高的选择。
学习率这块,初始 0.01 配合前 3 个 epoch 的 warmup 是标准操作。warmup 的作用是让模型在训练初期不要被大学习率带偏,尤其是预训练权重刚加载进来的时候,直接上大学习率容易把预训练学到的特征冲掉。
4. 训练过程中的监控与问题排查
4.1 看什么指标,怎么看
训练启动后,终端会持续输出各类损失和指标。新手容易盯着总 loss 看,但真正该关注的是分类损失、定位损失、置信度损失这三条曲线的相对走势,以及验证集上的 mAP50 和 mAP50-95。
我的经验判断标准是这样的:正常收敛的情况下,训练初期三条损失都快速下降,大概 20 到 30 个 epoch 后进入缓慢下降阶段;验证集 mAP50 在 50 个 epoch 左右应该达到一个比较高的平台(安全帽检测这个任务,数据质量过关的话 mAP50 到 0.9 以上是合理的)。如果出现下面几种情况,就得停下来排查:
- 训练损失降但验证损失升:典型过拟合,考虑加数据、加正则、减模型规模。
- 损失震荡剧烈不收敛:学习率太大或者 batch size 太小,也可能是标注里有脏数据。
- mAP 卡在低位不动:大概率是数据问题,类别不平衡或者标注错误,先别急着调参。
我这次训练到第 40 个 epoch 左右,发现 head 类的召回明显低于 helmet 类。排查下来是数据里 helmet 实例远多于 head 实例(工地上戴帽的本来就占多数),类别不平衡导致模型偏向多数类。解决办法是在数据加载时对 head 类做重采样,或者用类别权重调整损失。我选了前者,把含 head 的图片复制了一份加入训练集,简单粗暴但有效。
4.2 常见报错与处理
训练过程中我遇到过几个典型报错,列出来供参考:
显存溢出(CUDA out of memory):最直接的办法是降 batch size,从 16 降到 8 再试。如果还不行,检查是不是 img size 设太大了,或者数据加载的 worker 数太多占用了额外显存。我一般把 workers 设成 4 到 8,太多反而会因为数据预取占显存。
标签文件找不到或格式错误:YOLO 训练时会校验每张图对应的 label 文件,缺失或格式不对会直接报错。我写了个小脚本在训练前统一校验一遍,检查每个 label 文件的行数、每行的字段数、坐标是否在 0 到 1 之间。这个前置检查能省掉大量训练中途崩溃的麻烦。
损失出现 NaN:一般是学习率过大或者数据里有异常值(比如坐标超出范围)。先降学习率,再查数据。我遇到过一次是某张图的标注坐标写成了像素值没归一化,导致 loss 直接 NaN。
4.3 训练时长与硬件预期
给个实际的预期,方便你规划时间。我这次的配置是单卡 24G 显存,YOLOv9c,640 输入,batch 16,150 个 epoch,总训练时间大约 18 到 20 小时。如果显卡弱一些(比如 12G 显存),batch 得降到 8,时间会拉长到 30 小时以上。这个时间预算要提前算好,别指望几个小时就能出结果。
训练过程中建议开启断点续训,YOLO 系列默认会保存 last.pt 和 best.pt。万一训练中断(断电、显存崩了),可以从 last.pt 接着训,不用从头来。这个习惯一定要养成,我吃过一次没开续训、训到 120 轮机器重启、全部重来的亏。
5. 从训练结果到可用模型的关键一步
5.1 验证集评估的正确姿势
训练结束后,best.pt是验证集上表现最好的权重,但别急着拿它上线。我习惯做两件事:一是在独立的测试集上再评一遍,这个测试集是从现场新采的数据里留出来的,和训练、验证数据都不同源,能真实反映泛化能力;二是按场景分组看指标,把测试集按光照、距离、角度分组,分别算 mAP,找出模型的短板场景。
我这次的测试结果,整体 mAP50 在 0.93 左右,但分组看下来,夜间补光场景的 mAP 只有 0.81,明显拖后腿。这说明模型对夜间光照的适应性还不够,后续要么补夜间数据,要么在部署端加图像预处理。这种分组分析比一个笼统的总分有用得多,能直接指导下一步优化方向。
5.2 推理测试:拿真实图片跑一遍
指标好看不代表实际好用,一定要拿真实图片做推理可视化。我用验证集里挑出来的几十张典型图跑了一遍,重点看三类情况:小目标(远景工人)有没有漏检、遮挡目标(被脚手架挡住半个身子)框得准不准、逆光目标有没有误判。可视化结果里,漏检主要集中在远景极小目标上,这个在 640 输入下是预期内的,如果业务上对远景要求高,就得考虑提高输入分辨率或者做图像分块推理。
推理脚本的核心逻辑不复杂,加载模型、读图、前向、后处理(NMS)、画框。这里提醒一个细节:置信度阈值和 NMS 的 IoU 阈值要根据业务调。安全帽检测里,漏检(该报警没报警)的代价通常高于误检(误报一下),所以置信度阈值可以适当调低,比如从默认的 0.25 降到 0.2,用多一点误报换更低的漏检。这个取舍得和业务方对齐,不能自己拍脑袋定。
5.3 部署前的最后检查清单
模型要真正用起来,部署前我一般过一遍这个清单:
- 模型导出格式确认(onnx 还是 tensorrt,取决于部署端硬件)
- 推理速度实测(单帧耗时,是否满足实时要求)
- 输入预处理和后处理逻辑与训练时完全一致(归一化方式、通道顺序)
- 类别索引映射正确(训练时的 0/1 对应部署端的 helmet/head,别搞反)
- 边界情况测试(全黑图、全白图、无人图,确认不崩溃不误报)
导出 onnx 的时候有个坑我踩过:动态 batch 和动态尺寸的设置。如果部署端输入尺寸固定,导出时就固定尺寸,别开动态,否则推理引擎优化会打折扣。这个细节在后面的部署篇里我会展开讲。
6. 这一轮做下来最值钱的几条经验
回过头看,安全帽检测这个任务本身不难,难的是把工程链路走通、把每个环节的细节抠到位。我最想分享的几条经验是:数据决定上限,标注一致性比数据量更重要,我见过太多项目在标注环节埋雷,后面怎么调参都救不回来;增强要贴合场景,别为了指标好看乱开增强,垂直翻转这种物理上不合理的增强只会帮倒忙;训练监控要看分组指标,别被总分骗了,一个漂亮的 mAP 背后可能藏着某个场景的系统性失效。
还有一条是关于心态的:这类检测任务的第一版模型几乎不可能直接上线,做好迭代两三版的准备。第一版把链路跑通、暴露问题,第二版针对性补数据和调参,第三版才谈得上稳定。急着一步到位,往往会在某个细节上翻车。下一篇我会接着讲模型导出、推理加速和部署端的工程化处理,把这条链路彻底闭环。