简介:面向YOLO系列目标检测学习与实战的仓库工人数据集,主要由623张真实仓储场景图像构成,标注了工人位置及安全相关目标,可用于工人安全防护检测、人员活动监控等任务的模型训练。数据集已完成训练/验证/测试划分,适配YOLOv5、v7、v8、v9、v10、v11等主流版本,解压后可直接训练与验证,无需额外标注或比例拆分。压缩包共1870个文件,包括623张jpg原图、623份txt格式YOLO标签、623份xml格式VOC标签及1个yaml配置文件。其中txt标签采用class、归一化中心坐标和宽高的标准写法,yaml便于定义类别与路径,xml则为需要VOC数据流的工具提供了衔接通道。整体压缩包约30.25MB,体量适中,易于下载和分享。已有43人学习/浏览,适合正在开展仓储安全项目、工人行为识别或目标检测课程设计的开发者,便于快速对比不同YOLO版本在该场景上的精度和速度表现。
1. 一个能直接训练的仓库工人YOLO数据集:623张带标签图的价值在哪
做目标检测的都知道,找数据集最怕两种:一种是标签格式不统一,拿到手要先写半天的解析脚本;另一种是没做划分,训练集和验证集混在一起,跑的指标根本没法信。这份warehouse-skj9z压缩包是仓库场景的工人目标检测数据集,623张图像,每张图都同时带了YOLO格式的txt和VOC格式的xml标签,训练验证测试划分得明明白白,拿给yolov5、yolov8、yolov9甚至yolo11这系列算法都能直接开训。适合刚入门YOLO训练流程的学生,也适合做智慧仓储、安全帽检测这类业务场景的工程师。
2. 双重标签解析:YOLO的txt和VOC的xml到底记录了哪些坐标信息
拿到数据集第一件事,不是急着训练,而是把标签文件的结构摸透。这套datasets最方便的地方在于同时给出了两种主流标签格式,可以互相校验。我先讲YOLO格式的txt,再讲VOC格式的xml,最后讲两者怎么联动验证。
2.1 YOLO标签txt的坐标定义和像素换算
YOLO系列的标签文件是纯文本,每行代表一个目标框,格式固定为<class> <x_center> <y_center> <width> <height>。五个字段全部用空格分隔,类别索引从0开始计数,四个坐标值全部是归一化后的比例,数值范围在0到1之间,对应关系如下表。
| 字段 | 含义 | 取值范围 |
|---|---|---|
| class | 目标类别索引 | 0到类别总数减1 |
| x_center | 目标框中心点X坐标(相对图像宽度) | 0到1 |
| y_center | 目标框中心点Y坐标(相对图像高度) | 0到1 |
| width | 目标框宽度(相对图像宽度) | 0到1 |
| height | 目标框高度(相对图像高度) | 0到1 |
归一化坐标一旦写错,目标框就会偏移或者尺寸错误。我习惯于拿到txt后立刻做一次像素级还原,确认坐标确实落在图像有效范围内。下面这段脚本可以把txt里的归一化坐标转成像素坐标,顺便检查边界。
import os def yolo_txt_to_pixel(txt_path, img_width, img_height): invalid_boxes = [] with open(txt_path, 'r', encoding='utf-8') as f: lines = f.readlines() for line in lines: parts = line.strip().split() if len(parts) != 5: print(f'异常行内容: {line.strip()}') continue cls, x_c, y_c, w, h = int(parts[0]), float(parts[1]), float(parts[2]), float(parts[3]), float(parts[4]) x1 = int((x_c - w / 2) * img_width) y1 = int((y_c - h / 2) * img_height) x2 = int((x_c + w / 2) * img_width) y2 = int((y_c + h / 2) * img_height) print(f'类别{cls}: 左上({x1},{y1}) 右下({x2},{y2})') if x1 < 0 or y1 < 0 or x2 > img_width or y2 > img_height: invalid_boxes.append((txt_path, line.strip())) return invalid_boxes # 调用示例,图像尺寸以实际图为准,这里用640x640 invalid = yolo_txt_to_pixel('labels/train/img_0509_92.txt', 640, 640) print('越界框数量:', len(invalid))这段脚本的逻辑是逐行读取txt,把字符串拆开,前五列分别映射到类别、中心点和宽高。然后就是纯数学换算:左上角x是中心点减半宽,右下角x是中心点加半宽,Y轴同理。换算完成后判断是否超过图像边界,越界的框单独收集。参数说明两点:一是img_width和img_height必须传实际图像尺寸,替换成PIL.Image.open(image_path).size得到更准;二是把最后的打印改成累计统计就能批量扫整个目录,我一般直接跑批量检查,单文件只是看格式对不对。
2.2 VOC格式xml的读取逻辑与标签字段
VOC格式用XML描述目标框,这套数据集把xml统一放在另一个文件夹中,文件名和图片名一致,比如img_0509_92.jpg对应的就是img_0509_92.xml。xml里真正用来画框的是bndbox节点下的xmin、ymin、xmax、ymax四个值,它们是像素坐标,不归一化,这一点和YOLO格式有本质区别。
<annotation> <filename>img_0509_92.jpg</filename> <size> <width>640</width> <height>640</height> <depth>3</depth> </size> <object> <name>worker</name> <bndbox> <xmin>120</xmin> <ymin>80</ymin> <xmax>420</xmax> <ymax>560</ymax> </bndbox> </object> </annotation>解析xml常用xml.etree.ElementTree,读取所有object节点,从中提取name和bndbox四个子节点。需要注意:VOC里不存类别索引而是存类别名,所以在做格式转换时,必须维护一个类别名称到索引的映射表,顺序不能乱,否则训练出来的类别索引对不上。下面这段代码可以直接把xml转成yolo格式的txt内容。
import xml.etree.ElementTree as ET def voc_xml_to_yolo(xml_path, target_size): tree = ET.parse(xml_path) root = tree.getroot() img_w = int(root.find('size/width').text) img_h = int(root.find('size/height').text) # 类别映射表,必须和训练yaml里的names顺序一致 class_map = {'worker': 0, 'forklift': 1, 'helmet': 2} yolo_lines = [] for obj in root.iter('object'): name = obj.find('name').text if name not in class_map: continue cls_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) x_c = ((xmin + xmax) / 2) / img_w y_c = ((ymin + ymax) / 2) / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h yolo_lines.append(f'{cls_id} {x_c:.6f} {y_c:.6f} {w:.6f} {h:.6f}') return yolo_lines # 调用示例 lines = voc_xml_to_yolo('annotations/img_0509_92.xml', (640, 640)) print('\n'.join(lines))这个脚本的关键在于class_map的维护。VOC的<name>是字符串,YOLO的类别是数字,两者对应关系一旦弄错,训练出来的模型在推理阶段就会把工人识别成货架一类的错乱对象。另外日期里size节点的宽高需要和实际图片一致,如果xml里记录的尺寸和真实图片尺寸不一致,转换成txt时算出的比例也是错的。
2.3 txt与xml的联动校验:标签一致性检查
双标签格式给了我们一个偷懒的机会:把txt转回像素坐标,和xml提取的bndbox对比,如果偏差超过几个像素,说明其中一份标签是复制粘贴改错的。我常用的检验逻辑是遍历同一图片文件的txt和xml,把txt还原成xmin ymin xmax ymax,再和xml的对应字段求差异。
import os import xml.etree.ElementTree as ET def check_label_consistency(image_id, labels_dir, voc_dir): txt_path = os.path.join(labels_dir, f'{image_id}.txt') xml_path = os.path.join(voc_dir, f'{image_id}.xml') if not os.path.exists(txt_path) or not os.path.exists(xml_path): print(f'{image_id} 标签缺失: txt={os.path.exists(txt_path)} xml={os.path.exists(xml_path)}') return False # 从xml中提取像素坐标 tree = ET.parse(xml_path) root = tree.getroot() img_w = int(root.find('size/width').text) img_h = int(root.find('size/height').text) xml_boxes = [] for obj in root.iter('object'): box = obj.find('bndbox') xml_boxes.append((int(box.find('xmin').text), int(box.find('ymin').text), int(box.find('xmax').text), int(box.find('ymax').text))) # 从txt中还原像素坐标 yolo_boxes = [] with open(txt_path, 'r') as f: for line in f: parts = line.strip().split() if len(parts) != 5: continue _, x_c, y_c, w, h = parts x_c, y_c, w, h = float(x_c), float(y_c), float(w), float(h) xmin = int((x_c - w / 2) * img_w) ymin = int((y_c - h / 2) * img_h) xmax = int((x_c + w / 2) * img_w) ymax = int((y_c + h / 2) * img_h) yolo_boxes.append((xmin, ymin, xmax, ymax)) # 数量对照 if len(xml_boxes) != len(yolo_boxes): print(f'{image_id} 目标框数量不一致: xml={len(xml_boxes)} yolo={len(yolo_boxes)}') return False for idx, (xb, yb) in enumerate(zip(xml_boxes, yolo_boxes)): diff = sum(abs(a - b) for a, b in zip(xb, yb)) if diff > 5: print(f'{image_id} 第{idx}个框差异过大: xml={xb} yolo={yb}') return False return True # 批量遍历示例 for img_id in ['img_0509_92', 'img_0509_93', 'img_0509_94']: result = check_label_consistency(img_id, 'labels/train', 'annotations/train') print(img_id, result)这段脚本做了三层校验:一是确认txt和xml都存在,二是数量一致,三是坐标差异不能超过5像素。参数说明:labels_dir指向yolo标签目录,voc_dir指向xml目录,image_id是不带后缀的文件名。如果你发现两份标签存在系统性偏差,多半是标注软件导出的坐标系原点不同,这时另说。检验通过后再进训练,能省下大量反复看loss曲线的时间。
3. 用YOLOv8复现训练:从环境准备到mAP出炉
标签结构摸清了,下一步就是真枪实弹地训练。我以yolov8为例做完整演示,因为ultralytics是目前用得最顺手的一套训练框架,对txt标签原生态支持,不需要额外转格式。yolov5、yolov7的原理一致,只是命令行入口略有区别。这一章先把目录和yaml配置理顺,再跑一个数据自检脚本,最后说清楚训练参数的取舍。
3.1 环境安装与数据集目录组织
建议在Linux服务器上用Python 3.10以上的版本,装torch和ultralytics。先创建一个干净的虚拟环境可以避免把系统Python搞乱。目录结构我用的是YOLO标准约定:images和labels同级,训练、验证、测试各三个子目录,这样ultralytics默认的dataloader能找到文件。
python -m venv yolovenv source yolovenv/bin/activate pip install ultralytics torch --index-url https://download.pytorch.org/whl/cu118 # 如果只是CPU环境跑通流程,可以把torch换成CPU版本 mkdir -p warehouse_dataset/images/{train,val,test} mkdir -p warehouse_dataset/labels/{train,val,test}数据集目录的组织决定了后续data.yaml怎么填写。常见的翻车是把所有图片放在一个文件夹,标注文件放在另一个文件夹,然后靠YOLO按文件名去猜对应关系,这种结构即使能跑,验证集的逻辑也是乱的。仓库工人数据集已经帮你分好了,解压后把images和labels整体移入warehouse_dataset目录即可。这里两个mkdir -p命令分别创建原始目录和标签目录,如果你下载的压缩包内部结构不是这样,先用find . -type f | head -20看一眼再调整。
3.2 训练前的数据自检脚本
环境就绪后不要直接敲训练命令,先跑一遍数据自检。自检脚本需要覆盖三个方面:图片能否正常打开、txt标签是否有空文件、类别索引是否在合法范围内。空标签文件会导致训练时该图片没有梯度信号,类别索引越界则会直接报错终止训练。
import os from PIL import Image def dataset_self_check(image_root, label_root, num_classes): images_dir = os.path.join(image_root, 'images') labels_dir = os.path.join(label_root, 'labels') errors = [] total_valid = 0 for split in ['train', 'val', 'test']: split_images = os.path.join(images_dir, split) split_labels = os.path.join(labels_dir, split) if not os.path.exists(split_images): errors.append(f'缺目录: {split_images}') continue img_files = [f for f in os.listdir(split_images) if f.endswith(('.jpg', '.jpeg', '.png'))] for img_file in img_files: img_path = os.path.join(split_images, img_file) base_id = os.path.splitext(img_file)[0] label_path = os.path.join(split_labels, f'{base_id}.txt') try: with Image.open(img_path) as img: img.verify() except Exception as e: errors.append(f'图片损坏: {img_path} -> {e}') continue if not os.path.exists(label_path): errors.append(f'缺少标签: {label_path}') continue with open(label_path, 'r') as f: lines = f.readlines() if len(lines) == 0: errors.append(f'空标签文件: {label_path}') continue for line in lines: parts = line.strip().split() if len(parts) != 5: errors.append(f'标签格式异常: {label_path} -> {line.strip()}') continue cls_id = int(parts[0]) if cls_id < 0 or cls_id >= num_classes: errors.append(f'类别索引越界: {label_path} -> {cls_id}') for coord in parts[1:]: val = float(coord) if not (0 <= val <= 1): errors.append(f'坐标越界: {label_path} -> {coord}') print(f'检查发现 {len(errors)} 个问题') for err in errors[:30]: print(err) return errors errors = dataset_self_check('warehouse_dataset', 'warehouse_dataset', num_classes=2)这段脚本的检查粒度到了每张图片、每个标签、每行坐标。num_classes参数需要和数据集实际类别数对齐,仓库工人场景如果标签只含工人这一类就是1,如果还有安全帽等就对应增加。坐标越界检查的原理很简单,字符串转float后判断是否在0到1闭区间,YOLO格式的归一化坐标理论上不可能是负数或大于1。注意Image.open().verify()只检查文件头不是完整解码,更严格的做法是load一次,但verify更快,适合批量初筛。
3.3 训练参数设置与启动命令
数据自检通过后,先写data.yaml,类名列表必须和标签类别索引顺序一致。然后启动训练。
train: warehouse_dataset/images/train val: warehouse_dataset/images/val test: warehouse_dataset/images/test nc: 2 names: ['worker', 'forklift']训练命令我用yolo detect train,关键参数是model、data、epochs、imgsz和batch。先跑一个小epoch数验证流程能走通,再放大batch正式训练。
yolo detect train model=yolov8n.pt data=warehouse_dataset/data.yaml epochs=20 imgsz=640 batch=16 device=0model=yolov8n.pt表示从预训练权重开始,这种做法叫迁移学习,仓库场景的目标和COCO预训练权重里的person类别有重叠,能明显加速收敛。epochs=20是验证用的短跑,正式跑我一般给100到150个epoch,具体看loss收敛情况。imgsz=640是训练尺寸,如果标签框特别小,可以试试960,显存不够就降到416。batch=16在24G显存显卡上能跑yolov8n,如果用yolov8l模型需要降到8。device=0指定GPU,多卡训练写成device=0,1`。
4. 避坑指南:标签错位、类别索引与显存不足
训练这件事,理论流程跑通只是第一步,真正的价值在踩坑。下面四条是我在多个数据集上反复遇到的经典问题,每一条都按现象、原因、解决的顺序讲清楚。
4.1 现象:训练时loss不降反升
训练刚开始两三个epoch,loss曲线不但没下降,反而从1.0冲到2.5以上,最后要么震荡要么直接NaN。出现这种情况,先看学习率是不是默认的0.01,再看目标框是不是太小。
原因大概率是多方面叠加的。仓库场景下,摄像头俯拍时工人目标在图像里占比很小,比如一个身高150像素的工人落在640×640分辨率的图中,归一化宽高不足0.25。此时如果模型初始权重没有针对小目标优化,前向传播的梯度本身就大,再加上学习率偏高,loss容易爆掉。
解决方法是分三步走。第一步,把学习率调低到0.001或者0.0005,在ultralytics里通过lr0=0.0005指定;第二步,增加warmup_epochs到5,让模型先预热;第三步,如果还是不稳,把imgsz从640降到512,减小输入分辨率能降低单步的计算波动。这套组合拳基本能压住loss爆炸。
4.2 现象:验证阶段检测框全部偏移到角落
训练过程挺正常,loss降得也漂亮,但用验证集跑预测时,画出来的框全堆在图像左上角或者右下角,完全没有贴合目标。这个现象几乎可以肯定是标签坐标写错了。
原因出在txt标签的坐标值上。比如把x_center写成了左上角坐标,或者把width和height直接写成了像素值没有除以图像宽度高度,归一化失败。这类问题在人工标注后搬运、拼接数据集时非常常见,双标签格式的数据集尤其容易发生:某次批量脚本把VOC转YOLO时忘了除以size里的宽高。
解决办法就是回到第2.3节的联动校验脚本,逐张对比txt和xml的坐标差。如果两类格式不一致,以xml的像素坐标为基准重新生成txt。仓库工人数据集既然两种格式都有,这个校验脚本就是你最顺手的排障工具。
4.3 现象:训练命令一敲,报Dataset is empty错误
启动训练后,ultralytics直接提示数据集为空,但明明目录下文件数量是623张,图片也看得见。这种报错通常不是真的空目录,而是路径不对或者标签过滤过于严格。
最常见的原因有三个。一是data.yaml里的train和val路径是相对路径,而当前工作目录和data.yaml不在同一个位置,导致找不到文件。二是标签txt文件内容全部无效,比如只有4列数据或坐标全是0,dataloader在过滤完无效样本后发现一张可用图都没有。三是数据集中存在没有对应标签的图片,被排除后剩余的图片数量被判定为过少。
解决方法是先打印完整的数据集路径,确认路径确实拉起了文件。然后检查txt是否有空文件,用批量脚本统计空标签数量。最后注意data.yaml中train、val的路径建议写绝对路径,避免工作目录切换带来的误解。
4.4 现象:类别数比预期少,训练却正常跑完了
有一种隐蔽的坑:训练没报错,指标看起来也正常,但mAP50只有0.5左右,远低于预期。这时候用代码扫一遍所有txt文件里的类别索引最大值,很可能发现标签里只出现了类别0,而类别1的框数为0。
原因在于标注阶段或格式转换阶段,某个类别的标签被过滤了。常见的过滤逻辑是类别名匹配失败,比如xml里写的是foklift少了一个字母,在转换脚本里被当成未知类别直接跳过,导致该类别所有框消失。注意这种情况训练流程不会报任何错误,因为类别0的样本量足够让模型学习。
解决方法是写一个类别分布统计脚本,遍历所有标签文件统计每个类别索引的出现次数。如果发现某个类别的总框数不到几十个,要么补充该类的增强样本,要么检查转换映射表哪里有拼写错误。这个脚本我会在最后单独讲。
5. 验证与导出:从best.pt到ONNX的完整走查
模型训练完,还差最后一道工序:用自己的验证机制确认效果,然后导出成可以部署的格式。先跑验证指标,再做推理可视化,最后导出ONNX。
5.1 用验证集跑mAP:val.py的参数怎么调
训练结束后,runs/detect/train目录下会生成weights/best.pt和last.pt。best.pt是在验证集上mAP最高的权重,last.pt是最后一个epoch的权重。用best.pt在验证集上重新跑一次评估,确认最终指标。
yolo detect val model=runs/detect/train/weights/best.pt data=warehouse_dataset/data.yaml batch=16 device=0这个命令会输出一张完整的结果表,包含每个类别的precision、recall、mAP50和mAP50-95。重点关注两个数:mAP50反映的是位置和类别判定的综合准确率,通常仓库场景小目标多,mAP50能到0.85以上就算健康;mAP50-95评价更苛刻,阈值从0.5到0.95逐步提高,数值比mAP50低0.2到0.3是正常的,如果差距过大说明框的定位精度不够好。batch=16影响评估速度但不影响指标,显存小就调成8。
5.2 推理单张图片并可视化预测框
验证集指标是宏观判断,实际效果还是得看单张图的预测质量。用训练好的权重跑推理,保存带预测框的图片。
yolo detect predict model=runs/detect/train/weights/best.pt source=warehouse_dataset/images/val/img_0509_92.jpg conf=0.25 save=Trueconf=0.25是置信度阈值,低于这个值的预测框会被过滤。可视化的目的是检查是否存在误检和漏检,特别是仓库场景中工人背对镜头或者被货架遮挡的情况,这种图片单一数值指标很难直接反映问题。保存的图片在runs/detect/predict文件夹下,建议把预测结果中明显异常的照片找出来,看看对应的标签文件是否标注正确。推理这一步还有一个隐藏用途:如果best.pt的指标高但推理结果画得乱七八糟,多半是训练集和验证集划分时有泄漏。
5.3 导出ONNX的边界条件
部署阶段最常用的格式是ONNX,可以用yolo export一行命令完成。这里有一个需要注意的边界条件:导出时的imgsz必须和训练时的尺寸一致,否则输出的模型在推理阶段需要额外的resize逻辑,会引入精度损耗。
yolo export model=runs/detect/train/weights/best.pt format=onnx imgsz=640 opset=12 simplify=Trueopset=12是ONNX算子版本,部署用的推理引擎如果版本较老,可以降到11;simplify=True会做一次计算图简化,去掉冗余节点,减小模型体积。导出的best.onnx可以用onnxruntime加载做一个前向推断,验证输出张量的shape是否为(1, 4, 8400)(yolov8在640分辨率下的输出格式)。如果你的部署环境要求动态输入,还需要额外设置动态轴参数,dynamic=True支持动态宽高,但推理速度会稍有下降。仓库场景监控流的输入分辨率通常是固定的,我建议固定imgsz即可。
6. 数据健康度检查:类别分布统计的实用脚本
最后分享一个每次训练前我必跑的脚本:统计所有标签文件的类别分布。这个脚本能帮你发现类别不均衡和标注遗漏问题,比直接看loss曲线更直观。
import os from collections import Counter def count_class_distribution(label_root): counter = Counter() for split in ['train', 'val', 'test']: split_dir = os.path.join(label_root, split) if not os.path.exists(split_dir): continue for label_file in os.listdir(split_dir): if not label_file.endswith('.txt'): continue with open(os.path.join(split_dir, label_file), 'r') as f: for line in f: parts = line.strip().split() if len(parts) >= 1: counter[int(parts[0])] += 1 return counter dist = count_class_distribution('warehouse_dataset/labels') print(dist) # 输出示例: Counter({0: 1080, 1: 320})脚本逻辑不复杂:遍历每个划分目录下每个txt文件的每一行,把类别索引取出来计数。参数只需要一个label_root路径,指向labels的上级目录。当你看到某一类的数量是另一类的三倍以上,训练时需要考虑加mosaic增强或者copy_paste去补齐少样本类别,否则验证集mAP会虚高,因为模型只要猜中多数类就能拉高指标。
我自己以前拿到数据集直接开训,觉得指标够高就完事,后来发现仓库工人数据集里某些姿态的样本太少,模型一到遮挡场景就漏检。从那以后我每次训练前都强制跑一遍类别分布统计,再配合第2.3节的标签一致性检查,确认数据和标签都健康了才进训练。那份623张图的仓库工人数据集解压之后,我也走的这条完整检查链路,希望帮到你。
本文还有配套的精品资源,点击获取