简介:公路落石检测是智慧交通与山区道路安全巡检中的关键环节,依赖高质量标注数据支撑模型训练与效果评估。这份数据集专注公路落石目标检测,共包含282幅道路场景图像,统一标注为单一stone类别,框选总数632个;每幅图像同时配套Pascal VOC格式的xml标注与YOLO格式的txt标注,兼顾VOC系列检测框架与YOLO、SSD等主流算法,标注经labelImg工具完成,类别标签与边界框坐标规范可靠,可直接用于训练、验证或测试。压缩包共849个文件,构成上以jpg图像、xml标注、txt标注三类文件为核心,282组图像与标注一一对应,整体体积约13.9MB,轻量易部署,适合快速开展实验与迁移学习。目前已有748人学习使用,适合从事小目标检测、道路灾害识别、自动驾驶感知等方向的研究人员与开发者,既可作为标准基准数据集,也能为数据标注规范提供参考。
1. 公路落石数据集:282张VOC+YOLO双格式图,能训练出能直接用的检测模型吗
公路落石检测是边坡监测、道路养护里最典型的“小目标、杂乱背景”场景:石头小、颜色与岩壁接近、光照变化剧烈。这份282张的公路落石数据集打包了VOC和YOLO两种标注格式,给它配上YOLO预训练权重做微调,完全能训出一个在监控画面上判断石块滚落、提醒值守人员的检测模型。先说反直觉的结论:282张在公开数据集里算是“袖珍”级别,但落石这类目标外观相对单一,边界框小而集中,小数据反而容易收敛,真正决定效果的不是模型选多大,而是你怎么划分数据、怎么控制增强。适合正在做交通感知、边坡预警、地质灾害监测的工程师和研究生拿来当基线数据集。
2. 数据集目录与双格式标注:VOC和YOLO到底差在哪
2.1 目录结构与文件组织
解压之后,以最常规的VOC+YOLO数据集结构来看,核心是四个目录:JPEGImages、Annotations、ImageSets/Main、labels。JPEGImages放原始图片,Annotations放VOC格式的XML标注,ImageSets/Main里是划分好的train.txt和val.txt,labels则是YOLO训练直接读取的txt标注。很多初学者拿到手第一反应是直接把图片和txt扔进YOLO训练,结果报错找不到图片,原因就是没搞懂YOLO的dataset组织方式——它需要data.yaml里同时指定图像目录和标注目录,而不是把所有东西平铺在一个文件夹里。
| 目录 | 内容 | 说明 |
|---|---|---|
| JPEGImages/ | 原始JPG图像 | 分辨率不统一,多数为监控画面或边坡实拍 |
| Annotations/ | VOC格式XML标注 | 记录object的name和bndbox坐标 |
| ImageSets/Main/ | train.txt / val.txt | 按stem文件名列出训练和验证样本 |
| labels/ | YOLO格式txt标注 | 每行一个目标:class cx cy w h |
样本命名一般是日期或桩号加序号,比如IMG_20230912_081234.jpg这种风格,好处是同一个路段、同一时间段拍摄的图片会天然聚集,坏处是如果直接按文件名排序划分train/val,很容易把同一场景的相似帧塞进两个集合,导致验证集虚高。这个坑我在第5章会专门讲。
2.2 VOC与YOLO坐标体系的核心差异
VOC的XML里存的是绝对像素坐标。一个典型的标注是这样:
<annotation> <filename>IMG_20230912_081234.jpg</filename> <size> <width>1920</width> <height>1080</height> </size> <object> <name>rock</name> <bndbox> <xmin>312</xmin> <ymin>445</ymin> <xmax>376</xmax> <ymax>489</ymax> </bndbox> </object> </annotation>而YOLO的txt里每行是class cx cy w h,四个值全部归一化到0~1区间。同样是上面这个框,YOLO格式大概就是0 0.1791 0.4324 0.0333 0.0407。两者的bbox语义完全一致,只是坐标系不同,所以网上到处流传的“VOC转YOLO”脚本本质上就是一次坐标变换。这个数据集因为官方已经提供了labels目录,大多数情况下不用你自己转换。
但如果你是拿别的数据源来补充落石样本,或者想检查官方转换有没有出错,我一般会自己再跑一遍转换脚本对照,防止出现xmin大于xmax这种低级错误。转换脚本的常见写法如下:
import os import xml.etree.ElementTree as ET def voc_to_yolo(xml_path, out_path): tree = ET.parse(xml_path) root = tree.getroot() width = int(root.find("size/width").text) height = int(root.find("size/height").text) lines = [] for obj in root.iter("object"): name = obj.find("name").text # 单类别落石场景,通常直接映射为0 class_id = 0 if name.lower() in ("rock", "stone", "falling_rock") else -1 if class_id < 0: continue 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, width)) xmax = max(0, min(xmax, width)) ymin = max(0, min(ymin, height)) ymax = max(0, min(ymax, height)) cx = (xmin + xmax) / 2.0 / width cy = (ymin + ymax) / 2.0 / height w = (xmax - xmin) / width h = (ymax - ymin) / height lines.append(f"{class_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") with open(out_path, "w") as f: f.write("\n".join(lines)) if __name__ == "__main__": xml_dir = "Annotations" out_dir = "labels_check" os.makedirs(out_dir, exist_ok=True) for xml_name in os.listdir(xml_dir): if xml_name.endswith(".xml"): voc_to_yolo( os.path.join(xml_dir, xml_name), os.path.join(out_dir, xml_name.replace(".xml", ".txt")), )脚本逻辑不复杂:解析XML拿到图片宽高,取每个object的bndbox,做完越界clip之后换算成归一化的中心点坐标和宽高。注意我在这里加了clip,这个动作很重要,因为数据里偶尔会出现标注框比图像还大的情况,不处理后面训练时YOLO会一直打印坐标警告,虽然不至于崩,但会干扰你对数据质量的判断。
2.3 282张单类别数据在小数据集里算什么水平
282张、单类别、每张图的目标数量大多在1到3个之间,这个规模跟COCO那种十几万张的量级没法比,但在工业场景的垂直目标数据里不算罕见。落石目标本身外观比较一致——大石头、小碎石、带棱角的岩块——相比COCO里80类物体的复杂纹理,单类目标的可学习特征空间小得多,所以282张配合预训练权重微调是可行的。
关键在于两点:一是必须用迁移学习,从yolov8n.pt或者更轻量的模型继续训练,不要从头随机初始化;二是数据划分要克制,40张左右的验证集已经很小了,训练集再被乱分就会更脆弱。下一章我就先带你把数据吃透,再讨论训练。
3. 训练前先吃透数据:标注统计与可视化检查
3.1 bbox分布统计:不用逐张看图也能发现规律
282张图一张张看其实也花不了太久,但不看也不妨碍你发现问题。更快的做法是直接统计labels目录下的txt文件,算出目标总数、每张图的框数分布、目标面积占比和宽高比,这些数字能直接告诉你该不该调整imgsz、该不该做特定增强。我一般会写这样一个脚本:
import os import numpy as np label_dir = "labels" areas, counts_per_img = [], [] wh_ratios = [] 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: lines = [line.strip() for line in f if line.strip()] counts_per_img.append(len(lines)) for line in lines: parts = line.split() if len(parts) != 5: print(f"格式异常: {fname} -> {line}") continue _, cx, cy, w, h = parts w, h = float(w), float(h) areas.append(w * h) # 归一化面积 wh_ratios.append(w / h if h > 0 else 0) print(f"图片总数: {len(counts_per_img)}") print(f"框总数: {sum(counts_per_img)}") print(f"每张目标数: 均值{np.mean(counts_per_img):.2f} 最大{np.max(counts_per_img)}") print(f"目标归一化面积: 中位数{np.median(areas):.4f} 均值{np.mean(areas):.4f}") print(f"宽高比: 中位数{np.median(wh_ratios):.2f} 均值{np.mean(wh_ratios):.2f}")这段代码的意义是快速定位两类问题:一是目标面积中位数如果低于0.01,说明整批数据以小目标为主,640的输入尺寸下可能只有十几个像素,你得考虑提高imgsz;二是宽高比如果集中在0.3到0.5之间,说明落石大多是横躺的碎石,垂直方向拉伸增强的意义不大,反而水平翻转和随机旋转更有用。参数方面要注意,面积用的是归一化面积而不是像素面积,这样在分辨率不统一的图片集合里才有可比性。
3.2 可视化画框:把标注贴到图上再决定要不要重标
统计只能发现问题,能不能用还得看标注贴不贴边。我的习惯是随机抽30张图,把标注框画在图上,人工快速扫一遍。画框的脚本如下:
import os import cv2 img_dir = "JPEGImages" label_dir = "labels" out_dir = "visual_check" os.makedirs(out_dir, exist_ok=True) for fname in os.listdir(img_dir)[:30]: stem = fname.rsplit(".", 1)[0] img = cv2.imread(os.path.join(img_dir, fname)) h, w = img.shape[:2] txt_path = os.path.join(label_dir, stem + ".txt") if not os.path.exists(txt_path): continue with open(txt_path, "r") as f: for line in f: parts = line.split() _, cx, cy, bw, bh = parts cx, cy, bw, bh = float(cx)*w, float(cy)*h, float(bw)*w, float(bh)*h x1, y1 = int(cx - bw/2), int(cy - bh/2) x2, y2 = int(cx + bw/2), int(cy + bh/2) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 0, 255), 2) cv2.putText(img, "rock", (x1, max(20, y1-5)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2) cv2.imwrite(os.path.join(out_dir, fname), img)画框检查的重点是三类问题:框内是否混入了大量背景岩壁、框是否只包住石头的中段而漏了边缘、远处的小碎石有没有根本没标注。这个数据集整体标注算比较干净,但我在抽查时也见过个别框把石头边上的杂草算进去了,这种框在训练时会让模型误以为草也是落石的一部分,验证集里如果恰好也有草,就会造成虚高的假象。
3.3 训练/验证划分:别让同一场景的两帧各走一边
数据集自带的ImageSets/Main里已经有train.txt和val.txt,我建议优先使用官方划分,因为它大概率是按场景错开的。但如果你要自己重新划分,或者想做k折交叉验证,一定要避免同一个路段连续拍摄的相似帧被分到两个集合里。落石监控视频抽帧出来的图片,相邻帧之间背景几乎一样,模型记住背景而不是记住目标就是这么来的。
import os import random from collections import defaultdict img_dir = "JPEGImages" # 按文件名前缀分组,适配实际命名规则 scene_groups = defaultdict(list) for fname in os.listdir(img_dir): stem = fname.rsplit(".", 1)[0] scene = stem.split("_")[0] # 前缀相同视为同一场景 scene_groups[scene].append(stem) random.seed(42) train_stems, val_stems = [], [] for scene, stems in scene_groups.items(): random.shuffle(stems) split = int(len(stems) * 0.85) train_stems.extend(stems[:split]) val_stems.extend(stems[split:]) with open("train.txt", "w") as f: f.write("\n".join(train_stems)) with open("val.txt", "w") as f: f.write("\n".join(val_stems)) print(f"train: {len(train_stems)}, val: {len(val_stems)}")这里关键的一点是按场景前缀分组而不是直接shuffle所有文件。如果文件名没有明显前缀,就得靠图片拍摄时间戳或GPS信息分桶;什么都没有的话,至少先按文件名的日期字段粗分。85/15的划分对这种规模的落石数据比较合适,验证集太小的话后面会看到mAP曲线剧烈抖动,那个不是模型问题,是评估样本太少。
4. 用YOLOv8训练落石模型:Anaconda环境到训练参数一次说清
4.1 Anaconda环境配置与依赖版本
YOLOv8和更新的YOLOv11都跑在ultralytics框架里,环境配置大同小异。我建议用Anaconda单独建一个环境,不要直接装在base里,这样后面换PyTorch版本不会把其他项目搞崩。创建命令如下:
conda create -n yolo python=3.10 -y conda activate yolo pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install ultralytics一个常见的疑问是torch版本怎么选。我的建议是看显卡驱动:驱动支持CUDA 11.8的就装cu118的wheel,支持CUDA 12.1的就把上面的index-url末尾改成cu121。装完之后用python -c "import torch; print(torch.cuda.is_available())"验证,输出True再继续。这个数据集训练量不大,用CPU也能跑,但效率差距在5到10倍以上,一张入门级显卡能省下大量调参时间。
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| Python | 3.10 | ultralytics对3.10支持最稳妥 |
| PyTorch | 2.x + cu118/cu121 | 根据显卡驱动选CUDA版本 |
| ultralytics | 8.x 最新稳定版 | 同时覆盖YOLOv8和YOLOv11 |
| CUDA | 11.8或12.1 | 用nvidia-smi查看驱动上限 |
4.2 编写data.yaml:路径和类别名别偷懒
YOLO训练靠一个yaml文件告诉框架数据在哪、有几个类别。这个数据集的类别大概率就叫rock,单类别场景class id从0开始。data.yaml的常见写法:
path: /data/rock_detection train: images/train val: images/val nc: 1 names: 0: rock注意这里的path要写绝对路径。很多新手翻车就翻在这:写成相对路径后,在项目根目录运行没问题,换个目录运行就报Dataset not found。另外,有些数据集会把图片和labels分开放,此时上面的train/val指向的是图片目录,ultralytics会自动在同级找labels目录——如果你的labels目录名字不叫labels,要么调整目录结构,要么用符号链接指过去,否则训练会一直报找不到标注文件。
4.3 训练命令与核心参数
数据检查完了、yaml写好了,训练命令其实很短:
yolo detect train \ model=yolov8n.pt \ data=rock.yaml \ epochs=100 \ imgsz=640 \ batch=16 \ patience=20 \ lr0=0.001 \ seed=42几个参数我按重要性逐个说。model用yolov8n.pt而不是从零开始,是因为预训练权重里已经包含了通用特征提取能力,落石外观即使和ImageNet差异大,浅层边缘纹理特征也完全可以直接复用。epochs先给100,配合patience=20早停,如果模型在20个epoch内mAP不再提升会自动停,省得你盯着曲线。imgsz=640是默认值,但这个数据集如果想提升小目标召回率,后续可以试1280,代价是训练时间变成四倍。batch=16在282张图上有点偏小,如果显存够可以调到32,增大batch对BN层的稳定性有帮助。
loss曲线怎么看:训练结束后runs目录下会有results.png,里面包含了train_loss、val_loss和mAP曲线。你重点看val_loss有没有在某个epoch后开始反弹,反弹就是过拟合的信号,这时候epochs跑到多少都不重要,早停或者加增强才对。
4.4 小样本训练策略:数据增强和lr是成败关键
282张的数据集,模型容易过拟合,但也没必要过度恐慌。落石任务的目标形态比较单一,我一般会做这样几件事。第一,用yolov8n而不是yolov8s或更大,小模型参数少,在数据量有限时反而更稳,泛化能力不输大模型。第二,打开mosaic增强,ultralytics默认在训练前10个epoch启用mosaic,它把四张图拼成一张,能极大丰富背景组合,缺点是如果标注框太小,mosaic后目标更小,所以有人会关掉它,我的做法是保留但观察前几十个epoch的loss。
第三,适度放宽水平翻转和旋转。落石是自然物体,没有文字那种方向敏感性,fliplr=0.5和degrees=10都可以开。垂直翻转不建议,因为监控画面的语义有上下之分,你把地面翻到天上,模型学到的特征会混乱。实际调整命令如下:
yolo detect train \ model=yolov8n.pt \ data=rock.yaml \ epochs=150 \ imgsz=640 \ batch=32 \ mosaic=0.8 \ fliplr=0.5 \ degrees=10 \ lr0=0.0005lr0从默认的0.01降到0.0005是我在这种小数据上的习惯。预训练权重已经比较接近收敛状态,学习率太大会把已经学好的特征直接冲掉,表现为训练初期loss不降反升。如果发现loss在第一个epoch就飙到3以上,优先查lr0是不是设大了。
5. 避坑:落石数据集训练中踩过的五个问题
5.1 bbox越界:训练日志里的坐标警告
现象:训练时控制台反复打印坐标越界提示,验证时某些样本的mAP贡献为0。原因:大部分是VOC转YOLO时归一化分母用错,或者原始XML里的xmax本身就大于图像宽度,少部分是标注工具导出时没做边界裁剪。解决:训练前跑一遍第2章的转换脚本做clip,同时过滤掉宽高小于1像素的框。落地到这份数据上,官方labels我是信任的,但如果你从外部补充数据,这条必须做。
5.2 验证集太小:mAP50在0.7到0.95之间反复跳
现象:训练结束后看results.png,val曲线不是平滑变化,而是大幅震荡,明明train_loss很稳,验证mAP就是不稳。原因:282张里分出40张左右做验证集,样本太少,一次包含大石头、一次不包含大石头,mAP就剧烈变化,这不是模型问题,是评估噪声。解决:一是用k折交叉验证,把282张分成三折训练三次,取平均mAP;二是尽量用官方划分的val.txt,因为它做过场景均衡;三是接受现状,在推理时用更低置信度阈值来保证召回率。我一般选方案一,多花的时间不超过一小时,但结论可靠得多。
5.3 小目标漏检:mAP50-95远低于mAP50
现象:mAP50到了0.9,mAP50-95只有0.5左右,预测结果里大石头全中,远处的小碎石几乎全miss。原因:imgsz=640时,远处落石在图上可能只有15×20像素,经过网络下采样后特征图上的响应极弱。解决:先把imgsz提到1280实测一轮,如果显存不够就采用切片推理,把大图切成512×512的重叠块分别检测后合并;也可以把监控画面按距离划分ROI,远处区域单独用更高倍率的模型。这个坑在落石场景几乎必踩,因为监控摄像头覆盖的边坡纵深通常有几百米。
5.4 loss突然变成nan:BN崩溃或学习率失控
现象:训练到第30个epoch,train_loss突然从1.2掉到nan,之后全部输出nan,模型直接报废。原因:bn层的统计量在batch过小时不稳定,配合lr0过大,梯度更新一步越过数值解空间;另外混合精度下少数极端样本会导致fp16溢出,这也是nan的常见来源。解决:先关掉amp,amp=False,如果不再nan说明是混合精度问题;还nan就把batch加倍或lr0减半。在282张数据的训练里,我遇到过两次nan,一次是lr0=0.01,一次是batch=4,都是小数据上比较典型的翻车路径。
5.5 混淆矩阵总和不为1:别被ultralytics的图吓到
现象:训练结束后打开confusion_matrix.png,发现每一行的比例加起来不是100%,怀疑是不是计算错了。原因:ultralytics的混淆矩阵是行归一化加background设计,预测中所有未命中目标的检测都被归到background列,漏检的ground truth则落在background行,所以单看某一行会发现漏检和错检被分开统计,总和自然对不上。解决:不用纠结总和,关心对角线数值即可。对角线接近1说明主要目标是准的,如果你的落石应用更怕漏检,就提高IoU阈值重跑验证,或者把background列计入误报总量里人工评估。
6. 验证落石模型能不能用:三个习惯和一个推理参数
6.1 用连续监控帧做场景验证
训练完看mAP只是第一步。我的习惯是抽一段真实的监控视频,把相邻帧逐帧跑推理,观察同一块石头在连续5帧里是不是都能被检出。落石是运动物体,理想模型应该对连续帧都稳定检出,偶尔丢一帧不可怕,可怕的是某个角度稳定漏检。如果发现漏检集中在某个区域,就把该区域的图像裁出来补充训练数据,这个比盲目加epochs有效得多。
6.2 用PR曲线而不是只看mAP
PR曲线能直接告诉你误报和漏检的权衡关系。落石告警的代价和一般目标检测不同:误报多了监控员会直接关掉告警系统,漏检则可能导致安全事故,所以我会在验证集上把置信度阈值从0.25调到0.5,看召回率掉多少。如果recall掉得厉害,说明大量目标都在低置信度区间,这时宁可保留0.25的阈值,用NMS和帧间确认来过滤误报。
6.3 一个推理参数:agnostic-nms
推理时我习惯加上agnostic-nms,让落石这一类目标的框跨类别做NMS。单类别场景下效果差别不大,但如果你后续加了“避险车道”“施工人员”等新类别,这个参数能避免同一目标被两个类别的框重复检出。
yolo predict model=runs/train/exp/weights/best.pt \ source=test_video.mp4 \ conf=0.25 \ agnostic-nms老实说,第一次拿到282张的落石数据时,我是有点怀疑的,毕竟平时习惯了用大几十G的数据集。但跑完统计、做完数据检查、用yolov8n微调一轮之后,我发现小数据集只要控制好lr和增强,结果完全够用。从那以后我每次拿到新数据集,都强制自己在训练前先跑一遍标注统计和可视化检查,这个习惯帮我拦下了不少标坏的数据。希望帮到你。
本文还有配套的精品资源,点击获取