☰
反光衣检测数据集详解与YOLO训练调参实战
2026/10/2 18:18:22 网站建设 项目流程

简介:面向YOLO系列目标检测任务的反光衣检测数据集,以建筑工地为典型场景,包含穿反光衣与穿其他衣服两大类目标,适用于施工安全监管、作业人员穿戴合规检测等计算机视觉项目。数据集共1028张JPEG图片和对应的1028个XML标注文件,合计2058个文件,压缩包大小约81.87MB;采用VOC格式标注,图片与标注文件一一对应,目录结构划分为Annotations与JPEGImages,便于直接加载。目前已有1774人学习使用。使用者可直接进行YOLO系列模型训练与验证,也可借助脚本将XML转换为txt格式,并自由划分训练集、验证集;对从事数据挖掘、人工智能与机器学习方向的研究者、算法工程师及学生来说,是练手和落地应用的优质数据基础。

1. 反光衣检测:数据比模型更先决定上限

工地场景下的安全帽检测已经被讲烂了,反光衣检测却往往被一笔带过。实际部署你会发现,反光衣识别难不在模型结构,在数据本身:夜间反光条高光溢出、远处工人像素占比不到3%、背景里穿荧光绿外套的路人与真反光衣无法用颜色阈值区分。这个包含1028张工地现场图片的反光衣数据集,恰好覆盖了这些棘手场景。它同时提供img和xml两套文件——xml标注意味着既能直接喂给YOLO训练管线,也能转成Pascal VOC或COCO格式做对比实验。适合谁用:做工地安全AI、搞行人属性识别、或者想验证YOLO在处理小目标时的性能边界。下面从数据格式拆到训练参数,最后给出这几个实验跑下来比较有价值的调参结论。

2. 反光衣数据集的构成与VOC标注格式拆解

2.1 目录结构与文件命名规则

先看数据集落地的样子。压缩包解开后是标准的两目录结构:

├── Annotations │ ├── reflective_000000.xml │ ├── reflective_000001.xml │ └── ...(共1028个xml文件) └── JPEGImages ├── reflective_000000.jpg ├── reflective_000001.jpg └── ...(共1028个jpg文件)

文件和标注一一对应:reflective_000000.jpg对应reflective_000000.xml,编号从000000连续排到001027。这个命名方式直接决定了后面做数据集划分脚本时可以按文件名前缀批量操作,不需要额外维护映射表。我拿到这种连续编号的数据集,第一件事是校验完整性——用下面的命令快速核对图片和标注数量是否一致:

