简介:这份面向目标检测学习者的公路落石数据集,包含282张JPEG图像,仅标注单一类别“stone”,框总数632个;数据采用Pascal VOC(XML)与YOLO(TXT)两种格式并存,便于直接对比同一图像在两种标注体系下的坐标表达,适合初学者理解格式转换、训练数据组织等环节。压缩包共1019个文件,以283个jpg图像、282个xml标注、284个txt标注为主体,另有169个zbak备份文件与1个zip包,整体大小约13.98MB,目录结构简洁。目前已有47人浏览学习,资源来自网络分享,仅供学习交流,请勿商用。实际使用时,可先用labelImg或脚本抽查标注是否完整,再按训练/验证集比例切分,配合目标检测框架读取双格式数据;由于场景集中在公路落石,也可作为小样本检测、单一类别定位的练习样例,帮助开发者快速跑通数据标注到模型训练的基本流程。
1. 公路落石检测:282 张图、632 个框的小数据集,到底怎么用才不浪费
做目标检测的同学大多经历过这种尴尬:公开数据集一抓一大把,COCO 四百多类、VOC 二十类,但真要落到一个具体场景——比如公路落石检测——你会发现能直接用的数据少得可怜。这份《基于 VOC 与 YOLO 格式的公路落石数据集》,282 张图像、632 个标注框,量不大,但恰好把「真实场景小样本 + 双格式标注」这个组合给凑齐了。它适合两类人:一类是刚开始学目标检测、想走一遍「数据 → 格式转换 → 训练 → 评估」全流程的新手;另一类是已经在做边坡、地质灾害或交通安全相关项目,想快速验证落石检测可行性的工程师。我拆完这套数据后最直观的感受是:它的价值不在「多」,而在「真实」——落石的形态、山体背景、光照干扰都是实打实的,比在 COCO 上刷 mAP 更能暴露问题。
2. 先看懂两种标注格式:VOC 的 XML 与 YOLO 的 TXT 怎么对齐
2.1 VOC 格式的门道:annotation 目录里每个字段都有用
Pascal VOC 格式是目标检测数据集的老祖宗,它的标注信息存放在 XML 文件里,一个图像对应一个同名 XML。这套落石数据集既然标了 VOC 格式,那 annotations 目录下必然是 282 个 XML 文件。打开一个典型的 XML,结构大概是这样的:
<annotation> <folder>images</folder> <filename>rock_001.jpg</filename> <size> <width>1920</width> <height>1080</height> <depth>3</depth> </size> <object> <name>falling_rock</name> <bndbox> <xmin>412</xmin> <ymin>568</ymin> <xmax>534</xmax> <ymax>642</ymax> </bndbox> </object> </annotation>注意几个关键点。filename只写文件名不带路径,所以你的图像目录结构必须和 XML 里的folder字段对得上,否则脚本解析时容易找不到图。size里的宽高必须和原图真实尺寸一致,这个字段在做坐标换算时是分母,一旦写错,后续所有的归一化坐标全部错位。object可以出现多次,表示一张图里有多个落石,每个bndbox给出左上角和右下角的绝对像素坐标。这套数据集里 632 个框对应 282 张图,平均每张图 2.2 个框,说明很多图是单目标或双目标,密集场景不多,这对小样本训练反而是好事——类别相对单纯。
2.2 YOLO 的 class id 与归一化坐标:遗忘中心点计算是最大的翻车点
YOLO 格式和 VOC 最大的区别,是把绝对坐标换成了归一化坐标。每行一个目标,格式是class_id x_center y_center width height,其中 x_center、y_center 是目标中心点坐标除以图像宽高,width、height 是目标框宽高除以图像宽高,全部归一化到 0~1 之间。
从 VOC 转换的公式如下:
# VOC 坐标到 YOLO 归一化坐标的换算 x_center = ((xmin + xmax) / 2) / img_width y_center = ((ymin + ymax) / 2) / img_height box_width = (xmax - xmin) / img_width box_height = (ymax - ymin) / img_height很多第一次转格式的人会犯一个低级错误:直接用xmin / img_width当成中心点。中心点是左右边界相加除二,不是左边界本身。另一个容易翻车的点是:YOLO 的 TXT 里所有值都是浮点数,但如果目标框贴着图像边缘,算出来的宽度可能等于 1.0,某些框架在训练时会对边界坐标做 clamp,而有些老版本不做,导致 loss 异常。这套数据集里如果存在贴边目标,转换脚本里最好主动加一步clip(0, 1),后面第 4 章我给了完整脚本。
2.3 这套数据集的目录组织:直接套用 YOLOv5/v8 的 layout
拿到数据集后,建议按 YOLOv5/v8 的标准目录结构重新归置一遍,不要改原始文件的分布,而是新建一个工作副本:
rockfall_dataset/ ├── images/ │ ├── train/ │ ├── val/ ├── labels/ │ ├── train/ │ ├── val/ ├── data.yaml这样组织最大的好处是省去手动改路径。YOLO 系列的训练入口默认就是按images/train、labels/train这种镜像结构去找图的。注意 images 和 labels 是平级目录,不是 labels 放在 images 里面,这个镜像关系写错,训练时会报「no labels found」类错误。data.yaml里指定train/val路径和类别数量,如果是单类落石,nc: 1,names: ['falling_rock']即可。
3. 632 个标注框的统计思路:先摸清分布再动手训练
3.1 类别口径与数据不平衡:落石不是只有「一块石头」
拿到标注数据第一件事不是急着训练,而是先做统计。落石检测的标注口径在实际项目里往往有两种:一种是只标独立石块,框比较小;另一种是把一小片碎石堆当一个整体框出来,框比较大。这套数据集虽然只有单类,但混合了这两种形态。我一般会用一段脚本先分析框的尺寸分布和每张图的目标数,摸清楚标注者的口径偏好。
import os import numpy as np label_dir = 'labels/train' areas = [] obj_counts = [] for f in os.listdir(label_dir): if not f.endswith('.txt'): continue with open(os.path.join(label_dir, f)) as fp: lines = fp.readlines() obj_counts.append(len(lines)) for line in lines: parts = line.strip().split() w = float(parts[3]) h = float(parts[4]) areas.append(w * h) areas = np.array(areas) print(f'框总数: {len(areas)}') print(f'平均每图目标数: {np.mean(obj_counts):.2f}') print(f'框面积百分位: p25={np.percentile(areas, 25):.4f}, ' f'p50={np.percentile(areas, 50):.4f}, p75={np.percentile(areas, 75):.4f}')归一化面积直接相乘算的是「框占整张图的比例」,不用乘回像素宽高,也能看出框的相对大小。如果 p75 的面积和 p25 差了一个数量级,说明这张数据集里既有远处的碎石点,也有近处的大石块。这种尺度差异,直接影响要不要开 mosaic 增强、anchor 怎么设。对 282 张小数据集来说,我建议先用autoanchor重新聚类 anchor,别沿用 COCO 的默认锚框,原因很简单:落石的宽高比偏方,远没有行人、汽车那种细长框。
3.2 框尺寸分布:小目标占比决定了要不要开 mosaic
把框宽高单独拿出来看,比看面积更有指导意义。落石场景有个典型问题:远处落石在画面里可能只有 20×20 像素,而近处落石能占到 300×300,这种极端尺度分布对检测头非常不友好。小目标占多数时,模型很容易整体偏向学习大目标特征,导致小目标漏检。
我处理这种数据时有个习惯:按「小、中、大」三档做一次统计。具体阈值可以参考 COCO 的定义:小于 32×32 算小目标,32×32 到 96×96 算中目标,大于 96×96 算大目标。
import os from PIL import Image img_dir = 'images/train' label_dir = 'labels/train' for fname in os.listdir(img_dir): if not fname.endswith('.jpg'): continue img = Image.open(os.path.join(img_dir, fname)) W, H = img.size stem = os.path.splitext(fname)[0] lbl_path = os.path.join(label_dir, stem + '.txt') if not os.path.exists(lbl_path): continue with open(lbl_path) as fp: for line in fp: _, cx, cy, w, h = map(float, line.strip().split()) px_w, px_h = w * W, h * H if px_w < 32 and px_h < 32: small += 1 elif px_w < 96 and px_h < 96: medium += 1 else: large += 1如果小目标占比超过 40%,训练时建议把mosaic开到 1.0,同时把img_size适当放大到 960。对小目标来说,输入分辨率提升带来的收益比调参大得多。但注意 282 张图上 mosaic 会引入大量拼接伪影,落石和背景的拼接边界可能带来虚假特征,所以也要配合第 6 章说的温和增强策略。
3.3 图像场景分析:光照、阴影、背景山体对检测的干扰
公路落石数据的难点从来不在目标本身,而在背景。山体岩壁的纹理、阴影、水泥护坡的色块,都和落石长得非常像。我用图像聚类的方式粗筛过这类数据的场景:白天强光、阴天散射光、逆光、有阴影遮挡,每种场景对模型的影响完全不同。
建议逐张过一遍图像,按场景给每张图打个标签,再统计场景分布。如果发现逆光图特别少,而实际部署环境里逆光常见,那就要在增强阶段刻意模拟这种光照。这里的核心矛盾是:逆光落石几乎是纯色剪影,和阴影混在一起,用常规的颜色抖动增强模拟不出这种效果,得用亮度分布调整。
4. 把 VOC 转成 YOLO 并喂进模型:转换脚本与训练配置
4.1 转换脚本:XML 解析、归一化与越界 clamp
这套数据集给了 VOC 和 YOLO 两种格式,理论上可以直接用,但很多情况下你需要自己把 VOC 转成 YOLO——比如拿到的工程版本只保留了 XML,或者你要把类别名改掉。我平时会写一个通用转换脚本,逻辑不复杂,关键是处理好三个边界:目标贴边导致的越界、XML 里出现空bndbox、图像尺寸字段缺失时回退到真实读图。
import os import xml.etree.ElementTree as ET from PIL import Image def voc_to_yolo(xml_path, img_root, out_root, class_names): tree = ET.parse(xml_path) root = tree.getroot() filename = root.findtext('filename') img_path = os.path.join(img_root, filename) img = Image.open(img_path) W, H = img.size stem = os.path.splitext(filename)[0] txt_path = os.path.join(out_root, stem + '.txt') lines = [] for obj in root.findall('object'): name = obj.findtext('name') if name not in class_names: continue cls_id = class_names.index(name) box = obj.find('bndbox') xmin = float(box.findtext('xmin')) ymin = float(box.findtext('ymin')) xmax = float(box.findtext('xmax')) ymax = float(box.findtext('ymax')) # 处理边界:坐标越界统一裁剪到图像范围内 xmin = max(0, min(xmin, W - 1)) xmax = max(0, min(xmax, W - 1)) ymin = max(0, min(ymin, H - 1)) ymax = max(0, min(ymax, H - 1)) if xmax <= xmin or ymax <= ymin: continue cx = ((xmin + xmax) / 2) / W cy = ((ymin + ymax) / 2) / H w = (xmax - xmin) / W h = (ymax - ymin) / H lines.append(f'{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}\n') with open(txt_path, 'w') as fp: fp.writelines(lines) # 调用示例 class_names = ['falling_rock'] voc_to_yolo('annotations/rock_001.xml', 'images', 'labels', class_names)逐个参数说清楚。class_names是类别列表,顺序决定了 id 编号,训练时的data.yaml里names必须和这里完全一致,否则类别错位。max(0, min(xmax, W - 1))是为了处理标注时手滑把框画到图外面的情况——这种情况在真实标注里很常见,比如落石正好在画面边缘。最后一行lines.append里的:.6f是保留 6 位小数,别改成:d,YOLO 坐标全部是浮点。
4.2 划分 train/val:按图像而不是按框,防止数据泄漏
282 张图怎么划分?常见做法是 8:2,即 225 张训练、57 张验证。但有一个坑:划分时必须按图像粒度切,不能按框切。如果同一张图里的一些框进了训练集、另一些框进了验证集,那就是典型的数据泄漏,验证指标会虚高得离谱。更稳妥的方式是按场景聚类后分层抽样——比如先大致按「白天顺光 / 白天逆光 / 阴天」分成三堆,再从每堆里按 8:2 抽。
import os import random from shutil import move random.seed(42) img_dir = 'images/all' label_dir = 'labels/all' train_ratio = 0.8 all_imgs = [f for f in os.listdir(img_dir) if f.endswith('.jpg')] random.shuffle(all_imgs) split_idx = int(len(all_imgs) * train_ratio) train_imgs = all_imgs[:split_idx] val_imgs = all_imgs[split_idx:] for split, imgs in [('train', train_imgs), ('val', val_imgs)]: os.makedirs(f'images/{split}', exist_ok=True) os.makedirs(f'labels/{split}', exist_ok=True) for img_name in imgs: stem = os.path.splitext(img_name)[0] move(f'{img_dir}/{img_name}', f'images/{split}/{img_name}') move(f'{label_dir}/{stem}.txt', f'labels/{split}/{stem}.txt')random.seed(42)保证每次运行划分结果一致,复现性很重要。88 行代码逻辑很简单,但要注意一个细节:只移动图像文件,TXT 文件通过stem找同名文件,避免出现图片和标注错位。另外,划分前先确认每张图都有一一对应的 TXT,如果有孤立图像或孤立标注,先剔出来,不然后续训练会报错。对小数据集来说,验证集只有 57 张图,mAP 的置信区间很宽,所以不要只看一次结果,建议多跑几个随机种子或直接用 K 折交叉验证。
4.3 用 YOLOv8 训练这套数据:yaml 与超参数的几个关键位置
数据准备好之后,训练配置里最核心的是data.yaml。如果用的是 ultralytics 框架(YOLOv5/v8),文件大致长这样:
train: rockfall_dataset/images/train val: rockfall_dataset/images/val nc: 1 names: 0: falling_rock这里有个经常踩的坑:train和val路径写成绝对路径还是相对路径。我的习惯是写相对路径,位置相对于你执行训练命令的工作目录。如果你在项目根目录跑yolo train data=rockfall_dataset/data.yaml,那 yaml 里的路径就得相对项目根目录写。写错了会有两种报错,要么AssertionError: train: ... does not exist,要么训练能启动但 val 的时候找不到图。这两类错误都不难排查,只是浪费时间。
训练命令我用得比较多的是这个:
yolo detect train \ model=yolov8n.pt \ data=rockfall_dataset/data.yaml \ epochs=100 \ imgsz=960 \ batch=8 \ device=0 \ patience=20 \ mosaic=0.8几个参数的解释:imgsz=960对小目标友好,但也要看显存,8G 显存跑 batch=8 可能爆,可以降到 batch=4;patience=20表示 20 个 epoch 内 mAP 没提升就早停,282 张小数据集很容易过拟合,早停能省时间;mosaic=0.8别拉满,落石不是密集小物体数据集,拼接太频繁反而让模型学到假的上下文。另外建议把workers=4加上,数据读取别成为瓶颈。
训练完看指标时,对这类小数据集要重点关注两个点:mAP50和mAP50-95的差值。如果 mAP50 有 0.7 但 mAP50-95 只有 0.3,说明框定位精度不够,落石边界预测得不准。此时优先调box_loss的权重,或者在训练时把iou阈值相关的评估参数看清楚。第 5 章里有一条专门讲这个现象。
5. 避坑与常见问题:落石检测里最容易翻车的五个地方
5.1 现象:明明有落石,模型就是漏检
模型在简单场景上表现还行,一换到远处小落石就漏。我排查时先看漏检目标的框尺寸,发现基本都是小于 32×32 的。原因有两层:一是这张数据集里小目标本来就少,模型没见过足够多样的「小落石」样本;二是imgsz设太低,比如 640 输入时小目标特征图太小,检测头根本感知不到。
解决手段分两步。第一步把imgsz提到 960 甚至 1280,小目标特征从 10×10 像素放大到 15×15 以上,效果立竿见影。第二步是给小目标做针对性增强,比如在 mosaic 之外的copy-paste增强里,把标注框内的落石区域抠出来,缩放到小尺寸后随机贴到背景图上。注意copy-paste需要实例分割的 mask,YOLOv8 里可以直接用标注框抠图,不需要精确 mask。
5.2 现象:把山体纹理识别成落石
误报比漏检更头疼,因为公路场景里误报意味着频繁报警,系统根本不可用。山体岩壁的纹理、节理裂缝、不同颜色的岩层,在特征上和落石非常相近。我看过不少模型的误报框,几乎都集中在纹理变化剧烈的区域。原因是训练数据里「纯落石 + 均匀背景」这种组合占多数,模型没有见过「纹理丰富的背景 + 无落石」的反例。
解决思路有三个。一是引入负样本:刻意加一批完全没有落石的山体背景图,标注文件为空。这在 YOLO 训练里是被允许的,一个 TXT 文件里没有任何行就是负样本。二是调低模型对低置信度框的容忍度,把conf_thres从默认的 0.25 提到 0.45 左右,代价是召回率会掉几个点。三是在数据增强里加大hsv_v(亮度扰动),让模型不依赖单一颜色特征,更多依赖形状和边界梯度。
5.3 现象:训练 loss 很低,验证 mAP 上不去
这是典型的过拟合,小数据集上几乎必发生。282 张图,模型又是 YOLOv8n 这种参数量不大的版本,照理说不容易过拟合,但对象是落石这种尺度变化大的目标,模型的容量还是相对偏大。我遇到的具体情况是:train/box_loss降到 0.02,val/box_loss卡在 0.12 不动,mAP50 在 0.6 附近震荡。
解决办法按优先级排:第一,加增强,特别是scale=0.5配合fliplr=0.5,用小尺度变换模拟远近变化;第二,换更小的模型,yolov8n.pt可以再减通道或直接用yolov8n配合更小的batch;第三,降lr0到 0.005 —— 注意 YOLOv8 默认lr0=0.01,小数据集上这个初始学习率偏高,模型很快记住了训练集。我最终的配置通常是lr0=0.005、mosaic=0.8、degrees=10(限制旋转角度,落石不会大头朝下)。
5.4 现象:txt 坐标出现 0 和 1 以外的数
训练时日志里出现 warning:坐标值超出 [0, 1] 范围。这种情况大多是转换脚本没做轴向裁剪,或者原始 XML 里xmax比xmin小了 1 像素,转出来是负宽度。解决办法就是在转换脚本里做 clamp 后再算宽高,我在 4.1 节的处理方式就是干这个的。另外,label 文件里的行数如果和 XML 里object数量对不上,优先检查是不是有超框目标被continue跳过了。
5.5 现象:类别 id 和 class 文件对不上
训练正常,推理也正常,但可视化检测结果时发现预测框的名字变成了person或car。这个坑几乎都是data.yaml的names顺序和训练时不一致导致的。训练时用的names: {0: falling_rock},推理时加载的权重里记住的是 id 0 对应的类别,如果推理脚本里另写了一份names: {0: rock, 1: falling_rock},id 0 被分配给了 rock,显示自然错。判断依据很朴素:YOLO 权重只存类别 id,不存类别名,任何展示层的 names 都必须和训练时的 data.yaml 严格一致。我现在的习惯是把data.yaml固定放在数据集目录下,推理代码里直接读这份 yaml,不再手动传names。
6. 让 282 张图发挥更大价值:数据增强与半自动标注流程
6.1 针对性增强:对落石这种小目标有效的几个操作
数据量少,最直接的手段就是增强,但增强不是无脑叠加。我实测对落石检测提升最明显的是三组组合:hsv_h=0.01(色相基本不动,落石颜色不能乱变)、hsv_s=0.5(饱和度可降,模拟阴天)、hsv_v=0.4(亮度调低,模拟逆光)。其次是scale=0.5,配合translate=0.1,让落石在画面中的大小和位置更随机,模拟不同拍摄距离。最后是mixup=0.2,这个比例不能高,落石检测和常规目标不同,过度的图像混合会让模型学到石头和路面的假融合特征。
6.2 半自动标注:用第一版模型加人工修正,把数据集滚大
282 张图靠人工标注已经很累了,但为了提升模型效果,更高效的做法是:先用这套数据训一个第一版模型,再拿它去预测一批新的、未标注的公路图像,生成伪标签,人工只修正和删除错误框,修正后的数据再并回训练集。这个过程叫半自动标注,也叫模型辅助标注,做法很简单:把你的新图像丢进yolo predict,生成带框的 TXT,然后一张一张检查,删掉误报框,补上漏检框。
yolo detect predict \ model=runs/detect/train/weights/best.pt \ source=unlabeled_images/ \ save_txt=True \ save_conf=False \ conf=0.35 \ project=auto_label \ name=output注意参数save_txt=True是关键,它会把预测结果直接写成 YOLO 格式的 TXT 文件,存在auto_label/output/labels/目录下。人工修正的思路是:只看置信度偏低的框——因为置信度高的框大概率没错,置信度太低的框基本是误报,真正需要认真看的是中间地带。我会把一批预测结果导出成一个 HTML 页面,图像上叠框,旁边列出坐标和置信度,这样审查效率比逐个打开图片高很多。经过两轮这样的迭代,数据集通常能从 282 张扩到 1000 张以上,模型泛化能力明显上一个台阶,因为不同类型的背景、光照、路况都被补充进来了。
从那以后我每次拿到小规模数据集,都会强制走一遍「统计分布 → 格式核对 → 按场景划分 → 训练基线 → 半自动扩充」这五步,而且会在第一天就把第 2 章的格式对齐和第 5 章的五个坑过一遍,后面基本不会出幺蛾子。这套流程对落石适用,对其他类似的小样本检测场景也同样能参考,希望帮到你。
本文还有配套的精品资源,点击获取