简介:本资源为挖掘机目标检测专用数据集,包含1631张清晰实拍图片,同时提供VOC与YOLO两种主流标注格式,可直接用于训练YOLO系列、Faster R-CNN、SSD等目标检测模型,适用于智慧工地监控、施工车辆识别、工程进度自动化分析等场景,也适合目标检测入门与进阶开发者作为规范训练数据。压缩包内共有2000个文件,JPEGImages文件夹存放jpg原图,Annotations文件夹存放xml标注,labels文件夹存放txt标签,三者一一对应,整体压缩包约102.66MB。数据集仅含Excavator一个类别,共标注矩形框2036个,标注信息准确、框型统一,且未经图像增强,能有效降低数据清洗成本。目前已有207人学习下载,这份即下即用的基础资源可帮助使用者跳过繁琐采集与标注环节,快速进入模型训练与效果验证阶段。
1. 挖掘机检测数据集:1631张图、2036个框,拿到手就能开始训
拿到这份挖掘机数据集,第一反应是它能不能直接进yolo训练管线。解压之后看到JPEGImages、Annotations、labels三个文件夹都是1631个文件,jpg图片配VOC的xml同时配YOLO的txt,标签只有一类Excavator,框数2036个,平均每张图有1.25个目标要检测。同一份数据给了两种主流标注格式,省掉了用脚本从VOC转YOLO这一步,这是它最实在的地方。适合正在做工程车辆识别、工地监控统计这类项目,或者刚想用yolov8训练自己数据集但手里还没有干净标注的人。需要提醒的是它未做增强,场景集中在常见的工地和土方作业视角,迁移到有护栏、夜间光照等陌生环境时要自己补数据。
2. 拆解数据结构:VOC与YOLO两种标注格式怎么在一个包内共存
拿到任何数据集,我习惯先看目录,再抽查两三个文件。格式对不对、路径能不能对上,检查这一步跑通了,后面训练才稳。这一章把三个文件夹的关系说清楚,同时把两种标注格式的字段差异对比明白。
2.1 JPEGImages、Annotations、labels三个文件夹分别管什么
先用一段代码把三个目录扫一遍,确认文件数量:
import os root = "挖掘机数据集" for folder in ["JPEGImages", "Annotations", "labels"]: path = os.path.join(root, folder) files = [f for f in os.listdir(path) if not f.startswith(".")] print(folder, len(files))这段代码的作用很简单:遍历根目录下三个文件夹,统计非隐藏文件的数量。正常输出是三行都是1631,图片、VOC标注、YOLO标注完全对齐。如果数量不一样,说明某个环节丢文件了,先去解决对应关系,别急着训。
从抽样文件名看,图片叫sl_images1773.jpg这种编号格式,对应的xml和txt只是后缀不同,前缀保持一致,这样在后面写匹配脚本时可以用同一套前缀去三个目录里找文件,省很多事。我见过有人把txt改成jpeg_images_1773.txt这种带前缀的命名,训练时找不到标签,排查起来非常麻烦。这份数据集的命名风格是对齐的,属于比较省心的那一类。
2.2 打开一个XML和一个TXT,看VOC和YOLO字段差在哪
VOC格式的标注长这样:
<annotation> <filename>sl_images1773.jpg</filename> <size> <width>1920</width> <height>1080</height> </size> <object> <name>Excavator</name> <bndbox> <xmin>101</xmin> <ymin>202</ymin> <xmax>644</xmax> <ymax>430</ymax> </bndbox> </object> </annotation>这个结构里最重要的一对字段是<name>和<bndbox>,name是类别名Excavator,bndbox里存的是绝对像素坐标。xmin、ymin是框的左上角,xmax、ymax是右下角,单位就是图片的像素点,不需要额外换算。需要注意<size>里的width和height要和真实图片一致,如果xml里写的是1920×1080而实际图片是1280×720,后面转格式时坐标会整体偏移。
对应这份xml的YOLO标签文件里只有一行数字:
0 0.194 0.293 0.283 0.211五个数字分别是类别ID、中心点x、中心点y、框宽、框高,后四个值全部除以图片宽高做了归一化。拿前面的VOC坐标验证一下:中心点x = (101 + 644) / 2 / 1920 ≈ 0.194,中心点y = (202 + 430) / 2 / 1080 ≈ 0.293,框宽 = (644 - 101) / 1920 ≈ 0.283,框高 = (430 - 202) / 1080 ≈ 0.211,和txt里的数字能对上。这说明xml和txt是同一套标注导出的,没有转格式时弄乱。
两种格式的差异可以这样理解:
| 对比项 | VOC(Annotations) | YOLO(labels) |
|---|---|---|
| 存储位置 | 每个jpg对应一个xml | 每个jpg对应一个txt |
| 类别 | 字符串Excavator | 数字0 |
| 坐标 | xmin, ymin, xmax, ymax绝对像素 | 归一化中心点+宽高,范围约0~1 |
| 易读性 | 直观,文本编辑器可读 | 紧凑,模型直接读取 |
| 主要用途 | LabelImg等标注工具产出 | 训练和推理时用 |
留两种格式的意义在于:老项目维护标注习惯用VOC,yolo训练只认txt,两份都留着可以随时回溯。拿到数据后如果不想自己写转换脚本,直接用labels目录里的txt作为训练输入即可。实际训练时yolo只会读labels,xml可以视为存档,方便以后做格式迁移时恢复原始标注。
3. 训练前全量校验:把1631张图与标注逐对核对、画框、排错
我养成了一个比较“强迫症”的习惯:新到一个数据集,先做三项体检,全过了才进训练。因为yolo训练起步是几小时级别的任务,数据有问题时跑完才发现,浪费的时间比检查多得多。下面这三节就是完整的校验流程,每一步都有对应脚本。
3.1 三目录文件名对应性检查
第一步是确认JPEGImages、Annotations、labels三个文件夹里的文件前缀是不是一一对应:
import os root = "挖掘机数据集" sets = {} for folder in ["JPEGImages", "Annotations", "labels"]: files = os.listdir(os.path.join(root, folder)) sets[folder] = {os.path.splitext(f)[0] for f in files} img = sets["JPEGImages"] xml = sets["Annotations"] txt = sets["labels"] only_img = img - xml - txt only_xml = xml - img - txt only_txt = txt - img - xml print("三方都对得上的数量:", len(img & xml & txt)) print("只有图片没有标注:", len(only_img)) print("只有xml没有图片:", len(only_xml)) print("只有txt没有图片:", len(only_txt))逻辑说明:os.path.splitext把文件名按后缀拆开,取前缀加入集合;三个集合做交并差运算。正常结果应该是三方都对得上的数量是1631,另外三个差集都是0。如果出现只有图片没有标注的情况,多半是下载不全或者标注文件命名不一致,这时候直接找出具体哪些文件,而不是重新打包整个数据集。
这段代码跑完后,还有一个容易忽略的地方:检查有没有大小写后缀混用的情况,比如sl_images1773.JPG和sl_images1773.jpg同时存在,Windows下不一定报错,但在Linux服务器上会直接触发FileNotFoundError。用下面一行代码扫一遍:
exts = os.listdir(os.path.join(root, "JPEGImages")) print(set(os.path.splitext(f)[1].lower() for f in exts))正常输出应该只有{'.jpg'}。如果出现.jpg和.JPG并存,批量重命名为小写后缀再继续。命名统一这种事在单机上看不出来,一出问题就是连锁反应,值得提前处理。
3.2 越界坐标、异常尺寸和空标签检查
YOLO格式的归一化坐标有个隐性问题:看起来都在0到1之间,但0和1本身往往是越界信号。x_center取到1.0说明这个框的中心点落在图片右边缘上,实际物体不可能正好从边缘开始。写一个全量扫描把异常行筛出来:
import os root = "挖掘机数据集" bad_lines = [] empty_txt = [] for txt_name in os.listdir(os.path.join(root, "labels")): path = os.path.join(root, "labels", txt_name) lines = open(path, encoding="utf-8").read().strip().splitlines() if not lines: empty_txt.append(txt_name) continue for line in lines: parts = line.split() if len(parts) != 5: bad_lines.append((txt_name, line, "字段数不为5")) continue cid, cx, cy, w, h = map(float, parts) if cid != 0: bad_lines.append((txt_name, line, "类别ID不是0")) if not (0 < w <= 1 and 0 < h <= 1): bad_lines.append((txt_name, line, "宽高越界")) if not (0 <= cx <= 1 and 0 <= cy <= 1): bad_lines.append((txt_name, line, "中心点越界")) if w * 1920 < 3 or h * 1080 < 3: bad_lines.append((txt_name, line, "框像素尺寸过小")) print("空标签文件数:", len(empty_txt)) print("异常行数:", len(bad_lines)) for item in bad_lines[:20]: print(item)说明:这段脚本做了四类检查——字段数是否为5、类别ID是否为0、坐标是否在0到1之间、换算成像素后框面积是否太小。最后一项的阈值是基于常见1920×1080图片写的,如果你的图片分辨率不同可以改。异常结果不必立即丢弃,先看是普遍偏差还是个别脏数据。空标签文件也要单独统计,yolo训练时遇到完全空的txt只会打一条warning,但累积多了会影响loss统计。
检查越界坐标时,最常出现的现象是xml里xmax比图片width大,转换后w就大于1。出现这种情况的原因通常是标注人员在接近图片边缘的目标上把框拖出去了一点,脚本转格式时不校验直接除以width,结果就是w=1.01这种数值。这个坑在后续画框时才会暴露,所以越界检查要在画框之前做。
3.3 抽样20张图画框,人工看一眼标注质量
格式检查通过后,画框是最后一道保险。规则是画框,但本质是让人眼把标注和真实目标的关系复核一遍:
import cv2, os, random root = "挖掘机数据集" random.seed(0) samples = random.sample(os.listdir(os.path.join(root, "JPEGImages")), 20) for name in samples: img = cv2.imread(os.path.join(root, "JPEGImages", name)) h, w = img.shape[:2] txt_path = os.path.join(root, "labels", os.path.splitext(name)[0] + ".txt") for line in open(txt_path): cid, cx, cy, bw, bh = map(float, line.split()) x1 = int((cx - bw / 2) * w) y1 = int((cy - bh / 2) * h) x2 = int((cx + bw / 2) * w) y2 = int((cy + bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, "Excavator", (x1, max(0, y1 - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) out = f"check_{os.path.splitext(name)[0]}.jpg" cv2.imwrite(out, img) print("已输出:", out)逻辑说明:先读图片拿真实高宽,再从txt解析归一化坐标,乘回像素值画矩形。坐标换算后要用int()截断成整数,否则cv2.rectangle会报类型错误。给自己3分钟翻一遍20张图,重点看两点:框是否完全包裹目标,有没有把背景或挖掘机旁边的人框进来。如果有明显错框,说明这批标注可能有系统误差,需要抽更多图复核。
提示:随机种子设置为0,是为了保证每次抽到的样本一样。如果偶然抽到20张都质量很好,建议再换一次种子抽第二批,避免样本代表性不足。
4. 用YOLOv8训练自己的数据集:data.yaml配置、训练参数与结果判读
格式和标注都验证过,接下来就是把它喂给模型。这一章直接从拆分流、写配置、跑训练、看结果四个环节往下走,每个环节都有可以直接照抄的步骤。
4.1 按8:1:1拆分训练验证集,并把data.yaml写好
先说清楚一点:不要把train和val都指向同一个文件夹。很多人拿到数据先直接开训,val和train是同一批图,训练过程的mAP会虚高,部署到现场就露馅。先把图片按比例拆成训练、验证两个子集:
import os, random, shutil root = "挖掘机数据集" img_dir = os.path.join(root, "JPEGImages") lbl_dir = os.path.join(root, "labels") imgs = os.listdir(img_dir) random.seed(42) random.shuffle(imgs) train_imgs = imgs[: int(len(imgs) * 0.8)] val_imgs = imgs[int(len(imgs) * 0.8):] for split, split_imgs in [("train", train_imgs), ("val", val_imgs)]: os.makedirs(os.path.join(img_dir, split), exist_ok=True) os.makedirs(os.path.join(lbl_dir, split), exist_ok=True) for name in split_imgs: prefix = os.path.splitext(name)[0] shutil.move(os.path.join(img_dir, name), os.path.join(img_dir, split, name)) shutil.move(os.path.join(lbl_dir, prefix + ".txt"), os.path.join(lbl_dir, split, prefix + ".txt")) print("train:", len(train_imgs), "val:", len(val_imgs))逻辑说明:随机打乱后按比例切片,前80%进train,后20%进val,jpg和txt用同一个prefix一起移动。移动后目录结构变成JPEGImages/train、JPEGImages/val、labels/train、labels/val。Annotations里的xml不需要动,后续如果还想做别的格式转换,xml仍然对应原始全量图片。这里用shutil.move而不是copy,是为了避免重复文件占双倍空间。
然后写data.yaml:
train: 挖掘机数据集/JPEGImages/train val: 挖掘机数据集/JPEGImages/val nc: 1 names: 0: Excavatortrain和val指向图片目录,yolov8会自动在同级找labels目录下的同名txt。这里的“同级找”是指labels和JPEGImages属于同一个根目录,且子目录名一致,即JPEGImages/train对应labels/train。千万不要写成data/train这种自造的路径,除非你把文件夹真的放到了那里。如果data.yaml和数据集不在同一个工作目录,train路径建议写绝对路径,少一层拼路径的麻烦。
4.2 训练命令与参数选型
准备工作完成后,训练命令一行就够了:
yolo detect train data=挖掘机数据集/data.yaml \ model=yolov8n.pt \ epochs=100 imgsz=640 \ batch=8 workers=4 device=0参数怎么选,逐项说一下:model=yolov8n.pt会优先加载预训练权重,第一次运行会自动下载。yolo预训练模型下载在国内网络环境可能比较慢,建议先手动把yolov8n.pt放到当前目录,训练时直接复用,避免训练卡在下载阶段。epochs=100对单类数据通常是够的,可以边训边看curve,如果第50轮已经平稳,没必要硬跑满。imgsz=640兼顾速度和内存;如果挖掘机在画面里占比很小,可以考虑imgsz=1024,显存占用大概要增加一倍。batch=8是大多数8GB显存起步的保守值,显存吃紧就降到4。workers=4在Windows下建议改成0或2,避免多进程读取数据时崩掉。device=0指定第一块GPU,没有GPU就删掉这个参数改用CPU,但速度会慢很多。
关于损失函数,yolo训练曲线里会看到box_loss、cls_loss、dfl_loss三条都分train和val。单类数据的cls_loss很简单,主要观察box_loss和dfl_loss是否稳定下降。val曲线偶尔上下波动是正常的,一直上升才说明过拟合。
4.3 训练结束后先看这四个文件
训练结束输出在runs/detect/train目录下,最快定位问题的四个文件:
| 文件 | 看什么 |
|---|---|
| results.png | 损失与mAP曲线,判断收敛情况 |
| confusion_matrix_normalized.png | 混淆矩阵,看有没有把背景大量误判成Excavator |
| val_batch0_pred.jpg | 验证集抽样预测图,直接看框的好坏 |
| weights/best.pt | 保存最优权重,后面推理用 |
results.png里如果mAP50最后超过了0.9,说明这个数据集的标注质量是过硬的,可以直接进业务流程。如果卡在0.6~0.7,先别急着调模型,回到第3章的画框结果看看是不是存在系统性的漏框或错框。看过混淆矩阵的都知道,归一化后行列总和不可能严格等于1,这是正常现象,重点关注的是背景列有没有意外的高值。
关于下载页面“不对训练精度作保证”的声明,我的理解是:这份资源能保证的是框和类别标得准确,而不保证任何模型拿过去一定达到多少精度。所以自己跑出最优模型时,用同一份数据多跑两遍,把随机种子固定,再谈精度。
5. 避坑指南:训练前后最容易翻车的四个真实现场
5.1 文件对不齐:训练启动就FileNotFoundError
现象:yolo detect train一执行,日志刷了一屏后报错,提示找不到某个jpg或txt文件,但目录里明明有。
原因:多半是data.yaml里的路径与真实目录不一致,或者图片后缀大小写不统一。另一种常见情况是Windows下跑通的相对路径,切换到Linux服务器后相对路径基准变了,直接失效。
解决:先把data.yaml的train和val改成从项目根目录出发的绝对路径,或者把路径写死在python脚本里统一处理。命名时强制使用小写后缀,在Linux下用find命令批量处理:
find 挖掘机数据集 -name "*.JPG" -exec rename 's/\.JPG$/.jpg/' {} \;改名之后重新跑一遍第3章的对应性检查,确认没有因为改名产生新缺口。文件对不上看起来是小问题,但它是训练启动失败里出现频率最高的一种,值得最先排查。
5.2 单类数据集反而出现2个类别
现象:训练开始时的nc明明写的1,训练过程中却打印出2个类别,混淆矩阵里多出一行,或者验证集预测框里出现奇怪的类别标签。
原因:txt文件里混入了类别ID不为0的行,比如某一行写成了1 0.5 0.5...;或者是标签文件里的空行和空格符在读取时被解析成异常数据,yolo内部把它当成了别的东西。
解决:跑一遍全量统计,把每个标签文件第一列都收起来:
from collections import Counter import os root = "挖掘机数据集" counter = Counter() for txt_name in os.listdir(os.path.join(root, "labels")): path = os.path.join(root, "labels", txt_name) for line in open(path, encoding="utf-8"): line = line.strip() if not line: continue counter[int(line.split()[0])] += 1 print(counter)正常输出只有{0: 2036}。如果有1甚至更大的数字,按文件名定位到具体图片,重新看VOC标注确定到底是不是标注错误,还是转换脚本里把类别映射写错了。定位到文件后,直接修正txt里的数字,不要在整个数据集上做全局替换,否则可能把正常标签也改了。
5.3 画框时坐标越界和负值框
现象:第3章的画框脚本跑完后,翻开图片能看到某些绿框被切掉一部分,或者框完全不见,控制台输出坐标出现负值。
原因:xml转换txt时用width和height做除法,标注人员拖框时鼠标超出图片边缘,xmax比width大,除出来w就超过1。还有一种是标注界面支持自动吸附边缘,xmin被吸附到-1,转换后cx就变成负数。
解决:在训练前统一处理,把越界坐标clip回图片边界。用yolo格式做clip时要注意,先换算回像素坐标再裁剪,不能直接对归一化坐标做max(0, x - 0.01)这类操作,会把宽度也改掉。稳妥的写法是:
x1 = max(0, min(w - 1, int((cx - bw / 2) * w))) y1 = max(0, min(h - 1, int((cy - bh / 2) * h))) x2 = max(0, min(w - 1, int((cx + bw / 2) * w))) y2 = max(0, min(h - 1, int((cy + bh / 2) * h)))clip之后如果新的w、h小于3像素,说明这个框本身质量太差,建议删掉这一行标签而不是硬留着。越界框大多发生在图片边缘,这种位置目标本来就不完整,训练时对模型的贡献也有限。
5.4 数据增强开太猛,mAP反而不升
现象:开始训练时心想单类数据太少,把翻转、马赛克、HSV全开,结果mAP比不开增强时低了几个点,验证集预测框反而变得零散。
原因:这张数据集的背景比较统一,挖掘机占画面主体,马赛克拼接会把一台完整的挖掘机切成四块,模型学到的特征碎片化。翻转角度大时,斗臂的左右位置变化太大,模型还没收敛就被增强干扰了。
解决:分两阶段来。先关掉大部分增强,把baseline跑稳:
yolo detect train data=挖掘机数据集/data.yaml \ model=yolov8n.pt epochs=100 imgsz=640 batch=8 \ hsv_h=0.015 hsv_s=0.5 hsv_v=0.4 \ flipud=0.0 fliplr=0.5 mosaic=0.5参数说明:hsv_h、hsv_s、hsv_v控制色相、饱和度、亮度扰动,0.5左右足够模拟不同光照;fliplr=0.5表示一半概率水平翻转,对挖掘机这种左右对称的目标比较安全;flipud=0.0不打开垂直翻转,因为工地视角里挖掘机很少倒置。等这个配置跑完再逐渐提高mosaic权重,每调一档就对比一次results.png,不要一次全开。
6. 把这份数据用到更顺手:数据增强、批量验证与模型产出
6.1 自己写一个水平翻转增强,练手坐标变换
yolo自带增强是训练时在线做的,不会落盘。如果想要一份物理上多一倍的数据,用脚本做离线翻转更直接:
import cv2, os root = "挖掘机数据集" img_name = "sl_images1773.jpg" prefix = os.path.splitext(img_name)[0] img = cv2.imread(os.path.join(root, "JPEGImages", img_name)) flipped = cv2.flip(img, 1) cv2.imwrite(os.path.join(root, "JPEGImages", prefix + "_flip.jpg"), flipped) new_lines = [] for line in open(os.path.join(root, "labels", prefix + ".txt")): cid, cx, cy, bw, bh = map(float, line.split()) new_lines.append(f"{int(cid)} {1 - cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}\n") with open(os.path.join(root, "labels", prefix + "_flip.txt"), "w") as fp: fp.writelines(new_lines) print("生成翻转样本:", prefix + "_flip.jpg")坐标变换规则只有一条:水平翻转后中心点x变成1 - cx,y、宽、高都不变。width不变是因为对称翻转不改变目标宽度,height同样不变。用这条规则把整个数据集翻一遍后,验证集也要同步处理,否则训练和验证的分布又不一样了。离线增强生成的文件要和原文件区分开,建议统一加_flip后缀,后续不想用时直接按后缀删掉即可。
6.2 用best.pt对现场视频做批量验证
训练只是前半场,真实工地才能检验模型有没有泛化能力。找一段没有参与训练的视频,直接跑批量推理:
yolo predict model=runs/detect/train/weights/best.pt \ source=site_video.mp4 conf=0.25 iou=0.45 \ save=True show=Trueconf=0.25表示低于25%置信度的框直接丢弃,工地背景复杂时建议调到0.35。iou=0.45是NMS阈值,同类目标靠得近时调低到0.4能让重叠框少一些。save=True把结果视频存到runs/detect/predict目录,show=True在窗口实时预览,杀后台训练时记得关掉show避免无法显示。跑完一段视频后,挑几帧暂停看漏检目标,这也是评估数据是否够用的最直接方式。
6.3 导出ONNX,并把自己的使用习惯沉淀成清单
部署环节如果不想依赖python环境,把权重导成ONNX方便许多:
yolo export model=runs/detect/train/weights/best.pt format=onnx imgsz=640导出的best.onnx可以用onnxruntime在C++或Java服务里直接调用,推理速度比原版快不少。导完后建议顺手把三样东西记在一块:一是第3章的校验脚本,二是train设置好的参数,三是best.pt对应的data.yaml路径,方便别人接手时直接复现。这三样合起来才是这套数据能复用的核心。
从那以后我训练前都强制走一遍第3章的全量校验,画框确认之后才谈跑模型,这个习惯帮我避掉了不少数据问题,希望帮到你。
本文还有配套的精品资源,点击获取