YOLO入门实战:仓库工人检测数据集的标注转换与训练
2026/9/10 8:02:09 网站建设 项目流程

简介:这是一套面向目标检测任务、可直接用于YOLO系列算法训练与验证的仓库工人数据集,聚焦仓储环境下的工人安全场景,尤其适合计算机视觉初学者与算法工程师快速上手。压缩包共收录1870个文件,包含623张JPG图像、623个TXT格式的YOLO标签、623个XML格式的VOC标签以及1个YAML配置文件,整体大小约30.25MB,目录结构清晰便于直接使用。标注提供两类主流格式:TXT文件按<类别> <中心点x> <中心点y> <宽> <高>记录归一化边界框,XML文件则遵循VOC标注规范,数据集已预先划分好训练集与验证集,无需额外处理即可投入YOLOv5/v7/v8/v9/v10/YOLO11等模型训练。已有43人学习下载,对于仓储安全分析、工人行为识别等场景的开发者而言,这份资源能有效节省人工标注时间,开箱即用,适合算法对比、模型测试与快速迭代,尤其适合需要特定场景数据做迁移学习或算法落地的项目。

1. 仓库工人检测数据集,为什么值得拿来当yolo入门样本

仓库场景的视觉感知和开放世界检测不一样:工人、叉车、货架、安全帽这些目标大多小尺寸、互相遮挡、光照不均匀,而且监控视角固定。这个623张图像带标签的仓库工人数据集,正好把yolo系列算法训练中最容易出问题的几个点都压在了同一批数据上——目标比例小、类间形态接近、正负样本不均衡。我拆过不少开放数据集,绝大多数训练效果差不是模型不行,而是数据集的标注格式、划分方式和类别定义没有先理清楚。

这个数据集给的是yolo和voc两套标签,训练集、验证集已经划分好,yolov5、v7、v8、v9、v10、yolo11都能直接用。适合两类人:一是刚接触yolo目标检测,想拿真实场景数据跑通完整训练流程的工程师;二是要做仓库安防、人员统计、行为识别业务,需要先用公开数据验证模型基线再上自有数据的团队。后面几章我会从标签坐标系、目录组织、训练复现到自定义转换脚本,一步步把这个数据集的每个技术细节拆开。

2. yolo标签格式与voc标注的坐标换算逻辑

2.1 yolo格式为什么用归一化坐标

yolo的txt标签每一行是<class> <x_center> <y_center> <width> <height>,四个框坐标都是相对于图片宽高的比例值,范围在0到1之间。也就是说,一张1920x1080的图里,一个中心点在(960, 540)的框,记录为x_center=0.5、y_center=0.5。

这套设计的直接好处是模型训练时不必关心输入图片的绝对分辨率——无论原图是1280x720还是640x640,读取后都会做letterbox缩放到统一尺寸,如果标签存的是绝对值,缩放后所有框全错位。yolo从v3开始就要求标注归一化,就是这个原因。实际拿到这个仓库数据集时,先用下面这段代码抽查几个txt文件的数值范围,可以快速判断标签是否有效:

import os label_dir = "datasets/warehouse/labels/train" for fname in os.listdir(label_dir)[:5]: path = os.path.join(label_dir, fname) with open(path, "r") as f: lines = f.readlines() print(f"=== {fname} ===") for line in lines: parts = line.strip().split() cls = int(parts[0]) xc, yc, w, h = map(float, parts[1:]) # 越界校验:中心点和宽高都必须在(0,1]区间 assert 0 <= xc <= 1 and 0 <= yc <= 1, "center out of range" assert 0 < w <= 1 and 0 < h <= 1, "size out of range" print(f"class={cls}, center=({xc:.3f}, {yc:.3f}), " f"size=({w:.3f}, {h:.3f})")

这里对每个标签做了一次合法性断言,xcyc是边界框中心点的归一化坐标,wh是归一化宽高。跑完后如果没有任何AssertionError,说明这批标签坐标没有越界;如果某几行的宽高为0,训练时会被当成无效目标跳过,但不会报错,会直接影响收敛效果。我之前遇到过标签里出现负值的脏数据,yolov8能跑完训练但mAP一直上不去,最后就是用这个方式排查出来的。

