简介:面向煤矿传送带异物检测场景的VOC格式标注数据集,适合工业安全监测与计算机视觉目标检测方向的开发者使用。资源提供2345张煤矿传送带图像的标注信息,压缩包内共2000个XML标注文件,约186.53MB,每个XML均按Pascal VOC标准记录目标类别与边界框坐标,可直接用于训练YOLO、Faster R-CNN、SSD等常见检测模型。标注文件包含train、test等划分前缀,便于分层构建训练集与验证集。目前已有884人学习下载,适合需要真实工业场景数据来完成异物检测算法验证、模型调优或论文实验的工程师与研究人员。通过这批标注数据,可快速搭建煤矿传送带异物识别实验环境,减少数据采集与标注成本,专注于检测模型的设计和改进。
1. 煤矿传送带异物检测数据集:2345张VOC标注图,到底能训练什么模型
做煤矿传送带异物检测的人,手里最缺的不是模型,而是能用的数据。跑井下带式输送机的场景,常见异物是锚杆、托盘、木块、大块矸石,偶尔还有遗落的工具,目标小、背景黑、粉尘重,通用数据集根本覆盖不了。这个名为“煤矿传送带异物检测数据集”的项目提供了2345张真实井下图像,全部用Pascal VOC格式标记,这意味着拿到手的第一步不是训练,而是先读懂XML里的坐标和类别,再按yolov8训练自己的数据集的标准流程转成YOLO格式。适合谁?刚接手煤矿视觉项目、手里有现场相机但没标注数据的工程师,以及想用公开VOC数据集快速验证异物检测方案的算法新人。别急着跑训练,先把标注质量摸一遍,后面能省出几周的排错时间。
2. 先读懂VOC标注:从文件夹结构到XML里的每个字段
2.1 一个VOC工程的标准目录与xml文件长什么样
Pascal VOC格式的标注核心不是图像,而是每个图像对应的XML文件。这个数据集的结构严格按VOC布局组织,拿到手后你会看到JPEGImages和Annotations两个主目录,这很关键——训练时图像和标注是分开存的,靠文件名一一对应。打开任何一个XML,里面记录的远不止一个目标框。
<annotation> <folder>JPEGImages</folder> <filename>img_0031.jpg</filename> <size> <width>1920</width> <height>1080</height> <depth>3</depth> </size> <object> <name>anchor</name> <pose>Unspecified</pose> <truncated>0</truncated> <difficult>0</difficult> <bndbox> <xmin>421</xmin> <ymin>356</ymin> <xmax>587</xmax> <ymax>489</ymax> </bndbox> </object> </annotation>XML里最重要的是两个结构块:根部的<size>和每个目标的<bndbox>。<size>告诉你要处理的图像实际宽高,转换坐标全靠它;<bndbox>里的四个值是像素坐标,左上角(xmin, ymin)到右下角(xmax, ymax),这个框是模型学习的原始依据。如果这个框标偏了、标小了,后面做任何格式转换都救不回来。
是类别名,这个数据集里常见的标签包括anchor、tray、coal、rock之类,具体类别数取决于标注时的定义。有一点要说明:VOC格式本身没有严格的类别清单,全靠标注人员当时怎么命名,同一个异物在不同批次里可能叫anchor也可能叫Anchor,训练时会被当成两类,这是VOC数据集最常见的历史包袱。
2.2 自己检查一遍标注质量:四个必做的脚本化检查
VOC格式看着简单,真要用起来,第一步不是转换,是体检。标注质量决定训练上限,一个越界的框就能让loss曲线呈周期性飙升。我一般会用Python脚本批量检查四个项目:坐标越界、宽高为零、类别名拼写、xml与图像文件对应关系。
import os import xml.etree.ElementTree as ET from PIL import Image ann_dir = 'Annotations' img_dir = 'JPEGImages' issues = [] for xml_name in os.listdir(ann_dir): if not xml_name.endswith('.xml'): continue xml_path = os.path.join(ann_dir, xml_name) tree = ET.parse(xml_path) root = tree.getroot() img_name = root.find('filename').text img_path = os.path.join(img_dir, img_name) if not os.path.exists(img_path): issues.append(f'【缺图像】 {xml_name} -> {img_name}') continue with Image.open(img_path) as im: real_w, real_h = im.size size_tag = root.find('size') xml_w = int(size_tag.find('width').text) xml_h = int(size_tag.find('height').text) if (xml_w, xml_h) != (real_w, real_h): issues.append(f'【尺寸不符】 {xml_name}: xml={xml_w}x{xml_h}, 实际={real_w}x{real_h}') for obj in root.iter('object'): name = obj.find('name').text bndbox = obj.find('bndbox') xmin = int(bndbox.find('xmin').text) ymin = int(bndbox.find('ymin').text) xmax = int(bndbox.find('xmax').text) ymax = int(bndbox.find('ymax').text) if xmin >= xmax or ymin >= ymax: issues.append(f'【宽高异常】 {xml_name}: bbox=({xmin},{ymin},{xmax},{ymax})') if xmin < 0 or ymin < 0 or xmax > real_w or ymax > real_h: issues.append(f'【越界】 {xml_name}: bbox超出图像范围') with open('voc_check_report.txt', 'w') as f: f.write('\n'.join(issues) if issues else 'No issues found.') print(f'检查完成,共发现 {len(issues)} 个问题,报告已保存到 voc_check_report.txt')这段脚本的核心价值在于“对照真实图像尺寸检查XML”,而不是信任XML里写的size。很多人踩坑的原因是标注工具中途改过图像尺寸,或者数据搬运时图像被压缩过,但标注文件还是旧坐标。另外,xmin >= xmax这种判断能把零面积框直接揪出来,这类框训练时会变成NaN损失。跑完脚本后,优先处理“缺图像”和“越界”两类问题,不做的话后面转换的全是脏数据。
3. 把VOC数据集转成YOLO格式:转换脚本与四个边界坑
3.1 转换脚本:从xml到txt的一键脚本
VOC格式虽然标注信息全,但主流的YOLO系模型(yolov5、yolov8)都不直接吃XML,它们需要的是每张图像对应一个txt,每行是一类一框的归一化坐标。所以拿到这个数据集后的第一个实际动作,是写一个批量转换脚本。下面这个脚本是我常用的模板,直接遍历Annotations目录,把每个XML转成一个同名txt放进labels目录。
import os import xml.etree.ElementTree as ET # 定义类别列表。注意:顺序定义了label的id,一旦训练开始就别乱改顺序 class_names = ['anchor', 'tray', 'coal', 'rock', 'wood', 'scraps'] xml_dir = 'Annotations' img_dir = 'JPEGImages' label_dir = 'labels' # 转换后的YOLO标签存放目录 os.makedirs(label_dir, exist_ok=True) def convert_xml_to_yolo(xml_path, img_width, img_height): 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_names: print(f'警告:未知类别 {name} 出现在 {xml_path},已跳过') continue class_id = class_names.index(name) bndbox = obj.find('bndbox') xmin = float(bndbox.find('xmin').text) ymin = float(bndbox.find('ymin').text) xmax = float(bndbox.find('xmax').text) ymax = float(bndbox.find('ymax').text) # 核心归一化公式:中心点坐标 + 宽高,全部除以图像宽高 x_center = (xmin + xmax) / 2.0 / img_width y_center = (ymin + ymax) / 2.0 / img_height box_width = (xmax - xmin) / img_width box_height = (ymax - ymin) / img_height # 防止float计算导致超界,裁剪到[0,1]区间 x_center = min(max(x_center, 0.0), 1.0) y_center = min(max(y_center, 0.0), 1.0) box_width = min(max(box_width, 0.0), 1.0) box_height = min(max(box_height, 0.0), 1.0) lines.append(f'{class_id} {x_center:.6f} {y_center:.6f} {box_width:.6f} {box_height:.6f}') return lines for xml_name in os.listdir(xml_dir): if not xml_name.endswith('.xml'): continue xml_path = os.path.join(xml_dir, xml_name) # 读取对应图像尺寸。这里假设JPEGImages里的图和XML文件名一一对应 img_name = xml_name.replace('.xml', '.jpg') img_path = os.path.join(img_dir, img_name) # 实际项目中,图像尺寸优先从XML的size字段读,但要注意XML可能撒谎 tree = ET.parse(xml_path) root = tree.getroot() img_width = int(root.find('size/width').text) img_height = int(root.find('size/height').text) yolo_lines = convert_xml_to_yolo(xml_path, img_width, img_height) label_path = os.path.join(label_dir, xml_name.replace('.xml', '.txt')) with open(label_path, 'w') as f: f.write('\n'.join(yolo_lines)) print(f'转换完成: {xml_name} -> {os.path.basename(label_path)}')转换脚本的成败在于三处。第一,类别列表的顺序一旦确定就不能改,因为转换后txt里的数字0、1、2代表的就是class_names列表的下标,训练到一半改了顺序,模型输出和标注就彻底错位。第二,宽度和高度用的是float而不是int,这样保证除法结果是浮点数。第三,最后一步的min/max裁剪,能兜住标注框边缘坐标等于图像边界时生成的1.000001这种越界值,YOLO训练对越界标签有宽容度,但最好还是裁剪干净。转换后随手打开一个txt看看,0 0.4235 0.5123 0.1124 0.2331这种格式才是正常的。
3.2 归一化坐标的公式与边界值处理
VOC转换YOLO的坐标换算逻辑,说到底就是“像素坐标除以图像尺寸”。但四个边界值最容易被忽略:图像左边缘xmin=0,转化后x_center可能在0.01左右没问题,但如果异物紧贴右下边缘,公式算出的x_center可能到0.99以上,而box_width加上这个中心点后超过1.0,就形成了实际上的越界框。极端情况下,一个标注框的xmin=1920, xmax=1920,宽度是0,训练时会产生除零或NaN。
还有个隐藏坑是宽高比。如果数据集里的图像不是统一尺寸,有的图是1920x1080,有的是1280x720,转换时按各自图的尺寸归一化,模型训练时会resize到统一尺寸(比如640x640),这会带来宽高比变形。默认yolov8用letterbox方式保持比例,不会拉伸,所以只要归一化坐标正确,不同尺寸的源图是可以混用的。但如果某张图是压缩过的,xml尺寸和实际尺寸不一致,那归一化后的框就会整体偏移,这对小目标检测是致命的,你会在训练时发现loss在下降,但精度永远上不去。
4. 异物检测数据集的五个避坑记录:现象、原因、解决
4.1 类别名大小写不一致,训练时类别爆炸
现象:转换脚本跑完,发现labels目录下的txt里出现了两个锚杆类别,训练时置信度互相打架,mAP一直很低。
原因:VOC标注时部分XML里写的是Anchor,部分写的是anchor,转换脚本把它们当成了两个类。这在多人协作标注时很常见,标注工具自动补全首字母大写,或者不同批次标注员习惯不一样。
解决:转换前先统计所有<name>标签的唯一值,把拼写统一。直接在XML级别改,比转换后再改txt稳妥:用脚本把<name>Anchor</name>替换成<name>anchor</name>再跑转换。这个动作要做在转换脚本之前,否则越早改越好,改了之后夹带原标签的txt全得删掉重来。
4.2 图像旋转标注未同步,GT框全错位
现象:现场拍回来的图有的转了90度存储,图像打开是竖着的,但XML里的坐标还是横图时的坐标。训练出来模型把正常位置的东西全漏检。
原因:手机或防爆相机拍摄时带了EXIF旋转信息,标注工具没读取或者读取逻辑不一致,导致标框坐标对应的是旋转前的图像。这个坑在“从现场拿图”的数据集里出现频率很高。
解决:转换前先把所有图像统一方向并重存,用PIL读图并exif_transpose后保存为统一方向,同时重新生成XML里的size字段。标准流程是“先统一图像,再标/转标注”,但拿到的是别人标好的数据,那就只能写脚本检测:对比xml里size和图像实际宽高,如果宽高互换,大概率是旋转问题。
4.3 异物目标过小,中大尺寸锚框失效
现象:训练后验证集mAP看着有0.7,但实际井下测试时,小锚杆一个都检不出来。
原因:异物在传送带上的占比很小,2345张图里大部分目标在32x32像素以下。yolov8默认的锚框是P3/P4/P5三层,P3层负责小目标,但如果训练时输入resize到640,小目标的信息在多次下采样后几乎丢失。
解决:把训练输入尺寸调大到960甚至1280,并开启多尺度训练。同时检查数据集中小目标的占比,如果超过50%,换用yolov8n的P2层版本,或者改用mmrotate训练dota数据集那套多尺度策略,虽然mmrotate是针对旋转框的,但它的多尺度训练参数对水平小目标同样有效。
4.4 训练集和验证集样本重叠
现象:训练loss正常下降,val loss前期也正常,但到第20个epoch后val mAP突然剧烈抖动,精度不升反降。
原因:划分训练集和验证集时用了随机乱序,但灰尘大的场景里连拍图像高度相似,同一块矸石的不同帧被分到了两边,造成数据泄露。模型记忆的是场景而非异物特征。
解决:按拍摄时间或批次划分,保证同一个时间段拍的图像全进训练集,验证集用另一天的数据。这个数据集有没有提供划分好的train.txt和val.txt,决定了你是否需要自己按文件名规则切。如果文件名带时间戳,直接用时间戳切最稳妥。
4.5 井下光照不均,增强策略把模型带偏
现象:训练时开了随机旋转90度和左右翻转,验证集上效果反而比不开增强还差。
原因:传送带场景里,异物是放在带面上的,有明确的重力方向。旋转90度后异物像是在侧壁上挂着,翻转后传送带运行方向变了,模型学到的是“带面纹理+异物位置”,而不是异物本身的特征。
解决:数据增强只保留轻微的色彩抖动、缩放和灰度噪声,关掉旋转和大角度翻转。煤矿井下图像本身的亮度差异已经够大,不需要再加亮度增强。这个数据集2345张图不算多,但真实场景下宁可少增强也要保住物理合理性。
5. 用YOLOv8训练自己的数据集:从yaml配置到mAP迭代
5.1 编写数据集yaml与训练命令
VOC标注转换到YOLO格式后,训练的第一步是写一个数据集配置文件。yolov8用yaml描述数据路径和类别信息,这个文件写错了,训练会直接报错或类别对不上。我通常把转换好的labels目录和数据拆分成train/val两个子集,然后让yaml指向它。
# dataset.yaml # 训练集和验证集的图像路径,建议写绝对路径,避免相对路径在不同机器上失效 path: /home/engineer/data/conveyor_forgeign_body train: images/train val: images/val # 类别列表,必须和转换脚本里的class_names顺序完全一致 names: 0: anchor 1: tray 2: coal 3: rock 4: wood 5: scraps训练命令我一般用yolov8n起步,先不急着上大模型。传送带异物检测不需要识别几百类,目标少、任务单一,模型参数量不是瓶颈,数据质量和配置才是。
# 先跑30个epoch看数据有没有问题 yolo train data=dataset.yaml model=yolov8n.pt epochs=30 imgsz=1280 batch=8 device=0 # 确认稳定后,再上yolov8s跑完整训练,开启多尺度 yolo train data=dataset.yaml model=yolov8s.pt epochs=120 imgsz=1280 batch=8 device=0imgsz设1280的原因在4.3节里说了——传送带上的异物很多是小目标,640分辨率下十几个像素的锚杆会被下采样吞掉。显存不够就把batch从8降到4,或者先用640跑通验证全流程,再上1280。前30个epoch宁可少跑一点,目的是看loss曲线是否正常下降,以及有没有NaN。如果出现了NaN,不要去调学习率,先回头查标签文件里有没有0宽度的框或者越界坐标。
5.2 每次迭代后要看的指标和要做的调整
训练不是把命令丢进去等结束,每轮迭代都要盯几个关键指标。第一个是train/cls_loss的下降曲线,如果出现台阶式下降或者突然跳高,多半是学习率过高或数据里有脏标签。第二个是val/box_loss,如果验证loss在某个epoch后开始回升,说明过拟合了,需要增强或增加数据。第三个是metrics/mAP50-95,这个指标比mAP50更能反映小目标的检测质量,传送带场景小目标多,mAP50-95和mAP50的差值会比较大,这是正常的,但如果差值超过0.25就需要注意。
还有一个容易被忽视的动作:把验证集的预测结果可视化出来,画框保存成图。不要只盯指标曲线,因为mAP是数字,看不到模型到底把锚杆检成了托盘还是矸石。每轮迭代后抽几张典型的图,看预测框和真实框的位置关系。框偏左半格,说明输入尺寸和锚框比例不匹配;框漏在尾部,说明训练时目标被裁切。这些细节在数字上看不出来,但图上一眼就能定位。
这个阶段也用得上yolov5训练自己的数据集的常见流程,很多在v5上验证过的经验可以直接搬:比如预热训练、余弦退火、以及把置信度阈值设低一点做二次筛选。这2345张图的数据量不够撑起大规模预训练,但足够做迁移学习微调。
6. 收尾技巧:先跑通100张冒烟集,再决定要不要全量投喂
数据科学的翻车往往不在模型,而在“第一次训练就全量灌数据”。我的习惯是用这个2345张的煤矿传送带异物检测数据集时,先随机抽100张图,标注不动,直接转换成YOLO格式训练30个epoch。冒烟测试的意义不是验证精度,而是验证全链路:VOC转换脚本有没有产生空txt、装箱脚本是否能在1280分辨率下正常运行、模型的loss能否正常收敛。这100张图如果能跑通,后面全量数据只是在等时间,而不是在赌配置。
冒烟集跑完后,把模型的预测结果和真实框叠加导出成图片,人眼扫一遍。这个动作只要十分钟,但能直接把4.1到4.5的所有坑暴露出来,因为100张图里只要有1张脏数据,预测图上就能看出来。我踩过的最大一次翻车,就是一个类别的名称里带了空格,从XML一直传到yaml,模型训练了200个epoch后才发现全部检错。玄学一点说,数据集工程就是“先信自己写的脚本,再信模型,最后信数据”,顺序反了,时间就全搭进去了。
全量训练不再赘述,如果冒烟通过,正式训练的配置直接复用即可,只是把epoch拉长到120到150。希望帮到你。
本文还有配套的精品资源,点击获取