ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

公路落石检测数据集:282张VOC+YOLO双格式,YOLOv8小目标训练全解析

公路落石检测数据集:282张VOC+YOLO双格式,YOLOv8小目标训练全解析 简介公路落石检测是智慧交通与山区道路安全巡检中的关键环节依赖高质量标注数据支撑模型训练与效果评估。这份数据集专注公路落石目标检测共包含282幅道路场景图像统一标注为单一stone类别框选总数632个每幅图像同时配套Pascal VOC格式的xml标注与YOLO格式的txt标注兼顾VOC系列检测框架与YOLO、SSD等主流算法标注经labelImg工具完成类别标签与边界框坐标规范可靠可直接用于训练、验证或测试。压缩包共849个文件构成上以jpg图像、xml标注、txt标注三类文件为核心282组图像与标注一一对应整体体积约13.9MB轻量易部署适合快速开展实验与迁移学习。目前已有748人学习使用适合从事小目标检测、道路灾害识别、自动驾驶感知等方向的研究人员与开发者既可作为标准基准数据集也能为数据标注规范提供参考。1. 公路落石数据集282张VOCYOLO双格式图能训练出能直接用的检测模型吗公路落石检测是边坡监测、道路养护里最典型的“小目标、杂乱背景”场景石头小、颜色与岩壁接近、光照变化剧烈。这份282张的公路落石数据集打包了VOC和YOLO两种标注格式给它配上YOLO预训练权重做微调完全能训出一个在监控画面上判断石块滚落、提醒值守人员的检测模型。先说反直觉的结论282张在公开数据集里算是“袖珍”级别但落石这类目标外观相对单一边界框小而集中小数据反而容易收敛真正决定效果的不是模型选多大而是你怎么划分数据、怎么控制增强。适合正在做交通感知、边坡预警、地质灾害监测的工程师和研究生拿来当基线数据集。2. 数据集目录与双格式标注VOC和YOLO到底差在哪2.1 目录结构与文件组织解压之后以最常规的VOCYOLO数据集结构来看核心是四个目录JPEGImages、Annotations、ImageSets/Main、labels。JPEGImages放原始图片Annotations放VOC格式的XML标注ImageSets/Main里是划分好的train.txt和val.txtlabels则是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 filenameIMG_20230912_081234.jpg/filename size width1920/width height1080/height /size object namerock/name bndbox xmin312/xmin ymin445/ymin xmax376/xmax ymax489/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_okTrue) 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_okTrue) 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(ftrain: {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 python3.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倍以上一张入门级显卡能省下大量调参时间。组件推荐版本说明Python3.10ultralytics对3.10支持最稳妥PyTorch2.x cu118/cu121根据显卡驱动选CUDA版本ultralytics8.x 最新稳定版同时覆盖YOLOv8和YOLOv11CUDA11.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 \ modelyolov8n.pt \ datarock.yaml \ epochs100 \ imgsz640 \ batch16 \ patience20 \ lr00.001 \ seed42几个参数我按重要性逐个说。model用yolov8n.pt而不是从零开始是因为预训练权重里已经包含了通用特征提取能力落石外观即使和ImageNet差异大浅层边缘纹理特征也完全可以直接复用。epochs先给100配合patience20早停如果模型在20个epoch内mAP不再提升会自动停省得你盯着曲线。imgsz640是默认值但这个数据集如果想提升小目标召回率后续可以试1280代价是训练时间变成四倍。batch16在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。第三适度放宽水平翻转和旋转。落石是自然物体没有文字那种方向敏感性fliplr0.5和degrees10都可以开。垂直翻转不建议因为监控画面的语义有上下之分你把地面翻到天上模型学到的特征会混乱。实际调整命令如下yolo detect train \ modelyolov8n.pt \ datarock.yaml \ epochs150 \ imgsz640 \ batch32 \ mosaic0.8 \ fliplr0.5 \ degrees10 \ lr00.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.pngval曲线不是平滑变化而是大幅震荡明明train_loss很稳验证mAP就是不稳。原因282张里分出40张左右做验证集样本太少一次包含大石头、一次不包含大石头mAP就剧烈变化这不是模型问题是评估噪声。解决一是用k折交叉验证把282张分成三折训练三次取平均mAP二是尽量用官方划分的val.txt因为它做过场景均衡三是接受现状在推理时用更低置信度阈值来保证召回率。我一般选方案一多花的时间不超过一小时但结论可靠得多。5.3 小目标漏检mAP50-95远低于mAP50现象mAP50到了0.9mAP50-95只有0.5左右预测结果里大石头全中远处的小碎石几乎全miss。原因imgsz640时远处落石在图上可能只有15×20像素经过网络下采样后特征图上的响应极弱。解决先把imgsz提到1280实测一轮如果显存不够就采用切片推理把大图切成512×512的重叠块分别检测后合并也可以把监控画面按距离划分ROI远处区域单独用更高倍率的模型。这个坑在落石场景几乎必踩因为监控摄像头覆盖的边坡纵深通常有几百米。5.4 loss突然变成nanBN崩溃或学习率失控现象训练到第30个epochtrain_loss突然从1.2掉到nan之后全部输出nan模型直接报废。原因bn层的统计量在batch过小时不稳定配合lr0过大梯度更新一步越过数值解空间另外混合精度下少数极端样本会导致fp16溢出这也是nan的常见来源。解决先关掉ampampFalse如果不再nan说明是混合精度问题还nan就把batch加倍或lr0减半。在282张数据的训练里我遇到过两次nan一次是lr00.01一次是batch4都是小数据上比较典型的翻车路径。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曲线而不是只看mAPPR曲线能直接告诉你误报和漏检的权衡关系。落石告警的代价和一般目标检测不同误报多了监控员会直接关掉告警系统漏检则可能导致安全事故所以我会在验证集上把置信度阈值从0.25调到0.5看召回率掉多少。如果recall掉得厉害说明大量目标都在低置信度区间这时宁可保留0.25的阈值用NMS和帧间确认来过滤误报。6.3 一个推理参数agnostic-nms推理时我习惯加上agnostic-nms让落石这一类目标的框跨类别做NMS。单类别场景下效果差别不大但如果你后续加了“避险车道”“施工人员”等新类别这个参数能避免同一目标被两个类别的框重复检出。yolo predict modelruns/train/exp/weights/best.pt \ sourcetest_video.mp4 \ conf0.25 \ agnostic-nms老实说第一次拿到282张的落石数据时我是有点怀疑的毕竟平时习惯了用大几十G的数据集。但跑完统计、做完数据检查、用yolov8n微调一轮之后我发现小数据集只要控制好lr和增强结果完全够用。从那以后我每次拿到新数据集都强制自己在训练前先跑一遍标注统计和可视化检查这个习惯帮我拦下了不少标坏的数据。希望帮到你。本文还有配套的精品资源点击获取
返回列表