2.2 VOC的xml格式与坐标绝对值

voc格式把标注信息存在xml文件里,每个目标对应一个<object>节点,里面用<bndbox>记录xminyminxmaxymax四个整数,单位是像素绝对值。同样一张1920x1080的图,中心点(960, 540)的框在voc里就是一组具体像素坐标,它依赖图片原始分辨率。

这个数据集同时保留两种格式,意味着同样一份标注,yolo和voc各自存了一份副本。它们的物理坐标与归一化坐标之间的换算关系是:

x_center = (xmin + xmax) / 2 / image_width y_center = (ymin + ymax) / 2 / image_height box_width = (xmax - xmin) / image_width box_height = (ymax - ymin) / image_height
# 用python内置的xml.etree解析一个voc标注文件 python - <<'EOF' import xml.etree.ElementTree as ET tree = ET.parse("datasets/warehouse/annotations/train/img_0509_92.xml") root = tree.getroot() width = int(root.find("size/width").text) height = int(root.find("size/height").text) for obj in root.iter("object"): box = obj.find("bndbox") xmin = int(box.find("xmin").text) ymin = int(box.find("ymin").text) xmax = int(box.find("xmax").text) ymax = int(box.find("ymax").text) x_center = (xmin + xmax) / 2 / width y_center = (ymin + ymax) / 2 / height w = (xmax - xmin) / width h = (ymax - ymin) / height print(f"{obj.find('name').text}: " f"yolo=({x_center:.4f}, {y_center:.4f}, {w:.4f}, {h:.4f})") EOF

这段命令把voc的像素坐标转成了yolo的归一化坐标。注意xml里的size/widthsize/height才是图像原始尺寸,不能用缩放后的尺寸去算,否则转出来的坐标整体偏移。常见的一种误用是把xml里没有size节点,就硬编码一个固定分辨率,这在监控视角数据集上尤其致命——同一套摄像头采集的图分辨率确实一致,但一旦换了测试集就会暴露。

2.3 两种标签格式共存时的目录设计

目录内容格式
labels/train每个jpg对应的txt标签yolo格式
labels/val每个jpg对应的txt标签yolo格式
annotations/train每个jpg对应的xml标注voc格式
annotations/val每个jpg对应的xml标注voc格式
images/train训练图片jpg
images/val验证图片jpg

这个目录布局的好处是yolo训练时直接指定data.yaml里train和val的图片路径,框架会自动在相邻的labels目录里找同名txt。比如图片路径是images/train/img_0509_92.jpg,yolov8会去labels/train/img_0509_92.txt找对应标签。如果你要按自己的习惯重排目录,一定保持这种图片和标签同名同目录前缀的结构,否则会报found no labels的警告。

3. 训练前的数据集体检与图像组织分析

3.1 用脚本统计标注框的尺寸分布和遮挡情况

拿到数据集不要直接开训,先做一次标注分布统计。623张图虽然不算大,但仓储场景里工人目标往往只占画面的5%到15%,小目标比例如果过高,默认的yolo参数跑出来会漏检严重。统计每个标注框的相对面积、宽高比,能判断该不该调anchor或者是否要加tiling策略。

import os import numpy as np label_dir = "datasets/warehouse/labels/train" all_boxes = [] for fname in os.listdir(label_dir): if not fname.endswith(".txt"): continue path = os.path.join(label_dir, fname) with open(path, "r") as f: for line in f: parts = line.strip().split() if len(parts) < 5: continue _, xc, yc, w, h = parts all_boxes.append([float(xc), float(yc), float(w), float(h)]) boxes = np.array(all_boxes) areas = boxes[:, 2] * boxes[:, 3] print(f"total boxes: {len(areas)}") print(f"area min/mean/max: {areas.min():.6f}, " f"{areas.mean():.6f}, {areas.max():.6f}") print(f"boxes smaller than 0.01: {(areas < 0.01).sum()} " f"({(areas < 0.01).mean()*100:.1f}%)")

这段统计里,areas是每个框相对图片面积的占比,小于0.01意味着这个框只占图片面积的1%,按640x640输入算基本就是64x64像素以下的目标。这个仓库数据集如果小目标占比超过30%,我一般会建议配合yolo的sahi切片推理或者把imgsz从640提到960再训练。不要一上来就换模型结构,先看数据分布。

