ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

YOLOv5行人检测数据集制作全攻略:目录规范、标注转换与训练避坑

YOLOv5行人检测数据集制作全攻略:目录规范、标注转换与训练避坑 简介面向目标检测学习者和开发者的行人检测数据集采用YOLOV5标准目录结构可直接用于训练与验证无需额外格式转换。数据聚焦单类person目标训练集包含3000张图像及对应3000个txt标注文件测试集包含300张图像及对应300个txt标注文件覆盖生活场景中多种环境与姿态的行人形态适合目标检测入门及行人识别模型实战。除标注数据外还提供类别字典txt文件和可视化py脚本随机传入一张图片即可自动绘制边界框并保存至当前目录脚本无需任何修改即可直接运行极大方便标注质量的快速检视与使用前验证。整个压缩包共2000个文件以txt标注文件为主体附1个Python可视化工具包体约523MB。目前已有356人学习使用对于希望快速获取规范YOLOV5数据集开展模型训练的开发者是省时省力的选择。1. 目标检测数据集YOLOV5目录格式为什么行人检测项目先卡在数据目录上做目标检测落地的第一关经常不是模型选型而是数据集的目录结构能不能被训练脚本直接吃进去。行人目标检测数据集按YOLOv5目录格式组织是实际项目里最常见的数据交付形态images放原图labels放同名txt标注train/val/test子集切分明确。这套结构看似简单但标签路径错、类别ID对不上预训练权重、边界框没归一化训练要么直接报错要么跑十几个epoch都看不到loss下降。这篇文章面向两类人想用开源行人数据集做训练的新手想把自有场景数据整理成YOLOv5可直接消费格式的工程师。我会把目录规范、转换脚本、校验方式和常见坑一次讲透照着做完你就能拿到一份让YOLOv5稳定吃进去的行人数据资产。2. 行人数据集按YOLOv5目录格式组织的完整规范目录、标签与三类选型理由2.1 images/labels双目录结构与train/val/test划分标准一个规范的YOLOv5行人数据集顶层结构通常是这样的dataset/ ├── images/ │ ├── train/ │ │ ├── street_001.jpg │ │ └── street_002.jpg │ ├── val/ │ │ └── street_100.jpg │ └── test/ │ └── street_150.jpg ├── labels/ │ ├── train/ │ │ ├── street_001.txt │ │ └── street_002.txt │ ├── val/ │ │ └── street_100.txt │ └── test/ │ └── street_150.txt ├── data.yaml └── README.md这里最关键的一点是images和labels必须保持同一套子目录名文件名去掉扩展名后一一对应。YOLOv5在读取data.yaml时会根据images路径自动推导labels路径默认认为labels和images在同一个父目录层级下。如果images/train里放着street_001.jpglabels/train里却没有street_001.txt训练时这张图会被当成无标签样本直接跳过。反过来如果txt文件比jpg多训练能正常启动但val阶段的指标会严重失真因为你根本不知道哪些图片真正参与了回归。目录名称的大小写问题也踩过不少人。YOLOv5官方对大小写不敏感但实际部署到Linux服务器或Docker容器里文件系统是大小写敏感的Labels、LABELS这类命名会在训练中途报FileNotFoundError。所以建议全程小写这是最省心的约定。train/val/test的划分比例上行人检测和其他任务没有本质区别总原则是训练集占比不低于70%常见做法是8:1:1或者7:2:1。行人检测对场景多样性比普通物体更敏感人的外观在不同光照、季节、拍摄角度下差异极大如果训练集全来自同一个摄像头同一段路val再漂亮也说明不了问题模型换个路口就废了。2.2 行人标注的核心参数归一化坐标、类别ID与边界框截断处理YOLOv5的标签文件每行对应一个目标格式固定为class_id x_center y_center width height这五个值全部是归一化到[0,1]的浮点数。x_center和y_center是边界框中心点相对整张图的位置width和height是边界框相对图宽图高的比例。举个例子一张1280×720的街景图里一个人行人的框左上角像素坐标是(320, 200)右下角是(480, 600)对应的标签行就是0 0.3125 0.5556 0.1250 0.5556计算过程是中心点x(320480)/2/12800.3125中心点y(200600)/2/7200.5556宽width(480-320)/12800.1250高height(600-200)/7200.5556。最容易出错的地方是忘记归一化直接把像素值写进txt此时训练能启动但模型输出的框位置会乱飘基本不可用。下面这张参数表可以快速对照参数含义取值区间示例值(1280×720图)x_center框中心点横坐标占图宽比例0~10.3125y_center框中心点纵坐标占图高比例0~10.5556width框宽占图宽比例0~10.1250height框高占图高比例0~10.5556类别ID从0开始计整数。纯行人检测通常只定义一个类别ID是0。如果数据集里还细分了骑车人、儿童、群体data.yaml里的names字段声明顺序必须和标注ID严格一致。这里有个容易被忽略的点预训练权重是在COCO这类大型数据集上训练出来的COCO里person类别索引是0所以单类行人数据集用ID0能和预训练权重语义对齐迁移效果最好。如果把ID换成1训练初期要多花不少时间做语义重对齐收敛肉眼可见变慢。关于边界框的截断处理行人被遮挡、被图边缘切掉一半是常态。行业里更接受只标注可见部分的方案而不是标注完整身体。原因很实在标注员推测被遮挡部分形状时主观性太强不同人画出来的完整框不一致模型学进去的其实是标注噪声。只标可见部分虽然框的尺寸分布带截断偏差但标注一致性能保证模型能学到稳定的规则。如果手里的数据标注风格本身就不统一宁可花时间清洗也别指望模型自己学会兼容。2.3 为什么选这套格式做行人数据集生态、兼容与现实选择现在YOLOv8、YOLOv11都出来了为什么还要按YOLOv5目录格式来人这个问题我被问过不少次。原因其实很朴素。第一YOLOv5目录格式已经是标注工具的事实输出标准。LabelImg、X-AnyLabeling、Roboflow导出的YOLO格式目录结构和标签语法完全一致。不管你是从零开始标注行人数据还是从开源社区下载行人数据集拿到的基本都是这套格式。YOLOv8、v11在数据加载层完全兼容这套格式改一下data.yaml就能直接训练反而是一开始就按某个新版本的特殊格式组织数据遇到工具不支持会非常麻烦。第二生态里的坑别人都踩过了。YOLOv5被使用的时间最长数据增强、超参数调节、部署转换各个环节的踩坑记录最全。在数据集格式上沿用v5标准遇到问题能搜到的经验远比新版本多。第三预训练权重的衔接更顺v5到v8再到v11的权重虽然都能用在这份数据上但从v5的COCO权重起步遇到不收敛的概率远低于从v11直接起步。数据格式标签组织方式转YOLOv5格式工作量工具生态支持度YOLOv5 txt每图一个同名txt无需转换最全面VOC XML每图一个xml需写转换脚本较全面COCO JSON全数据集一个json需拆分并转换一般如果项目对模型效率有更高的要求这份数据直接喂给YOLOv8或v11训练也没问题数据本身不需要重新组织。只是对大多数行人检测场景来说YOLOv5这条链路仍然是部署资料最全、踩坑成本最低的选择。3. 把原始标注转成YOLOv5目录格式转换脚本、数据集划分与data.yaml配置3.1 VOC XML/COCO JSON 转成 YOLOv5 txt一个能直接跑的转换脚本从开源渠道拿到的行人数据集常见的是VOC格式或COCO格式。VOC格式每张图对应一个XML文件COCO格式整个数据集对应一个JSON文件。第一步是把它们统一转成YOLOv5的txt格式。这里给出一个兼容VOC XML的转换脚本import os import xml.etree.ElementTree as ET def voc_xml_to_yolo(xml_path, class_names, out_dir): 把单张图的VOC XML标注转成YOLOv5格式txt class_names: 类别名列表列表下标就是class_id tree ET.parse(xml_path) root tree.getroot() img_w int(root.find(size/width).text) img_h int(root.find(size/height).text) lines [] for obj in root.findall(object): name obj.find(name).text if name not in class_names: continue cls_id class_names.index(name) box obj.find(bndbox) x_min float(box.find(xmin).text) y_min float(box.find(ymin).text) x_max float(box.find(xmax).text) y_max float(box.find(ymax).text) x_center (x_min x_max) / 2.0 / img_w y_center (y_min y_max) / 2.0 / img_h width (x_max - x_min) / img_w height (y_max - y_min) / img_h # 越界坐标裁到合法区间避免训练时算IoU出现负数 x_center max(0.0, min(1.0, x_center)) y_center max(0.0, min(1.0, y_center)) width max(0.0, min(1.0, width)) height max(0.0, min(1.0, height)) lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}) base os.path.splitext(os.path.basename(xml_path))[0] out_path os.path.join(out_dir, base .txt) with open(out_path, w, encodingutf-8) as f: f.write(\n.join(lines)) return len(lines)这段脚本的核心逻辑就是先把VOC的[x_min, y_min, x_max, y_max]坐标读取出来再除以图像宽高完成归一化。有两个地方要特别留意第一VOC里经常出现坐标为0的框说明目标紧贴图像边缘归一化后是0这没问题但如果出现负值说明标注工具导出了越界坐标必须裁掉否则YOLOv5在算anchor匹配和IoU时会出现异常结果。第二width或height归一化后小于0.001这个框几乎是一条线属于无效标注建议直接过滤留着只会污染训练。COCO JSON转YOLO格式的原理类似区别在于COCO的坐标是[x_min, y_min, width, height]的绝对像素值而VOC是[x_min, y_min, x_max, y_max]两边不能混用。如果把COCO的width当成了x_max来用转换出来的框会全部偏移训练时能看到loss收敛但mAP为0非常迷惑。目录整理动作一般用shell脚本批量完成# 在数据根目录下创建YOLOv5标准目录 mkdir -p dataset/images/train dataset/images/val dataset/images/test mkdir -p dataset/labels/train dataset/labels/val dataset/labels/test # 把jpg移动到images/train同名txt移动到labels/train for img in raw_data/train/*.jpg; do base$(basename $img .jpg) mv $img dataset/images/train/ mv raw_data/train/${base}.txt dataset/labels/train/ done如果原始文件名带空格或中文建议先统一重命名成字母数字下划线的形式。YOLOv5的Dataset类能处理带空格的文件名但到了C部署阶段很多推理框架的路径解析会直接出问题。数据命名规范化是花十分钟能避免后续大量麻烦的事情。3.2 按8:1:1划分train/val/test随机种子与按视频ID分组数据集划分不能手动拖文件夹必须脚本化。原因只有一个可复现性。调模型的人需要知道训练集和验证集是怎么分的否则报告里的mAP数字没法复核。这里给出一个用random.shuffle实现的三分划分脚本import os import random import shutil random.seed(42) # 固定随机种子保证每次划分结果一致 src_img_dir dataset/images/all # 先把所有图放在一起 src_lbl_dir dataset/labels/all train_ratio, val_ratio 0.8, 0.1 # test自动取剩余0.1 all_imgs [f for f in os.listdir(src_img_dir) if f.endswith((.jpg, .jpeg, .png))] random.shuffle(all_imgs) n_train int(len(all_imgs) * train_ratio) n_val int(len(all_imgs) * val_ratio) split_map { train: all_imgs[:n_train], val: all_imgs[n_train:n_train n_val], test: all_imgs[n_train n_val:], } for subset, files in split_map.items(): os.makedirs(fdataset/images/{subset}, exist_okTrue) os.makedirs(fdataset/labels/{subset}, exist_okTrue) for f in files: base os.path.splitext(f)[0] shutil.copy(os.path.join(src_img_dir, f), fdataset/images/{subset}/) shutil.copy(os.path.join(src_lbl_dir, base .txt), fdataset/labels/{subset}/)这个脚本最重要的一行是random.seed(42)。如果不固定种子每次跑划分结果都不一样后面实验对比时根本无法判断精度变化到底来自模型改动还是数据划分变动。另一点划分时千万别用listdir返回的文件顺序直接按序号切因为操作系统返回的顺序往往按文件名排而采集设备通常按时间戳命名这样划分出来的train和val在时间分布上完全不独立val等于变相泄露了未来信息。行人数据集还有一个特殊的坑如果数据来自某个固定摄像头的连续视频帧直接随机划分会让高度相似的相邻帧同时出现在train和val里导致val精度虚高。严谨做法是先按视频片段ID分组保证同一组视频只进一个集合。开源行人数据集通常已经按场景分好了目录转换之前先看一眼命名规则再动手。3.3 生成data.yaml并启动训练行人检测的关键训练参数data.yaml是YOLOv5读取数据集的入口文件配置很简洁# data.yaml train: dataset/images/train val: dataset/images/val test: dataset/images/test nc: 1 names: [pedestrian]train和val字段建议写相对项目根目录的路径不要写绝对路径。绝对路径在本机跑没问题换台机器就容易忘改。我习惯在训练脚本里用os.path.abspath动态生成这几行避免路径漂移。names列表的顺序就是标注里class_id的语义映射顺序不能随便排。训练命令给一个稳妥的初始配置python train.py \ --data dataset/data.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 100 \ --workers 4 \ --project runs/pedestrian这里几个参数对行人检测尤其敏感。--img 640是速度和检测小目标能力之间的折中如果数据集里远距离小行人占比很大可以把--img提到960或1280代价是显存和训练时长大约涨3倍。--batch不建议贪大batch太大配合默认anchor策略容易在训练早期出现loss震荡从16起手比较稳稳定后再逐步往上加。--epochs对行人这类类内差异大的任务100轮是底线不少数据集在80轮之后mAP仍在缓慢爬升。手里有yolov5s.pt预训练权重就直接用完全从随机权重训练行人检测要在同样数据规模下多花两到三倍的轮次而且对数据质量更敏感。4. 行人数据集的五个高频坑标签错位、小目标漏检与不收敛排查4.1 标签文件与图片不同名启动即报错的排查链路现象运行train.py后日志里出现大量WARNING: Ignored corrupted labels或No labels found in dataset/images/train训练整体不跳轮次loss一直是NaN。原因最直接的原因是labels目录下的txt文件名和images下的jpg文件名不一致。常见的有两种情况一是标注工具导出时给txt改了名或加了后缀二是图片从相机或手机拷出来时被系统自动重命名比如IMG_20240101_123456.jpg到电脑上变成IMG_20240101-123456.jpgtxt没有跟着变。解决写两行代码做一致性核验不要用肉眼看import os img_dir dataset/images/train lbl_dir dataset/labels/train img_bases {os.path.splitext(f)[0] for f in os.listdir(img_dir) if f.endswith((.jpg, .png))} lbl_bases {os.path.splitext(f)[0] for f in os.listdir(lbl_dir) if f.endswith(.txt)} missing_labels img_bases - lbl_bases missing_images lbl_bases - img_bases print(缺标签的图片:, len(missing_labels)) print(缺图片的标签:, len(missing_images)) for name in list(missing_labels)[:5]: print( 示例:, name)跑完如果缺标签的图片数量不为0就去原始数据源补齐。这里有个容易放松警惕的点缺失比例小于1%时YOLOv5会直接跳过这些图片继续训练日志里可能只有一行警告很难注意到。最终模型上线后在某些特定场景漏检回查才发现训练时把某个角度的样本全跳过了。所以这个校验动作养成分训练前必跑的习惯成本极低收益极高。4.2 类别ID与预训练权重错位loss正常但mAP极低现象训练过程一切正常loss曲线漂亮地下降训练结束后用val集验证mAP只有0.1左右检测框的位置大致合理但输出的类别标识全是乱的。原因标注里的类别ID和data.yaml里names的顺序不一致。比如txt里行人标的是2但data.yaml里的names写成了[car,bicycle,pedestrian]这样行人的语义就和COCO预训练权重里的第三个类别错位了。检测框确实学到了哪里有目标但类别语义完全是乱的。解决先统计txt第一列出现过的类别ID再和names长度对比# 统计labels里出现过的class_id awk {print $1} dataset/labels/train/*.txt | sort -n | uniq输出的最大值如果大于等于names列表长度说明类别ID越界一定有哪里错了。还有一种隐蔽情况labels里同时出现0和1但你的names只有[pedestrian]此时所有ID为1的框会被YOLOv5直接忽略等于数据量少了一半。这种问题靠人眼基本发现不了脚本统计是唯一的可靠手段。4.3 小尺寸行人框被当背景远距离漏检的根源现象模型对近处行人检测良好对图像远端的小行人经常漏检val集上small尺寸的AP远低于medium和large。原因YOLOv5默认的anchor配置是在COCO数据集上统计出来的COCO里小目标占比不高模型在低分辨率特征层上对小目标的回归能力本来就偏弱。行人检测场景尤其是监控视角小目标占比往往很高如果不针对这个偏差做调整漏检是必然。解决第一选择是提高输入分辨率--img从640提到960或1280。第二是写脚本统计小框的占比先搞清楚数据分布再决定手段import os sizes [] for txt in os.listdir(dataset/labels/train): path os.path.join(dataset/labels/train, txt) with open(path) as f: for line in f: parts line.strip().split() if len(parts) ! 5: continue w, h float(parts[3]), float(parts[4]) sizes.append((w, h)) small sum(1 for w, h in sizes if w * 640 32 and h * 640 32) total len(sizes) print(f小目标(小于32x32)占比: {small / total * 100:.1f}%)这个脚本假设训练输入分辨率是640×640所以把归一化的宽高乘以640得到训练尺度下的像素尺寸。如果小目标占比超过30%说明数据集本身就是小目标主导的分布这时候别只靠调anchor推理端可以考虑用切片推理方案做补偿训练端则要把mosaic增强中的单图比例调低避免小目标在拼接时被切得更碎。4.4 空标签与纯背景图删不删除是个选择题现象labels里存在内容为空的txt文件对应图片里确实没有人训练时YOLOv5会跳过这些图片。有的同事觉得空标签碍事直接连图带标签全删掉。原因空标签文件的来源多半是数据清洗时把不确定是否含人的模糊图片标注清空了或者标注工具导出时把漏标的图片也导出了一个空txt。解决这里给一个反直觉的建议保留适量纯背景图。YOLOv5在训练时对负样本的处理依赖背景比例完全不出现背景图会导致模型对空场景的误检率升高具体表现就是图片上明明没有行人模型却给出置信度很高的框。纯背景图的比例控制在训练集总量的5%到10%之间比较合适太少没用太多浪费训练容量。如果手里的纯背景图全是黑白监控画面或低质量压缩帧建议去掉这些低质量背景会让模型把亮度特征误当成行人特征。4.5 YOLOv5超参数适配行人场景anchor与mosaic的隐藏坑现象用默认超参数训练行人数据集训练时loss下降正常但val集的召回率偏低尤其是遮挡严重的样本几乎全漏。原因YOLOv5默认的超参数是在COCO数据集上调出来的COCO的类别多样性高、目标尺度分布均衡而行人数据集的特点是类别单一、尺度变化大、遮挡严重。anchor设置和mosaic增强强度对这类数据的适配度其实并不好。解决在训练前先用YOLOv5自带的anchor自适应脚本重新聚类命令是python train.py --data dataset/data.yaml --weights yolov5s.pt --img 640 --epochs 1YOLOv5默认会在前几个epoch自动计算anchor但如果想直接控制anchor配置可以单独运行聚类脚本生成anchor文件再在yaml里指定。mosaic增强方面行人数据集的样本本来就少mosaic的概率可以适当调低让模型多看到完整的人而不是常常被切碎的人。这些参数在YOLOv5的hyp配置里都有对应项调之前一定先看默认值别盲目往极限拉否则训练早期loss容易出现剧烈震荡。5. 训练前的最后10分钟数据集体检脚本与可视化验证5.1 一个脚本统计框数、空标签与小目标占比训练启动前把下面这个脚本放在项目根目录跑一遍它会直接告诉你这份行人数据集在数量层面的真实状态import os train_dir dataset/images/train label_dir dataset/labels/train total_boxes 0 empty_labeled 0 total_valid 0 for img_name in os.listdir(train_dir): base os.path.splitext(img_name)[0] txt_path os.path.join(label_dir, base .txt) if not os.path.exists(txt_path) or os.path.getsize(txt_path) 0: empty_labeled 1 continue total_valid 1 with open(txt_path) as f: total_boxes sum(1 for line in f if len(line.strip().split()) 5) print(f有标签图片数: {total_valid}) print(f空标签/无标签图片数: {empty_labeled}) print(f总标注框数: {total_boxes}) print(f平均每图框数: {total_boxes / max(total_valid, 1):.1f})正常的行人检测数据集平均每图框数在1到4之间。算出来如果小于0.5说明大量图片是空背景或标注漏标严重模型训练出来会很虚大于10则说明是人群密集视角要多关注模型在较小尺寸目标上的表现。5.2 可视化抽查的三个观察维度数字体检能发现数量异常但发现不了标注质量问题。最有效的办法是随机抽200张图把标注框直接画上去用肉眼翻一遍。翻看时重点看三个维度框有没有明显超出行人身体边界两个行人挨得很近时框是否重叠出格小行人的框是不是只圈住半个人。这三个维度分别对应标注精度不足、遮挡处理不当和漏标。如果抽样图里有超过10%的框存在这些问题先修标注再训练否则模型会把标注误差也当特征学进去。最后说一个我自己的习惯每次拿到新的行人数据集我都会把目录校验、格式转换、数据集划分、体检统计、可视化抽查这五步串成一个固定脚本输出一份数据集质量报告之后才动train.py。这套流程看起来要多花半小时但确实能挡住九成以上的训练翻车。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进