乱堆物料检测数据集:VOC/YOLO双格式与YOLOv8训练实践
简介:面向工地安全与杂物监管场景的乱堆物料检测数据集,可用于训练识别沙堆、混凝土堆等目标检测模型。数据共1143张jpg图像,每张均配套Pascal VOC格式xml和YOLO格式txt标注文件,文件总数约2000个,压缩包约90.23MB,便于直接接入主流检测框架。数据集标注类别为单一"pile"类,总框数1174,采用labelImg人工绘制矩形框,标注规范统一;除沙堆、混凝土堆外还包含约50张箱子等杂物样本,兼顾一定场景多样性。已有527人学习下载,适合刚入门目标检测的开发者以及需要标准化乱堆物料数据的算法工程师,可直接用于模型训练、验证与算法对比。
1. 乱堆物料检测数据集:双格式、一张7z包能解决什么问题
施工现场、仓库堆场、厂区通道,物料堆放是否合规是安全检查的高频项。"乱堆物料"就是这类监控场景里的一个单类别目标:把不该出现在通道、消防区域、作业面旁边的堆料自动框出来。这份数据集把1143张真实场景图片、1个类别、VOC和YOLO两种标注格式打包进同一个7z压缩包,拿到手就能喂给训练流程,不用自己重新标注。适合正在做工地巡检、安防告警或者想快速验证YOLO迁移学习效果的算法工程师;对新手来说,它也是一个"标注完整、格式双份、规模不大不小"的练手数据源。1143张单类别不算多,但跑通解压、格式校验、训练、验证、导出这一整条链路完全够用。
2. 拿到7z包之后:解压、目录结构和两种格式的对应关系
2.1 Linux和Windows下的7z解压:3条命令和踩过的密码坑
7z不是那种右键一键解压就完事的格式,尤其在服务器上操作时,命令行才是常态。Windows下我用7-Zip或Bandizip,图形界面没什么好说的;Linux下先确认装没装p7zip。
# Debian/Ubuntu系先装p7zip
sudo apt install p7zip-full
# 列出压缩包内容,先看个底,确认目录结构和文件数是否符合预期
7z l 乱堆物料检测数据集VOC+YOLO格式1143张1类别.7z
# 解压到指定目录,保留内部目录结构
7z x 乱堆物料检测数据集VOC+YOLO格式1143张1类别.7z -o./data
第一行是安装,第二行的 l 是list模式,只列目录不解压,适合先确认压缩包没损坏、文件数对不对得上"1143张"这个预期。第三行的 x 是解压并保留路径, -o 后面直接跟目录名,不是 -o 目录 ,这里没有空格,写错了命令行会直接报错。
有个普遍现象:下载完解压输入密码,提示"7z压缩文件密码是正确的但一直报错",CRC校验失败。我遇到过好几次,原因分两类:一是压缩包创建者用了非UTF-8编码的文件名,当前系统语言环境下解压时文件名解析失败;二是7z文件本身在传输过程中损坏。解压前先运行 7z t 做完整性测试,这个习惯能帮你把问题定位到"文件坏了"还是"系统兼容性"上,别一上来就怀疑密码记错了。
2.2 VOC格式的目录约定:JPEGImages、Annotations、ImageSets
Pascal VOC是老牌目标检测数据集的目录组织方式,很多开源检测框架的评测脚本都认它。解压后先看顶层目录,常规结构是这样:
dataset_root/
├── JPEGImages/ # 图片,jpg格式
├── Annotations/ # 每张图对应的xml标注
└── ImageSets/
└── Main/ # train.txt / val.txt / trainval.txt
每个xml对应一张图片,里面的核心内容是 size 和 object 。 size 记录原图宽高, object 里是类别名和检测框的像素坐标。
<annotation>
<folder>JPEGImages</folder>
<filename>img_0001.jpg</filename>
<size>
<width>1920</width>
<height>1080</height>
<depth>3</depth>
</size>
<object>
<name>pile</name>
<bndbox>
<xmin>100</xmin>
<ymin>120</ymin>
<xmax>800</xmax>
<ymax>640</ymax>
</bndbox>
</object>
</annotation>
需要注意,VOC里的 xmin、ymin、xmax、ymax 一律是像素坐标,不做归一化。类别名是字符串,单类别数据里通常是一个固定值。拿到数据后先统计一下所有xml的 <name> 取值,如果是单类别但出现了多个类名,说明标注时写的不统一,后面训练yaml里的 names 会踩坑。
2.3 YOLO格式:images、labels和一行txt的含义
YOLO格式和VOC格式的坐标表达方式完全不同。解压后如果自带 images 和 labels 两个同级目录,说明作者已经转好了一份:
dataset_root/
├── images/ # 和JPEGImages内容一致
│ └── img_0001.jpg
└── labels/ # 每个jpg对应一个txt
└── img_0001.txt
打开一个txt,每一行代表一个目标框,格式固定为: class_id center_x center_y width height ,其中后四个值全部是相对图片宽高的归一化比例。
0 0.468750 0.351852 0.364583 0.481481
这一行的意思是:类别ID是0,目标中心点在图片水平46.875%、垂直35.185%的位置,框宽占整图宽度的36.458%,框高占整图高度的48.148%。归一化的好处是训练时无论 imgsz 缩放到640还是512,标注不用跟着改。坏处是数字不直观,没法直接肉眼判断框画在哪,所以后面一定要做"把txt还原成像素坐标"的验证。
2.4 两份格式怎么对应:文件名主干匹配和数量检查
VOC和YOLO两套目录共存时,靠文件名主干对应: img_0001.jpg 对应 img_0001.xml 和 img_0001.txt 。这个约定很脆弱,任何一方多了空格、改了大写后缀,匹配就断了。
我拿到数据后会先跑一个简易检查,不急着训练:
# 统计三类文件数量
ls JPEGImages/*.jpg | wc -l
ls Annotations/*.xml | wc -l
ls images/*.jpg | wc -l
ls labels/*.txt | wc -l
# 找出jpg里没有对应xml的文件
for f in JPEGImages/*.jpg; do
base="${f%.jpg}"
[ -f "${base}.xml" ] || echo "no xml: $f"
done
这个检查的价值在于:VOC目录里的xml和YOLO目录里的txt如果数量不一致,说明转换时丢过数据;图片和xml不能一一对应,训练时会出现"No labels found"或者某张图没有标签被静默跳过。先把数量对齐,再进训练环节。
另外强烈建议用Python统计一下这1143张图的尺寸分布,乱堆物料场景经常混着不同分辨率的相机画面:
from PIL import Image
import os, collections
img_dir = 'JPEGImages'
sizes = collections.Counter()
for name in os.listdir(img_dir):
if name.lower().endswith('.jpg'):
with Image.open(os.path.join(img_dir, name)) as im:
sizes[im.size] += 1
for size, cnt in sizes.most_common(10):
print(size, cnt)
如果发现大图小图混杂,比如有1920x1080也有960x540,训练时统一缩放到640会把小图拉伸,导致框位置偏移。这种情况要么训练时 imgsz 调小,要么用LetterBox的预处理方式,YOLO默认已经做了保持比例的resize,但知道数据原始分布对你的 imgsz 选择很有帮助。
3. VOC转YOLO:转换脚本与四个边界坑
3.1 为什么题目给了两种格式还可能需要自己转
标题里写"VOC+YOLO格式",正常理解是两种都备好了。但训练链路上你会遇到两种情况:一是你手里的工具版本只认VOC,比如某些mmdetection配置,需要把YOLO格式转回VOC;二是项目要求严格,需要你重新校验一遍标注,最靠谱的方式是自己写脚本从原始VOC转一次YOLO,再和压缩包自带的txt做diff。常见做法是:用脚本在本地重新生成一份labels,拿它和原来的txt逐行对比,坐标误差超过0.002(约1-2像素)就输出警告。这一步能抓出转换脚本的bug,也能发现原始标注本身的问题。
3.2 转换脚本:解析xml、归一化坐标、写txt
我用的转换脚本如下,单类别和多类别都通用,核心是把类名映射成从0开始的整数ID:
import os
import xml.etree.ElementTree as ET
def voc_xml_to_yolo_txt(xml_path, out_txt_path, class_names):
tree = ET.parse(xml_path)
root = tree.getroot()
# 注意:xml里的size是原图像素尺寸,不是缩略图尺寸
size = root.find('size')
img_w = int(size.find('width').text)
img_h = int(size.find('height').text)
lines = []
for obj in root.findall('object'):
name = obj.find('name').text.strip()
if name not in class_names:
# 宁可跳过,也不要把它映射成错误类别
continue
class_id = class_names.index(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, min(xmin, img_w))
xmax = max(0, min(xmax, img_w))
ymin = max(0, min(ymin, img_h))
ymax = max(0, min(ymax, img_h))
if xmax <= xmin or ymax <= ymin:
continue
center_x = (xmin + xmax) / 2 / img_w
center_y = (ymin + ymax) / 2 / img_h
bw = (xmax - xmin) / img_w
bh = (ymax - ymin) / img_h
lines.append(f"{class_id} {center_x:.6f} {center_y:.6f} {bw:.6f} {bh:.6f}")
with open(out_txt_path, 'w', encoding='utf-8') as f:
f.write('\n'.join(lines))
if __name__ == '__main__':
# 类名列表顺序决定ID,单类别就是0;多类别按这个列表固定顺序
classes = ['pile']
xml_dir = 'Annotations'
txt_dir = 'labels'
os.makedirs(txt_dir, exist_ok=True)
for xml_name in os.listdir(xml_dir):
if not xml_name.endswith('.xml'):
continue
stem = xml_name[:-4]
voc_xml_to_yolo_txt(
os.path.join(xml_dir, xml_name),
os.path.join(txt_dir, stem + '.txt'),
classes
)
逻辑说明:先用 size 拿原图宽高,否则归一化结果全错;再遍历每个 object ,用 class_names.index(name) 把类名转成ID;四个坐标取中心点和宽高后除以宽高得到归一化值。写文件时用UTF-8编码,避免Linux和Windows下中文路径出幺蛾子。
参数说明: classes 列表的顺序就是训练时类ID的顺序,单类别永远是 ['pile'] ,代码里写死没问题,但要和数据集yaml里的 names 一致。保留6位小数是为了让框的误差控制在1像素以内,转完最好抽样验证。
3.3 边界坑:归一化精度、空xml、越界框、大小写不一致
第一个坑是归一化坐标精度不够。只保留3位小数,框的误差在640分辨率下能有几十个像素,训练时增强一放大,框对不上目标,模型学的是错误位置。至少保留6位,脚本里写的是 .6f 。
第二个坑是空xml。单类别数据里存在"这张图其实没有乱堆物料"的情况,xml里没有 <object> 节点。转换脚本会生成一个空txt,这是正确的:YOLO把空txt的图当作背景,训练时参与负样本计算。如果脚本把图跳过,背景图就全部消失,模型会把任何东西都当成堆料,误报会高到没法用。
第三个坑是越界框。标注时偶尔会出现 xmax 大于图片宽度、 ymin 小于0的情况,不裁剪直接写进txt,训练时Mosaic增强对越界框的IoU计算是错的,现象是训练损失下降正常但验证mAP上不去。裁剪到 0 ~ img_w 和 0 ~ img_h 是转换脚本里必须做的一步。
第四个坑是类别名大小写和前后缀空格。 Pile 和 pile 会被YOLO当成两个类,如果xml里混着写了, class_names.index(name) 会跳过未知类,最后你的类别数变成2,而其中一个没有样本。解压后第一件事就是统计 <name> 字段的取值分布,把不规范的标注统一掉再跑转换。
4. 用YOLOv8把这份数据集跑起来:环境、yaml和最小训练命令
4.1 Anaconda环境配置要求:Python版本、PyTorch和CUDA
训练YOLO系列最常见的一道坎就是环境。我一般用Anaconda建独立环境,避免和系统Python打架:
# 创建Python 3.10环境,YOLOv8官方推荐的Python范围是3.8-3.12
conda create -n yolo python=3.10 -y
conda activate yolo
# 先装PyTorch,再装ultralytics
# CUDA 12.1对应的命令行安装方式
pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121
pip install ultralytics
注意装 ultralytics 会自动拉一个PyTorch,如果你先装CPU版后装ultralytics,它可能把你的torch覆盖成CPU版。正确顺序是先按CUDA版本装好torch,再装ultralytics,装完验证一句:
python -c "import torch; print(torch.__version__, torch.cuda.is_available())"
输出 True 代表GPU可用。对1143张图片的规模,CPU也能训练,但一个epoch等十几分钟很折磨人;一张V100或类似16G显存的卡,batch调到16,几十分钟就能跑完100个epoch。数据量不大,显卡反而不是最关键的限制,环境别搞坏更重要。
4.2 数据集yaml:路径、类别数和类名
YOLOv8用yaml描述数据集。假设解压后整理成这个结构:
/data/pile_dataset/
├── images/
│ ├── train/
│ └── val/
└── labels/
├── train/
└── val/
对应的数据集配置文件:
# dataset.yaml
path: /data/pile_dataset # 根目录绝对或相对路径
train: images/train # 训练图片相对path的路径
val: images/val # 验证图片相对path的路径
nc: 1 # 类别数
names:
0: pile # 必须和labels里的class_id对应
这里有个很容易错的地方: train 和 val 写的是图片目录,YOLO会自动把路径前缀换成 labels 去找同名txt文件。如果你把label目录命名为 label 而不是 labels ,它找不到标签但又不报致命错,直接跳过所有图,训练出来的模型什么也检测不到。
4.3 最小训练命令:模型、epochs、batch、workers怎么设
数据就绪后,跑最小训练命令:
yolo detect train \
model=yolov8s.pt \
data=dataset.yaml \
epochs=100 \
imgsz=640 \
batch=16 \
workers=4 \
device=0
参数说明: model=yolov8s.pt 会用预训练权重做初始化,第一次运行会自动下载yolo预训练模型;如果下载失败,手动从官方release下载后放在当前目录,命令改成 model=/path/to/yolov8s.pt 。 epochs=100 对1143张单类别是合理的,训练过程里看loss曲线定最终步数,不一定跑满。 batch=16 在16G显存上压力不大;显存小就降到8或4。 workers=4 是数据加载线程数,Windows下如果卡死在数据加载阶段,把它改成0。 device=0 指定第一张GPU,CPU训练写 device=cpu 。
训练结束后输出目录在 runs/detect/train/ 下, weights/best.pt 是按验证集指标保存的最佳权重,预测和部署都用它,不要用 last.pt 。
4.4 训练日志怎么盯:loss趋势、mAP50、误检率
终端输出的表格里有几个关键列: box_loss 是框回归损失, cls_loss 是分类损失,单类别时 cls_loss 通常降得很快; mAP50 是IoU阈值0.5下的平均精度,乱堆物料这种单类别检测,mAP50冲上0.9以上才算标注质量过关。
如果mAP50一直在0.6以下,先别调参,回到数据上:抽查几张图,看标注框是不是把整堆物料只框了一半,或者把背景框进去了。乱堆物料的特点是堆体不规则、遮挡多,标注框稍微松散一点,mAP就被拉低很多。数据没问题再考虑加大epochs或换更大的模型。
5. 乱堆物料检测的常见翻车点:解压、标注到训练逐条排查
5.1 7z压缩文件密码正确但一直报错
现象:解压时输入密码,系统立刻提示密码错误或CRC校验失败,但密码确认是正确的。
原因:两种常见情况。一是压缩包创建者在Windows上用非UTF-8编码存的文件名,Linux下解压时文件名编码转换失败,报错看起来像密码错;二是压缩包传输过程中损坏, 7z t 测试时会定位到具体某个文件出错。
解决:先用 7z l 列出压缩包内容,能正常列出说明密码没输错,问题是文件名编码或文件损坏。Linux下用 7z x 加 -mcp=utf-8 参数强制使用UTF-8解码文件名;Windows下换Bandizip或WinRAR解压,通常能绕过去。如果 7z t 提示某个文件CRC错误,重新下载整个压缩包,别想着只修单个文件。
5.2 图片和xml数量对不上
现象:JPEGImages有1143张,Annotations只有1139个xml,少了4个。
原因:数据采集时有些画面本身就没有乱堆物料,标注时被跳过;也可能是采集端和标注端文件名规则不一致,个别图片改名后xml没跟上。
解决:别直接删那4张图。先跑匹配脚本找出无xml的图片,单独建一个 background/ 目录放进去,训练时作为负样本加入,能显著降低误报。YOLO训练时,如果一张图没有txt,它既不会被当作正样本也不会被当作负样本,而是直接跳过。想让背景图参与训练,必须给它生成一个空的txt文件,这一点在处理数据集用于yolov8训练时特别容易忽略。
5.3 训练完的模型把整个画面框成目标
现象:预测一张没物料的空地上,模型画一个大框,几乎框住全图。
原因:典型的类别ID或标注框越界问题。单类别模型把任何大于阈值的区域都当成目标,通常是因为labels的class_id写成了1而不是0,或者越界框没裁剪导致训练时出现了"全图尺寸"的伪目标。
解决:检查labels目录里的txt文件,把所有txt里的第一个字段统一改成0;在转换脚本中强制让 classes = ['pile'] ,ID由 index() 生成而不是手写。验证方法:用验证集里的某张图跑 yolo detect predict ,把预测框和xml里的原始标注画在一起对比,如果预测框形状与标注相似但位置整体偏移,就是归一化还原时的问题。
5.4 YOLO混淆矩阵总和不为1
现象:训练完看confusion_matrix.png,行和列加起来不是100%,或者数值看着像"丢了样本"。
原因:YOLO的混淆矩阵默认包含 background 这一类,图上的每一格是百分比,但背景列和漏检部分会吃掉权重,所以单类别也有一块非零区域。另一个常见原因是验证集里存在漏标的目标,模型检出来了但标注里没有,在矩阵里表现为额外一列或者偏高的false positive。
解决:不用强行让每行加起来等于1。重点看"背景被误检成堆料"的比例,也就是background那一行里对应pile列的值,如果高说明正样本标注不够全,模型把纹理复杂的背景也当成了目标。这时候去补标注,不是调阈值能解决的。
5.5 训练中途OOM或卡死在数据加载
现象:训练刚开始几秒就报 CUDA out of memory ,或者进度条停在第一个batch不动。
原因:OOM是batch和imgsz太大;卡死大概率是 workers 在Windows下开多线程数据加载的兼容问题。
解决:OOM先把 batch 降到4、 imgsz 降到512,如果还不是,在配置文件里把模型换成 yolov8n.pt ,neck和backbone更小,显存占用能再省一截。卡死就把 workers=0 ,虽然慢一点,但稳定。把训练跑通比一次跑得快要紧,跑通后再逐步加大batch。
6. 把1143张数据榨干净:数据划分、置信度门限和导出检查
先重新划分一次train/val,别直接用压缩包自带的划分。我习惯按文件名哈希或拍摄场景分边,保证同一个摄像头画面不同时出现在训练集和验证集里。随机划分要固定随机种子,8:2切分后训练约914张、验证约229张,对单类别检测足够评估。
训练完先别急着部署,用验证集跑一遍指标,再扫一遍不同置信度门限下的precision和recall:
yolo detect val \
model=runs/detect/train/weights/best.pt \
data=dataset.yaml \
conf=0.15
默认门限是0.25,但在乱堆物料这类安全监控场景里,漏检的代价远高于误报。常见做法是把门限降到0.15或0.1,宁可多报几个框让值班人员复核,也不要漏掉一次真实堆料。对比几个门限下的F1值,选那个让误检和漏检相对平衡的数值作为部署参数。
最后导出成ONNX再落地:
yolo export \
model=runs/detect/train/weights/best.pt \
format=onnx \
imgsz=640
导出的ONNX喂给推理框架,先拿一张测试图跑一遍,确认输出张量的维度对不对、后处理有没有问题。差分这一步能提前暴露训练框架和推理框架之间的算子兼容问题,不然到了现场才发现在嵌入式设备上某个算子不支持,后悔药都没得吃。
做这类单类别检测项目,我最大的教训是:标注质量决定上线效果,模型结构反而是次要的。拿到任何一份双格式数据集,先花半天做解压校验、数量核对、类名统计,再碰训练。1143张单类别确实不多,但配合预训练权重和合理的训练参数,足以在乱堆物料这类固定场景里达到可用的检测精度。希望这套流程帮到你少踩几个坑。
本文还有配套的精品资源,点击获取 