3.2 图片名称序列与样本边界情况

从文件名能看到img_0509_92.jpg这类序号,同一个前缀img_0509说明这大概率是从一段连续监控视频里抽帧得到的,相邻帧之间画面相似度高。这种抽样方式有个隐患:训练集和验证集如果来自同一个视频片段,模型会通过背景记忆目标,而不是真正学习目标的视觉特征,泛化能力虚高。

验证方法也很直接,把训练和验证集的文件名前缀分组,看看有没有重叠:

ls datasets/warehouse/images/train | sed 's/_[0-9]*\.jpg//' | sort -u > train_prefix.txt ls datasets/warehouse/images/val | sed 's/_[0-9]*\.jpg//' | sort -u > val_prefix.txt comm -12 train_prefix.txt val_prefix.txt

如果comm的输出有公共前缀,说明训练和验证存在同源片段。这种场景下的解决方案是重划分,按前缀分组做group k-fold,保证同视频片段全部落在同一折里。这个数据集已经分好了目录,但如果你的下游业务需要更高的泛化保证,这一步值得做。

4. 用yolov8复现训练与验证流程

4.1 准备data.yaml配置

yolo系列所有算法版本都通过一个yaml文件约定数据路径、类别数和类别名。这个数据集已经划分好train和val,我习惯在数据集根目录放一个warehouse.yaml,内容如下:

path: /data/warehouse # 数据集根目录的绝对路径 train: images/train # 相对path的训练图片路径 val: images/val # 相对path的验证图片路径 nc: 2 # 类别数量,按数据集实际标注类别修改 names: 0: worker 1: forklift

注意path字段在yolov5和yolov8里都支持,但在yolov6、yolov7的部分实现里并不识别这个字段,需要把trainval写成完整路径,比如/data/warehouse/images/train。另一个坑是nc必须和标签里的类别索引最大值匹配,如果txt里出现class=2而nc=2,训练时会静默跳过该样本,loss曲线会异常波动。

4.2 执行训练命令与关键参数解析

cd /data/warehouse # 安装依赖(如果还没有ultralytics) pip install ultralytics # 训练:yolov8s 是小模型,适合在单卡上验证数据集质量 yolo train data=warehouse.yaml model=yolov8s.pt epochs=100 imgsz=640 batch=16 workers=4 # 训练完成后在best.pt上跑验证集 yolo val model=runs/detect/train/weights/best.pt data=warehouse.yaml batch=16

第一次跑训练建议把epochs从100降到50,imgsz用640,目的是快速看到收敛趋势而不是追求最终精度。batch根据显存调整:12G显存跑yolov8s可以用16,24G可以到32,太小会让batch normalization统计不稳定。workers是数据加载线程数,Windows下建议不超过4,Linux可以到8,过高反而会因为磁盘IO竞争拖慢每个epoch。

训练日志要重点看两个指标:一个是box_loss是否持续下降,如果前20个epoch loss波动不降甚至上升,大概率是学习率过大或者数据标签有脏数据;另一个是验证集的mAP50mAP50-95的差距,mAP50高但mAP50-95低,说明模型学到了大致位置但框的边界不精确,这种情况下可以适当增加epochs或者换用yolov8m。

4.3 损失函数视角下看小目标训练

yolo系列的损失函数由三部分组成:边界框回归损失、分类损失、置信度损失。yolov8去掉了objectness分支,改为直接在分类分支输出目标度,但对小目标而言,边界框回归的损失权重和IoU计算方式影响更大。yolov8默认使用CIoU loss,它同时惩罚框的重叠面积、中心点距离和长宽比,对仓库工人这种小尺寸目标,CIoU在训练初期梯度更稳。

如果在训练过程中发现box_loss在60个epoch后还有明显下降空间,可以尝试打开数据增强里的mosaicmixup,yolov8默认在训练最后10个epoch自动关闭mosaic以避免过度扰动。对623张的小数据集,这种自动调整反而可能减轻过拟合,但如果你的业务场景要求精确定位工人位置,建议把close_mosaic调大到15,让最后的精调阶段更充分。

5. 自定义voc到yolo的批量转换脚本与标注质量核验

