ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

YOLO头盔检测数据集构建全流程:从8300张标注到部署避坑指南

YOLO头盔检测数据集构建全流程:从8300张标注到部署避坑指南 做头盔检测大半年我最深的体会是模型换了好几个版本涨点都不如把数据集认真重做一遍来得明显。这套YOLO智慧交通头盔检测数据集一共8300张图前后折腾了两个月中间报废过两批标注也踩过不少训练阶段的坑。这篇文章准备把数据集的规划、采集、标注、训练和部署经验一次讲透适合正在做头盔检测、或者准备从零自建数据集做小目标检测项目的读者参考。我不打算只讲我做了什么更想把为什么这样做和哪些尝试是白费的讲清楚。1. 为什么先定数据规模再选模型8300张的规划逻辑1.1 头盔检测和常规目标检测不是一个量级的任务最开始我也犯过拿通用检测器直接上的毛病。COCO数据集里一辆车、一个人往往占图片面积的很大比例模型随便一跑就有不错的表现。但智慧交通路口的画面完全是另一回事骑手在画面里经常只有很小的像素高度而且大量是背影、侧身、逆光、被前车遮挡的状态。夜间抓拍里戴盔和不戴盔的差异可能就集中在头部一圈轻微的反光轮廓上。头盔检测本质上是一个小目标遮挡低对比度三座大山叠在一起的检测任务。这三类问题不是换网络结构就能解决的。我在项目里给团队定的第一条规矩就是模型先用成熟的数据必须把场景覆盖到每个角落。这也是为什么一开始就把规模定死在8300张而不是随手攒两三千张就开练。如果只检测头盔这一个大类两千张确实也能跑出来模型但一到夜间、雨天、半盔、后座乘客这些情况立马露馅。8300张是按下文这个覆盖矩阵算出来的结果。1.2 场景覆盖矩阵8300张是算出来的不是凑出来的我在立项时列了一张场景覆盖矩阵把影响头盔检测的主要维度全部拆开然后给每个格子定目标张数场景维度覆盖细项计划张数时段白天 / 傍晚 / 夜间约3000 / 2000 / 3300天气晴天 / 雨天 / 雾天与阴天约5000 / 2200 / 1100机位高位俯拍 / 平视机位约5500 / 2800道路密度单车骑行 / 多车混杂约4200 / 4100车辆类型摩托车 / 电动车约4400 / 3900夜间规划3300张是最关键的一笔。很多项目白天效果尚可一到晚上直接不能用就是因为夜间样本太少。雨天和雾天同样不能省雨滴反光、镜头水珠、低对比度都会让检测器迷茫。机位维度也容易被忽视高位俯拍是路口的常见监控视角平视机位则是执法记录仪和卡口相机的视角两者的形状、尺度分布差异很大。把每个格子的大致数量加总就是8300这个数字的来源——没有哪个场景是凑数硬塞进去的。1.3 类别定义被废弃的第一版五分类方案第一版标注我定义过五类with_helmet、without_helmet、helmet、head、motorcycle。结果开工第二天标注员就来问一个人背影看不见头该标head还是without_helmet骑手戴了头盔但只露出半个脑袋helmet和with_helmet是不是都要标一问就乱类别的语义边界模糊直接导致标注一致率跌破80%。后来砍成三类motorcycle、with_helmet、without_helmet。头盔本体和人头不再单独拆判断逻辑简单粗暴画面里的人确定戴盔就标with_helmet确定没戴盔就标without_helmet看不清楚就只标motorcycle不标人。对业务来说最终关心的就是未戴盔的人出现在哪里人框叠摩托车框做过滤已经够用。这个教训很值类别越少边界越清晰标注质量越高。如果确实需要区分摩托车和电动车我建议单独做另一套数据别混在一起抢标注资源。2. 采集与清洗8300张怎么凑齐又怎么筛掉一批2.1 三路采集渠道与抽帧策略数据集的三路来源我都试过。第一路是合作路口的真实监控视频不同机位、不同时段录的原始素材用ffmpeg按时间抽帧。抽帧频率我控制在每秒1到2帧太密相邻帧几乎一样数据集虚胖太疏又会漏掉车辆快速进出画面的瞬间。抽完帧还要用感知哈希做过近似去重相似度超过阈值的只留一张这一步直接砍掉了近三成冗余素材。第二路是公开可用的图像资源包括一些开放数据集和允许转载的图库。这里有个前提只使用明确标注可商用的部分逐张确认授权范围。第三路是合成渲染用三维场景摆摩托车和人体模型渲染不同光照与角度。合成图主要用来补夜间、雨天这类真实素材极缺的场景我的使用比例大概是真实抽帧55%、公开资源30%、合成渲染15%。合成数据不能当主力否则模型会学到塑料感纹理影响真实场景的泛化。2.2 一把尺子筛到底清洗标准数据清洗直接决定标注员的效率和最终质量。我定了几条硬性标准目标小于整图高度5%的骑手不标因为这种目标标注本身就是噪声严重运动模糊不标遮挡超过50%不标画面里没有任何有效目标的图一律删掉。很多路口画面里根本没有摩托车这类图占着数量却不贡献训练信息还会放大背景误检。还有一个容易忽略的坑是重复背景。同一个机位在同一个时间段连续抽帧即使做了去重背景依然高度相似。模型训练时看到大量相同背景很容易对背景过拟合一到新路口就失灵。我的做法是给每个机位每个时段设置截图数量上限宁可选的图分布广也不要一堆等价于同一段视频的图。2.3 合规与匿名化人脸和车牌的细节智慧交通数据涉及人脸、车牌发布前必须做匿名化。我在清洗阶段对所有清晰人脸做了高斯模糊、对车牌做了涂抹。这里有两个细节一是模糊要在最终出图的分辨率上做不然导出到训练尺寸后模糊区域可能缩得太小、又有可识别性二是保留一份匿名化前后的映射记录方便后续审计或追溯。商用项目尤其要看数据License不能只看网站写着免费下载就拿来直接用。这一关不过模型训出来也没法真正落地。3. 标注规范和YOLO格式落地3.1 标注流水线初标、复核、抽检标注工具我用的是X-AnyLabeling和LabelImg的组合。LabelImg稳定、导YOLO格式方便适合快速框选X-AnyLabeling带辅助分割适合处理复杂遮挡场景。团队四个人并行每天每人大概完成120到180张图全部初标用了一个多月。流水线设计成初标、复核、抽检三层初标员画框复核员重点检查类别是否正确、有没有漏标抽检时随机抽10%的图计算标注框的IoU一致率低于0.85整批退回。这套流程前期很费工但后面训练和调参省下的时间远超标注多花的成本。标注质量是数据集的命门这一层必须舍得投入人力。3.2 那些让标注员崩溃的边界情况BBox定义必须白纸黑字写进规范框要贴合目标可见轮廓不包含明显不属于目标的背景遮挡部分不靠脑补框到可见边缘为止两个目标挨得很近时边框不能互相吃进对方区域头盔框和人员框同时存在时以人员检测框为准。后座乘客是重灾区。标注员习惯只标骑手后座乘客经常漏掉但业务里后座不戴盔恰恰是高频违规。我在规范里专门加了一条车上出现几个人就标几个人不分前后排哪怕只露出肩膀也要尽力判断。这条规则直接决定了模型在后座检测上的表现也是我后来对比过有这条规则和没这条规则两个版本后确认有效的。还有一种边界情况头盔挂在车把上、放在车座上。这类是典型的hard negative如果误标成戴盔人员模型会以为车把上挂个头盔人戴了盔。我的处理是这类场景只标motorcycle不标人让模型学会没有人形轮廓的区域不预测人头。如果人下车站在旁边没戴盔就正常标without_helmet。3.3 YOLO标签格式的坑与一个自检脚本YOLO的txt格式核心是五列类别索引、归一化的中心点x、中心点y、框宽、框高。最容易犯的错有两个把中心点坐标写成了左上角坐标或者忘了归一化、直接写像素值。这两个错在训练阶段不会报错只会让你莫名其妙不收敛或者精度很怪。我的检查方法是一个小脚本逐图读取txt标签把框画回原图随机抽几十张直接看。十秒钟就能完成一次全量自检强烈建议每个人都跑一遍。另外类别索引从0开始我的映射是0motorcycle、1with_helmet、2without_helmet训练代码里禁止用类别名字符串直接匹配防止映射错位。import os import cv2 img_dir images label_dir labels check_dir check os.makedirs(check_dir, exist_okTrue) for name in os.listdir(img_dir): if not name.endswith(.jpg): continue img_path os.path.join(img_dir, name) label_path os.path.join(label_dir, name.replace(.jpg, .txt)) img cv2.imread(img_path) h, w img.shape[:2] with open(label_path, encodingutf-8) as f: for line in f: cls, cx, cy, bw, bh map(float, line.split()) x1 int((cx - bw / 2) * w) y1 int((cy - bh / 2) * h) x2 int((cx bw / 2) * w) y2 int((cy bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.imwrite(os.path.join(check_dir, name), img)跑完之后看到框的位置明显偏移、尺寸明显不对那就是标签格式出了问题。早发现好过训练到一半才回过头来怀疑数据这一步是整个流程里性价比最高的检查。数据划分方面8300张按8:1:1分成训练、验证、测试。划分前先按场景分层随机保证夜间、雨天、不同机位在三个集合里的比例接近不能直接random.shuffle否则可能出现验证集里完全没有夜间样本的情况。测试集一旦冻结就不再改动后续所有迭代都用它评估。4. 训练配置与调参实录4.1 模型和算力选型模型我用的YOLOv8s起步配合成熟的ultralytics训练生态跑通后再对比YOLOv8m。也试过YOLOv5s和更新的版本整体感觉是头盔检测的瓶颈在小目标和遮挡网络结构带来的差异远小于数据集质量带来的差异。版本号不是质变数据才是。训练用的云端V100实例batch size给到64输入尺寸640初次训练200个epoch大约7到9小时。如果你手里是单张消费级显卡建议把输入尺寸降到512、batch调到16到32先把流程跑通再加分辨率。4.2 关键超参数epochs、Mosaic、BNepochs我设置300并开启早停。头盔检测的训练曲线有一个特点mAP50涨到0.95之后看起来不动了但mAP50-95还在缓慢爬升这是在学精细的边界回归不能因为mAP50不动就提前停。Mosaic增强建议前开后关训练初期开着增加样本多样性最后几十个epoch关掉。原因是Mosaic生成的是拼贴图边框信息是假的一直开会让模型在真实自然图上的边界预测不稳定。这是我踩过最明显的坑也是后面几次训练稳定涨点的关键操作。再就是batch size和BN的配合。小batch加偏大的学习率很容易触发batch norm崩溃表现是loss突然变成NaN或者验证指标断崖式下跌。遇到这种情况不要先怀疑数据先看是不是BN炸了减小学习率、增大batch或者冻结BN层再训练几轮。我见过不少人夜里训练起来第二天一看全白跑多半都是这个原因。损失函数方面YOLO默认的CIOU对框回归已经很稳我没有再折腾其他变种。我反而更关注类别不均衡without_helmet的样本数大概只有with_helmet的一半直接结果是未戴盔类的召回偏低。这时候不要盲目加权重先统计各类别的实际业务需求再决定要不要在损失里给未戴盔类更高的权重。4.3 数据增强与类别平衡默认增强里HSV变换我保留但把幅度调低了。原因是头盔颜色对业务有意义比如建筑工地的黄色安全帽、外卖骑手的黄色头盔这些颜色特征是强先验。颜色偏得太离谱会让模型学歪。左右翻转可以开上下翻转绝对不能开——没有哪辆车会倒着开。夜间样本不足的问题我做了一个copy-paste增强把夜间摩托车的目标抠出来贴到其他夜间背景上人工生成更多夜间正样本。效果比单纯调亮度好因为保留了真实的夜间纹理。这个操作要注意边缘融合处理不好会生成一批贴纸图反而污染数据集。如果某个类别的AP特别低比如雨天头盔最直接的办法不是调参而是回数据仓补样本。我给自己定的排查链路是某个类别AP低先看测试集里这类样本到底长什么样再决定加数据还是调增强而不是一上来就改loss。这条链路帮我避免了大量无效调参。5. 评估与部署中的边界情况5.1 指标解读别被mAP50骗了mAP50高不等于能上线。我通常把mAP50-95和各类AP分开看再配合业务指标验收。头盔检测我给团队定的硬指标是with_helmet召回率不低于0.95without_helmet召回率不低于0.90把戴盔者误判成未戴盔的比例控制在0.5%以内。这些业务指标比单个mAP值更有决策意义。混淆矩阵必须看。网上经常有人问YOLO混淆矩阵总和不是1那是因为默认显示的是行归一化百分比每一行代表真实类别、行和为1列和自然不等于1。不用在归一化方式上纠结重点要看主对角线之外的误判分布。头盔检测里最常见的错误是把戴盔者框成未戴盔通常由半盔或者头盔颜色接近肤色引起这类错误在混淆矩阵里集中在特定格子一眼就能定位问题在哪一类场景。5.2 真实路口部署暴露的问题白天常规路口效果很好但部署第一个月就暴露了好几个数据集没覆盖的情况傍晚逆光时头盔边缘和天空融为一体检测框来回抖动雨夜路面反光把未戴盔的头部特征淹掉还有人戴棒球帽、鸭舌帽业务上不算头盔模型却容易识别成戴盔。这些都是数据覆盖缺口不是模型能力问题。解决办法是回到数据仓。我把现场产线跑出来的失败截图整理成hard example集按周回流训练。流程是每两周从真实部署流里抽一次失败样本大概300到500张人工确认后做增量训练每次增量训练结束都重新跑固定测试集防止修了这个洞、漏了那个洞。头盔检测这类安全相关场景宁可保守一点把漏检压到最低。5.3 数据集不是一次性交付物8300张是第一个冻结版本。冻结的好处是训练对比公平项目复现有据可查。但真实项目不可能止于第一版我把数据集做成了基座增量模式基座8300张保持不动增量目录按批次存放新补充的hard examples训练时基座和增量一起用评估永远只跑冻结测试集。版本管理用简单的目录加md5清单数据说明文件里写清楚标注规范版本、类别映射表、划分文件的校验值。这些细节看起来很琐碎真到要复现半年前某个结果的时候你就知道它们多重要了。最后再说一个我自己的体会。做头盔检测数据集最坑的其实不是采集和标注本身而是以为数据够了。每次模型在某个新场景翻车我回到原数据集里总能找到对应的覆盖缺口——不是缺时段、就是缺机位、或者缺某个特殊佩戴方式。数据集从来不是一次性交付物它是跟着模型一起迭代生长的资产。理解了这一点后面做任何小目标检测项目思路都会顺很多。
返回列表