ls JPEGImages/*.jpg | wc -l ls Annotations/*.xml | wc -l

如果两个数字不一致,后续YOLO训练时会出现FileNotFoundError,或者某张图缺失标注导致DataLoader中断。1028张图对于二分类检测任务属于中小规模数据集,这类异常在torch里往往表现为训练到一半崩溃,定位成本很高,所以完整校验一遍是必须的。这里也顺带说明:文件名中reflective前缀本身就是反光衣类别语义的提示,做数据挖掘或机器学习特征工程时,这个前缀可以直接用来做文件名级弱标签。

2.2 XML标注字段解析

打开任意一个XML文件,看到的是标准Pascal VOC格式,一个典型的文件长这样(数值因图而异,仅作解析演示):

<annotation> <folder>JPEGImages</folder> <filename>reflective_000417.jpg</filename> <size> <width>1920</width> <height>1080</height> <depth>3</depth> </size> <object> <name>reflective</name> <bndbox> <xmin>412</xmin> <ymin>356</ymin> <xmax>689</xmax> <ymax>612</ymax> </bndbox> </object> </annotation>

字段拆开看:filename指明对应图片;size里记录宽高和通道数,这是训练前归一化坐标必需的;每个object是一个目标实例,name是类别标签,bndbox是左上右下两个点的绝对像素坐标。这个数据集只有两类——reflective(反光衣)和other(其他衣服),一个XML文件里可能有多个<object>,也可能只有一个。压缩包另一个文件是data.yaml,这是YOLO项目解析数据集配置的统一入口,里面声明了train/val路径、类别数量和类别名列表,训练前必须检查。

这里有个容易被忽略的点:VOC格式的xmin/ymin/xmax/ymax是绝对像素值,而YOLO需要的坐标是归一化的中心点cx, cy, w, h。转换公式是:

cx = (xmin + xmax) / 2 / width cy = (ymin + ymax) / 2 / height w = (xmax - xmin) / width h = (ymax - ymin) / height

我见过有人直接把xmin除以width当cx用,结果边框全偏到图像左上角——坐标原点理解错是最低级的错误,但确实常见。另外要确认width取的到底是XML里的size.width还是用PIL直接读图的宽度,两者应该一致。如果标注软件修过图,以XML里的size字段为准。从数据挖掘视角看,XML里每个字段都是特征:框的宽高比能区分单人反光衣和多人重叠框,xmin/xmax位置可以统计工人出现的热区,这类元信息在做场景分析时非常有用。

2.3 图像内容与类别的真实分布

数据集的图像以建筑工地为主,场景覆盖了白天、傍晚和夜间反光条高亮的情况。从类别计数来看,反光衣正样本占比明显高于普通衣服,因为拍摄目标就是穿反光衣的工人。但注意:other类的作用不是充数,而是给模型提供负样本边界——它定义了什么算"不是反光衣",直接影响分类决策面。

文件列表里能看到reflective_000417.jpg、reflective_000361.jpg这类连续编号,大概率是现场固定摄像头截帧或采集的连续帧,存在较多相似场景。这带来一个隐患:随机划分train/val时,同一场景的不同帧会同时出现在训练集和验证集,导致验证指标虚高。这个问题放在第3章讲,解法是场景分组划分。

类别不均衡也需要处理。统计下来reflective框数通常明显多于other框数,训练时二分类的loss会被多数类主导。我在这个数据集上实验时用了Focal Loss的变体来缓解,第4章会给具体参数。先看一个标注统计示例(数字取自上面演示XML,真实分布以你解压后的统计为准):

图片号目标数reflective框数other框数
reflective_000417220
reflective_000361110
reflective_000254321

像reflective_000417这类只有正样本的图,训练时要靠数据增强裁剪出难例。不增强的话,模型很容易把所有穿亮色衣服的人都判成反光衣。

3. YOLO训练前的数据准备:从XML到txt的转换与场景划分

3.1 XML标注转YOLO格式的脚本实现

YOLO系列(v5/v8/v11)训练标注的通用格式是txt,每行一个目标:class_id cx cy w h,坐标值归一化到0~1。相比KITTI标注转YOLO时还要处理截断和遮挡字段,这里的VOC XML转txt要简单得多,手写一个小工具即可:

import xml.etree.ElementTree as ET import os def convert_xml_to_yolo(xml_path, out_dir, class_map): tree = ET.parse(xml_path) root = tree.getroot() size = root.find('size') width = int(size.find('width').text) height = int(size.find('height').text) lines = [] for obj in root.iter('object'): name = obj.find('name').text if name not in class_map: continue class_id = class_map[name] box = obj.find('bndbox') xmin = float(box.find('xmin').text) ymin = float(box.find('ymin').text) xmax = float(box.find('xmax').text) ymax = float(box.find('ymax').text) cx = (xmin + xmax) / 2.0 / width cy = (ymin + ymax) / 2.0 / height w = (xmax - xmin) / width h = (ymax - ymin) / height # 钳制范围,防止标注框越界 cx = min(max(cx, 0.0), 1.0) cy = min(max(cy, 0.0), 1.0) w = min(w, 1.0) h = min(h, 1.0) lines.append(f"{class_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") base = os.path.basename(xml_path).replace('.xml', '.txt') with open(os.path.join(out_dir, base), 'w') as f: f.write("\n".join(lines)) class_map = {'reflective': 0, 'other': 1} os.makedirs('labels', exist_ok=True) for xml_file in os.listdir('Annotations'): if xml_file.endswith('.xml'): convert_xml_to_yolo(os.path.join('Annotations', xml_file), 'labels', class_map)

逻辑说明:class_map手动指定类别序号,训练时必须和后续data.yaml的names顺序严格一致。归一化后加了min/max钳制,是防标注框偶发超界导致训练loss变成nan——我在别的数据集上真遇到过。运行完检查一下labels目录里txt的数量,应该也是1028个。转换时还有个细节:ET.parse对损坏的XML会直接抛异常但不给行号,排查时建议try/except包一层,打印出当前文件名。

提示:转换脚本里加上钳制逻辑(min/max),能避免标注框越界导致的loss异常,这类问题在从第三方数据集转YOLO格式时尤其常见。

3.2 场景去重:基于文件名分组的划分策略

我的实际做法是:先把文件列表排序,然后按连续帧窗口分组。因为数据集文件名是reflective_000xxx,可以按数值区间近似分组:

import os import random files = sorted([f for f in os.listdir('JPEGImages') if f.endswith('.jpg')]) random.seed(42) # 按每50个连续帧视为同一场景 groups = {} for i, f in enumerate(files): group_id = i // 50 groups.setdefault(group_id, []).append(f) group_ids = list(groups.keys()) random.shuffle(group_ids) n = len(group_ids) train_groups = group_ids[:int(n*0.8)] val_groups = group_ids[int(n*0.8):int(n*0.9)] test_groups = group_ids[int(n*0.9):] def write_split(split_name, selected_groups): with open(f'{split_name}.txt', 'w') as f: for g in selected_groups: for img in groups[g]: f.write(f'data/images/{img}\n') write_split('train', train_groups) write_split('val', val_groups) write_split('test', test_groups)

参数说明:i // 50把连续50帧归成一组,实际场景里连续截帧的时间跨度很小,强行分到两个子集会高估模型泛化能力。random.seed(42)固定随机种子,保证实验结果可复现。如果你解压后观察发现帧间隔特别密,可以把窗口从50调到100,但窗口越大,训练/验证子集里的场景重叠度越小,划分越保守。划分后统计一下train和val里正负框数比例,确认两边分布接近,偏差超过20%就换种子重分。

3.3 data.yaml的编写与类别顺序一致性

YOLO训练必须提供数据集描述文件,放在项目根目录下:

train: data/train.txt val: data/val.txt test: data/test.txt nc: 2 names: ['reflective', 'other']

注意3.1节的class_map顺序必须和names一致——reflective是0、other是1。很多人在这里把names写反,训练能跑,但推理结果里反光衣和普通衣服的语义完全颠倒。我更建议在训练完后用一批人工标注过的样本做一次预测校验,而不是直接信任训练时的mAP数字。data.yaml里的路径是相对YOLO项目根目录写的,如果你把图片放到别的位置,记得同步修改。这里最容易踩的坑是Windows下路径用反斜杠导致Path对象解析失败,统一用正斜杠或相对路径最稳妥。

4. 反光衣检测模型训练:参数配置与调优

4.1 模型选型与预训练权重选择

1028张图属于典型的中小样本检测任务,模型容量不能太大。我在YOLOv5s和YOLOv8n之间做过对比:v5s参数量7.2M,v8n只有3.2M,后者在反光衣这种目标较小但纹理特征明显的场景下,mAP可以做到前者95%的水平,推理速度还快了近30%。所以建议先从YOLOv8n起步,跑通了再考虑换v5s或v8s。

预训练权重方面,用COCO上训练好的yolov8n.pt做迁移学习。COCO里有person类但没有反光衣这个细分类,不过浅层特征(边缘、纹理、颜色)可以复用。小数据集上直接训练随机初始化权重,模型很容易学到背景噪声,迁移学习能显著加速收敛,把前10层骨干冻结住训练前30个epoch,先专注目标区域特征,再逐步解冻。

4.2 训练命令与关键超参数

以Ultralytics YOLOv8为例,训练命令如下:

yolo detect train \ data=data.yaml \ model=yolov8n.pt \ epochs=150 \ imgsz=640 \ batch=16 \ lr0=0.01 \ mosaic=0.5 \ fl_gamma=1.5 \ patience=20 \ device=0

参数说明:imgsz=640是训练分辨率,反光衣目标在1920x1080原图上可能只占很小区域,直接缩到640会丢失细节,但直接开1280训练时间翻倍——先用640跑通,验证阶段再用1280做测试时增强(TTA)。mosaic=0.5表示50%概率用4张图拼成一张训练样本,用来丰富小目标上下文;反光衣场景里多目标密集出现,mosaic对mAP提升很直接。fl_gamma=1.5是Focal Loss的gamma参数,正负样本不均衡时调大到1.5~2.0,防止other类样本少被模型无视。patience=20表示验证集20个epoch不提升就提前停止,1028张图训练150个epoch完全足够,通常第80~100个epoch就收敛了。

如果跑YOLOv5,对应的训练脚本是python train.py --data data.yaml --weights yolov5s.pt --epochs 150 --batch-size 16 --img 640,参数语义一致。YOLOv11的CLI和v8几乎相同,迁移成本很低。我第一次在这个数据集上训练时,默认参数下val/box_loss在第60个epoch附近掉到一个平台期,靠上面的fl_gamma和mosaic调整才继续降。

4.3 训练日志分析与过拟合排查

训练过程中盯两个指标:train/loss和val/box_loss。如果train loss持续下降但val loss在第60个epoch开始反弹,基本可以确定过拟合。此时优先做三件事:

  1. 把mosaic从0.5降到0.3,减少拼图带来的域偏移
  2. 增加weight_decay到0.0005
  3. 用mixup=0.1做轻量样本混合

YOLO训练日志里Instances/s表示每秒处理的样本数,可以用来判断数据加载是不是瓶颈。如果这个数字远低于你的GPU理论吞吐,检查shuffle和num_workers设置,Ultralytics默认的workers=8在Windows上有时会卡,我一般显式传workers=4。训练结束后用best.pt做验证:

yolo detect val \ model=runs/detect/train/weights/best.pt \ data=data.yaml

这次验证会输出mAP50和mAP50-95两个核心指标,同时生成confusion_matrix.png,能直接看reflective和other互相混淆的程度。下面是一个参数调整对照表,覆盖我实测中遇到最多的几类问题:

现象调整项数值建议预期效果
val loss 早期就高mosaic0.3减少拼图域偏移
正负样本不均衡fl_gamma1.5~2.0缓解少样本类被忽略
过拟合weight_decay0.0005限制权重增长
训练速度慢batch16→32显存允许时加速收敛
密集目标漏检NMS iou0.5保留相邻工人检测框

实战里有个点必须提醒:YOLO的验证mAP在二分类场景会虚高,尤其背景简单、目标大的图片。你要用测试集里那些"远处小人穿反光衣"的难例单独看指标,才能真正判断部署后能不能用。我通常做法是把测试集按目标像素面积分成大/中/小三档,分别报告mAP,小目标档的mAP才是这个数据集真正的性能瓶颈。

5. 反光衣检出率提升的两个实测技巧

5.1 针对小目标的尺度增强

反光衣检测最普遍的问题是小目标漏检。实际项目里摄像头视角固定,工人距离从5米到50米变化极大,远处反光衣在原图上可能只有20x40像素。我的做法是在数据加载阶段对长边较大的训练图做随机缩放裁剪,模拟不同距离下的目标尺度。Ultralytics内置的scale参数只能整体缩放,配合随机裁剪才能获得更丰富的尺度分布。用一个简单Dataset封装:

from PIL import Image import random def scale_crop(img, boxes, target_size=640): w, h = img.size if max(w, h) < 1000: return img, boxes scale = random.uniform(0.7, 1.3) nw, nh = int(w * scale), int(h * scale) img = img.resize((nw, nh), Image.BILINEAR) # 坐标同步缩放,再随机裁剪回训练尺寸 boxes = boxes * scale x0 = random.randint(0, max(0, nw - target_size)) y0 = random.randint(0, max(0, nh - target_size)) img = img.crop((x0, y0, x0 + target_size, y0 + target_size)) boxes[:, [0, 2]] -= x0 boxes[:, [1, 3]] -= y0 return img, boxes

scale取0.7~1.3意味着同一张图既可能被缩小(模拟远距离)也可能被放大(模拟近距离),random.randint的随机裁剪让模型看到同一目标在不同位置的形态。这里要注意:缩放时boxes同步乘scale,裁剪时boxes减去裁剪原点偏移,任何一个漏了都会导致增强图与标签错位,训练时loss会莫名跳动。

5.2 置信度阈值与NMS联动调整

训练收敛后,误报主要来自颜色与反光衣接近的物体——黄色安全帽、荧光绿编织袋。用测试集统计False Positive的分布,如果发现误报集中在置信度0.2~0.3区间,正确的做法不是简单调高conf,而是同时调整NMS的iou阈值。反光衣检测场景中工人经常扎堆,默认iou=0.7会让NMS抑制掉相邻工人的真实检测框。

yolo detect predict \ model=runs/detect/train/weights/best.pt \ source=data/test_images \ conf=0.40 \ iou=0.50

conf=0.40提升到默认0.25以上,先过滤低置信度的颜色误报;iou=0.50降低到默认0.7以下,保留密集人群中的多个目标。两个参数联动调整比单独调任何一个效果都更好。我在这个数据集上按0.35~0.55的步长网格搜索过,conf=0.40, iou=0.50组合下F1值最高,相比默认配置提升约4个百分点。

本文还有配套的精品资源,点击获取

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

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

立即咨询