5.1 完整转换脚本与边界条件处理

虽然这个数据集两种格式都已给全,但实际工作中经常遇到只有voc标注的自有数据,需要批量转成yolo格式。核心脚本如下:

import os import xml.etree.ElementTree as ET from glob import glob def voc_to_yolo(xml_path, img_w, img_h, class_mapping): tree = ET.parse(xml_path) root = tree.getroot() lines = [] for obj in root.iter("object"): name = obj.find("name").text if name not in class_mapping: continue cls_id = class_mapping[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) # 越界裁剪:标注框偶尔会超出图像边缘 xmin = max(0, xmin) ymin = max(0, ymin) xmax = min(img_w, xmax) ymax = min(img_h, ymax) # 过滤宽高为0的无效框 if xmax <= xmin or ymax <= ymin: continue x_center = (xmin + xmax) / 2 / img_w y_center = (ymin + ymax) / 2 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") return "\n".join(lines) class_mapping = {"worker": 0, "forklift": 1} xml_list = glob("annotations/train/*.xml") img_w, img_h = 1920, 1080 # 按数据集实际尺寸设置 for xml_path in xml_list: txt_path = xml_path.replace("annotations/train", "labels/train").replace(".xml", ".txt") content = voc_to_yolo(xml_path, img_w, img_h, class_mapping) with open(txt_path, "w") as f: f.write(content)

这段脚本做了一层越界裁剪和无效框过滤,是为了防止voc标注里出现极端情况:比如某个框的xmax大于图片宽度或ymin小于0。如果原始voc标注没问题,这两步就是纯防御。

5.2 可视化验证标注与类别平衡检查

转换完成后,不要只看txt里的数字,把框画到原图上逐张目检是最快的质量验证方式。我一般用OpenCV随机抽50张图,把标注框直接画出来存到debug_bboxes目录:

import cv2 import os import random image_dir = "datasets/warehouse/images/train" label_dir = "datasets/warehouse/labels/train" out_dir = "debug_bboxes" os.makedirs(out_dir, exist_ok=True) images = os.listdir(image_dir) random.shuffle(images) for fname in images[:50]: img = cv2.imread(os.path.join(image_dir, fname)) h, w = img.shape[:2] txt = os.path.join(label_dir, fname.replace(".jpg", ".txt")) if not os.path.exists(txt): continue with open(txt) as f: for line in f: parts = line.strip().split() if len(parts) < 5: continue cls, xc, yc, bw, bh = int(parts[0]), *map(float, parts[1:]) x1 = int((xc - bw / 2) * w) y1 = int((yc - bh / 2) * h) x2 = int((xc + bw / 2) * w) y2 = int((yc + bh / 2) * h) color = (0, 255, 0) if cls == 0 else (0, 0, 255) cv2.rectangle(img, (x1, y1), (x2, y2), color, 2) cv2.putText(img, str(cls), (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) cv2.imwrite(os.path.join(out_dir, fname), img)

画框时先算归一化坐标到像素坐标的还原,x1y1是左上角,x2y2是右下角,这一步和前面2.2节的转换是互逆的过程。如果画出来的框大面积偏移,问题往往出在标签归一化引用了错误的分辨率。这个仓库数据集如果看到某个框把货架的立柱都括进去了,是标签本身框得松散,不影响训练但不适合直接上线。

5.3 类别平衡统计

import collections cls_counter = collections.Counter() label_dir = "datasets/warehouse/labels/train" for fname in os.listdir(label_dir): if not fname.endswith(".txt"): continue with open(os.path.join(label_dir, fname)) as f: for line in f: if len(line.strip().split()) < 5: continue cls_id = int(line.strip().split()[0]) cls_counter[cls_id] += 1 for cls_id in sorted(cls_counter): print(f"class {cls_id}: {cls_counter[cls_id]} instances")

假设输出发现class 0有2100个实例,class 1只有300个,训练出来的模型对class 1的召回率会明显偏低。对623张的小数据集,缓解手段不是简单加样本,而是针对少数类调cls_loss权重——yolov8里可以通过传入weight=参数按类别频率反向加权,或者直接用OpenCV离线复制少数类样本做重复采样。

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

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

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

立即咨询