ARTICLE DETAIL

资讯详情

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

公路落石与滑坡检测实战:VOC数据集转换与YOLOv8训练

公路落石与滑坡检测实战:VOC数据集转换与YOLOv8训练 简介面向目标检测算法训练与评估提供公路落石和滑坡场景的标注数据适用于道路巡检、边坡监测等视觉任务可解决灾害检测样本不足的问题。压缩包共1984个文件含991张JPG图片、991个配套XML标注文件Pascal VOC格式及2个文本文件打包后约54.1MB便于离线使用与二次处理。数据分为真实场景与SD生成场景真实场景494张包含huapo滑坡与luoshi落石两类目标共534个标注框SD生成场景497张额外补充514个落石标注目标两类数据在光照、形态和背景上形成互补。已有2842人学习适合作为模型微调、迁移学习或检测性能对比的基准数据。用户无需额外格式转换即可直接读取快速适配YOLO、Faster R-CNN等常见检测框架既能帮助初学者熟悉VOC标注流程也能支撑工程师进行落石滑坡识别算法的快速验证与迭代。1. 公路落石与滑坡检测为什么991张VOC数据集值得下做目标检测的朋友都知道高质量的数据集往往比模型结构更“值钱”。公路沿线的落石、滑坡这两种地质灾害场景复杂、目标轮廓不规则背景还混着树木、岩壁和护栏的干扰。我手上这份公路落石和滑坡数据集包含991张真实公路场景图像标注文件采用VOC格式也就是每张图片配一个XML文件里面记录目标类别和边界框坐标。对于想快速上手目标检测、又不想从零开始标注的人来说这份数据够干净、够聚焦而且真实场景里的光线变化和遮挡都没被刻意清洗掉反而更像工程落地会遇到的情况。它适合三类人一是刚接触目标检测、需要一个真实而非玩具数据集的新手二是做边坡监测、交通安全相关项目的工程师可以用它验证模型可行性三是研究小样本检测、类别不平衡这类问题的同学。991张不算大但正因为小你才有机会把数据处理、训练、评估这条链路彻底跑通踩完该踩的坑。2. 揭开VOC格式的面纱目录结构与标注XML实战解析2.1 标准VOC数据集的目录组织VOC格式全称是Visual Object Classes源自PASCAL VOC竞赛后来成了目标检测领域最通用的标注格式之一。这份落石与滑坡数据集遵循的就是这套组织方式。按照惯例数据集根目录下会包含三个子目录JPEGImages存放原始图像Annotations存放XML标注文件ImageSets/Main存放划分后的图片文件名列表txt文件。有的分发版本会额外带一个labels目录那通常是别人已经转换好的YOLO格式如果你的资源是纯净VOC版那基本只有上面三个目录。拿到数据后第一步不是直接开训练而是先盘点目录和统计类别分布。这就像验收一件工具你要先确认螺丝和扳手都在。我一般会写一个快速脚本把JPEGImages里的图片数量和Annotations里的XML数量对齐同时解析出每个类别的框数。下面这个脚本可以帮你完成初步体检import os import xml.etree.ElementTree as ET # 修改为你的数据集实际路径 annotations_dir Annotations image_dir JPEGImages image_files set(os.listdir(image_dir)) xml_files set(os.listdir(annotations_dir)) # 检查图片和标注是否一一对应 missing_xml [img for img in image_files if img.replace(.jpg, .xml) not in xml_files] print(缺失XML的图片数量:, len(missing_xml)) # 统计每个类别的目标数量 from collections import Counter class_counter Counter() total_boxes 0 for xml_name in xml_files: tree ET.parse(os.path.join(annotations_dir, xml_name)) for obj in tree.findall(object): class_counter[obj.find(name).text] 1 total_boxes 1 print(目标类别统计:, dict(class_counter)) print(总目标框数:, total_boxes)逻辑说明先用集合做差集找出缺少标注的图片避免训练时因为空XML崩溃然后用xml.etree.ElementTree遍历所有XML统计每个object节点下的类别名。常见做法是把类别名放到一个字典里后续转YOLO格式时直接映射为数字ID。这里要注意数据集里的类别名可能并不统一比如「落石」有的写rockfall有的写rock_fall甚至会有中英文混杂。我见过一份历史数据里同一类出现三种写法导致训练时类别数量虚高、模型效果变差。所以这个统计脚本虽然简单却是整个流程里最不能省的一步。如果你发现自己拿到的数据存在这类问题别将就先写个小脚本做类别名映射。2.2 一个XML标注文件里藏着什么VOC格式的XML结构非常固定每个文件都包含以下关键字段folder是文件夹名filename是图片文件名size里是图像宽度、高度和通道数object节点中name是类别名bndbox里有xmin、ymin、xmax、ymax四个坐标值。下面用一个常见示例说明XML节点含义示例folder所属文件夹JPEGImagesfilename图片文件名rock_001.jpgsize/width图像宽度(像素)1920size/height图像高度1080size/depth通道数3object/name类别名称rockfallobject/bndbox边界框坐标xmin400, ymin600, xmax800, ymax900在落石和滑坡这样自然场景中目标形状往往不规则但边界框依然是矩形所以VOC格式完全够用。有一点值得说明VOC与YOLO、COCO的核心差异在于坐标体系。VOC记录的是绝对像素坐标即左上角和右下角YOLO记录的是归一化后的中心点坐标和宽高。如果你的模型用的是YOLO系就必须做转换而不是直接把XML喂给训练脚本。很多工程师在这个环节翻车是因为他们只改了图片路径却忘了坐标体系不匹配。2.3 用Python解析XML并可视化验证为了确认标注是否存在坐标越界或大小异常建议在转换前先做一次可视化验证。简单说就是把标注框画在原图上肉眼检查三五张图比任何自动化指标都直观。下面的代码会随机挑几张图并画出所有标注框import cv2 import xml.etree.ElementTree as ET import os, random random.seed(42) image_dir JPEGImages annotations_dir Annotations # 随机选取5张图 sample_xmls random.sample(os.listdir(annotations_dir), 5) for xml_name in sample_xmls: tree ET.parse(os.path.join(annotations_dir, xml_name)) root tree.getroot() img cv2.imread(os.path.join(image_dir, root.find(filename).text)) for obj in root.findall(object): 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) cv2.rectangle(img, (xmin, ymin), (xmax, ymax), (0, 255, 0), 2) cv2.putText(img, obj.find(name).text, (xmin, ymin-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1) cv2.imshow(check, img) cv2.waitKey(0) cv2.destroyAllWindows()参数说明random.seed(42)是为了复现抽样结果cv2.rectangle的坐标来自 bndbox 字段颜色用绿色和公路背景区分明显。如果画出的框和目视目标差距很大说明标注质量有问题需要回去检查原始XML。我自己的习惯是每100张抽出1张做人工复核991张也就是10张图花不了多少时间却能避免后面整个训练过程被脏数据带偏。2.4 为什么这份数据集坚持VOC格式兼容性与边界你可能会问为什么不直接发YOLO格式的txt标签VOC格式至少有四个优势。第一工具兼容性好labelImg、labelme、CVAT这些常用标注工具原生支持导出VOC后续补充标注不需要额外写转换脚本。第二XML是结构化文本标签错误能被人直接读出来而不像归一化坐标那样一眼看不出问题。第三它是很多传统检测模型如Faster R-CNN、SSD的标准输入不绑定某个具体框架。第四像素坐标便于做几何分析比如判断小目标占比、统计落石相对路面的大致位置。当然VOC格式也有它的问题坐标没归一化多语言标注时可能出现编码问题XML文件相对更占空间。但就991张这个量级来说这些缺点完全不是问题。我更关心的是这个数据集里的场景真实性——公路落石和滑坡目标往往嵌在岩壁纹理里颜色与背景相近所以类别之间的差异不光是框的位置还包括上下文语义。这种特性在后续转换、训练时都会体现出来你会在第3章和第4章看到我对这个问题的处理。3. 从VOC到YOLO格式转换与训练集划分的一条龙方案3.1 为什么要做格式转换YOLO系列训练脚本读的是txt标签格式每行代表一个目标格式为class_id x_center y_center width height所有坐标都是归一化到[0,1]的浮点数。VOC的XML是像素坐标且由左上角和右下角表达。转换的核心公式是将xmin/xmax转为中心点和宽再除以图像宽高。这一步如果除数选错会出现框错位、宽高比异常模型根本学不到东西。你可能想直接用一些现成工具包转换不就行了吗确实有xml_to_yolo这类库但在我实际经验里为了排查坐标越界自己写脚本更踏实。因为你需要在转换时对异常坐标做钳制这比事后处理日志更省事。3.2 一个可复用的VOC转YOLO脚本下面是我常用的转换脚本带类别映射和坐标钳制import os import xml.etree.ElementTree as ET from pathlib import Path # 类别映射顺序很重要后续训练也用这个顺序 class_mapping {rockfall: 0, landslide: 1} # 如果数据集有更多类别按类别名排序后映射 voc_root Path(/path/to/dataset) image_dir voc_root / JPEGImages xml_dir voc_root / Annotations label_dir voc_root / labels label_dir.mkdir(exist_okTrue) def clamp(value): return max(0.0, min(1.0, value)) def convert_xml_to_yolo(xml_path, image_width, image_height): tree ET.parse(xml_path) root tree.getroot() lines [] for obj in root.findall(object): name obj.find(name).text if name not in class_mapping: continue # 跳过未知类别并打印警告 class_id class_mapping[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 clamp((xmin xmax) / 2.0 / image_width) y_center clamp((ymin ymax) / 2.0 / image_height) width clamp((xmax - xmin) / image_width) height clamp((ymax - ymin) / image_height) lines.append(f{class_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}) return lines for xml_file in xml_dir.glob(*.xml): image_file image_dir / xml_file.name.replace(.xml, .jpg) if not image_file.exists(): print(f警告: 未找到对应图片 {image_file.name}) continue from PIL import Image with Image.open(image_file) as img: width, height img.size lines convert_xml_to_yolo(xml_file, width, height) out_path label_dir / xml_file.name.replace(.xml, .txt) with open(out_path, w) as f: f.write(\n.join(lines)) print(转换完成)逻辑说明脚本遍历每个XML读取对应图片的尺寸将每个目标的标注信息转成YOLO格式一行。关键参数是image_width和image_height必须从真实图片读取不能假设所有图片分辨率相同。我见过不少朋友在转换时直接用固定尺寸结果图片是1920x1080标注却按640x640归一化整个标签全废了。参数说明class_mapping是唯一需要你根据数据实际类别修改的地方。它决定了最终txt文件里第一列的数字如果你后续用YOLOv8训练类别顺序必须和数据配置yaml中的names列表一致。clamp函数会把越界坐标裁剪到0和1之间这能避免标注边缘目标时产生的负坐标或超宽框。3.3 划分训练集与验证集转换完标签后接着要划分训练集和验证集。YOLO系列通常需要三份txt列表train.txt、val.txt、test.txt有的版本只分train和val。传统做法是按8:1:1划分。但这个991张的数据集总量不算大我建议按 9:0.5:0.5 或者 8:1:1 都行关键是确保同一张图片的图片路径、标签路径和记录对齐。我一般用train_test_split并固定随机种子from sklearn.model_selection import train_test_split from pathlib import Path image_paths sorted(Path(JPEGImages).glob(*.jpg)) # 第一次切出10%作为测试集 train_val, test train_test_split(image_paths, test_size0.1, random_state42) # 第二次从剩余90%中切出大约10%作为验证集0.111 * 0.9 ≈ 0.1 train, val train_test_split(train_val, test_size0.111, random_state42) def write_list(file, paths): with open(file, w) as f: for p in paths: f.write(str(p.resolve()) \n) write_list(train.txt, train) write_list(val.txt, val) write_list(test.txt, test)逻辑说明先切出10%作为test再在剩余的90%里取11.1%作为val让val也约占总量的10%。random_state42是为了可复现如果有人想复现你的训练实验就需要同一个划分。代码里的resolve()会输出绝对路径这能避免相对路径在不同工作目录下失效。如果你用的是YOLOv8的detect模式其实不一定需要这三个txt文件。你可以在数据配置yaml里直接写图片目录路径框架会自动找标签。不过保留test.txt还是有价值的因为最终评估模型泛化能力时test集是你没碰过的一块能防止你在验证集上调参调过头。3.4 转换后的目录组织与常见误用我建议转换后的目录结构保持如下这也是YOLO官方推荐的模仿COCO的形式dataset/ ├── images/ │ ├── train/ │ │ ├── rock_001.jpg │ │ └── ... │ └── val/ │ └── ... ├── labels/ │ ├── train/ │ │ ├── rock_001.txt │ │ └── ... │ └── val/ │ └── ...这里最容易踩的坑就是图片和标签挂在同一个目录下然后还让训练脚本从同一个文件夹读取。YOLO默认是每个图片文件在同级目录下找相同文件名的txt如果你把图片放在images/train而标签不管挪到哪训练时都会Warning“label file not found”。所以无论用什么训练框架都要先搞清楚它读取标签的规则。如果用的是yolov5的detect.py它会找与图片同目录下同名的txt所以如果你按上面结构分开放需要额外在数据配置里指定train_labels路径吗并不用yolov5会按图片路径把标签路径由images替换为labels。但如果你乱放那就只能抱怨“玄学了”。3.5 转换后的校验清单转换完成不等于万事大吉。我每次都会执行一个简单的校验脚本输出每个txt的目标数和每个xml目标数是否一致并检查归一化坐标是否有大于1或小于0的异常项。import os from pathlib import Path label_dir Path(labels) xml_dir Path(Annotations) count_mismatch 0 for xml_file in xml_dir.glob(*.xml): label_file label_dir / xml_file.name.replace(.xml, .txt) if not label_file.exists(): print(f缺少标签文件: {label_file.name}) count_mismatch 1 continue with open(label_file, r) as f: lines [line.strip() for line in f.readlines() if line.strip()] # 检查坐标范围 for i, line in enumerate(lines): parts line.split() if len(parts) ! 5: print(f{label_file.name} 第{i1}行格式错误) count_mismatch 1 continue coords list(map(float, parts[1:])) if any(c 0 or c 1 for c in coords): print(f{label_file.name} 存在越界坐标 {line}) count_mismatch 1 # 粗略比较XML中object数量和txt行数因为可能有类别被跳过 # 通常应该相等若不相等需人工复核 print(校验完成异常数:, count_mismatch)这一步能提前筛掉标签缺失、坐标越界、行数不匹配三类问题。如果异常数不为0别急着训练先回头修数据。我统计过一次完整的人工复核大约需要1小时但能省下后面调试模型的很多个晚上。4. YOLOv8实战用991张落石数据训练你的第一个检测模型4.1 准备数据配置文件以YOLOv8为例训练时需要一个YAML文件指定训练/验证图片路径以及类别列表。假设我们已经把VOC转成了YOLO标签且图片放在images目录、标签放在labels目录那么配置文件可以写成# dataset.yaml path: /home/yourname/rockfall_landslide train: images/train val: images/val names: 0: rockfall 1: landslide注意names的顺序必须和第3章的class_mapping一致否则第一列数字对应的类别会错位。YOLOv8会自动读取images/train目录下所有jpg同时在同级目录下的labels/train目录中寻找同名txt。所以目录结构需要像第3.4节那样摆放。许多新手在这步会犯一个隐蔽错误他们在yaml中把path写成/home/yourname然后train: rockfall_landslide/images/train结果路径拼接后变成/home/yourname/rockfall_landslide/images/train没问题但如果写成相对路径比如train: ./images/train脚本实际是从path指定的绝对路径去找容易找不到。我一般都会用绝对路径虽然不优雅但省心。4.2 下载预训练权重与训练命令YOLOv8提供了在COCO上的预训练权重很适合迁移学习。你在这个小数据集上不要从零训练而是加载yolov8n.pt或yolov8s.pt作为起点收敛快且不易过拟合。训练命令如下yolo detect train datadataset.yaml modelyolov8s.pt epochs100 batch16 imgsz640 patience20 device0参数说明epochs100对于991张图是合理范围太小模型欠拟合太大容易过拟合batch16取决于显存如果显存只有6G可以降到8imgsz640是训练输入分辨率如果原始图是1920x1080640能保留大部分落石细节如果你发现目标太小可以试试imgsz960但训练时间会明显增加patience20是早停轮数当验证集指标连续20轮不提升会自动停止这比固定epoch更智能device0指定使用第一块GPU。如果你在CPU环境跑代码会报错或自动切CPU速度慢到怀疑人生。条件不允许的话可以把devicecpu但991张图一个epoch可能要跑好几分钟。如果尝试验证时发现 loss 一直不降我一般会加--cos_lr或调整lr0。但第一次训练什么都不动先看默认结果再说。4.3 评估指标怎么看训练结束后模型输出默认在runs/detect/train目录。打开results.png可以看到loss曲线和mAP曲线。你需要重点关注三个东西指标含义对于本数据集的期望mAP50预测框与真值框IoU0.5时的平均精度0.6以上算可用mAP50-95更严格的IoU阈值平均0.4左右Precision/Recall查准率与查全率两者平衡更健康如果是第一次训练大概率落石类的mAP不错滑坡类较差。这时可以打开confusion_matrix.png。如果滑坡样本大量被预测成落石说明两个类别的视觉特征太接近需要考虑增大输入分辨率或者使用第6章的数据增强策略。4.4 用训练好的模型做单张推理训练完成后验证模型是否真的“看到了”石头和滑坡最简单的方式是取几张验证集图片推理yolo detect predict modelruns/detect/train/weights/best.pt sourceimages/val/rock_001.jpg conf0.25参数说明conf0.25是置信度阈值低于该阈值的框会被过滤iou0.45可以控制NMS重叠阈值。如果你发现落石被框成多个交叠框增大iou到0.6融合相邻框如果你发现小石块被漏检降低conf到0.15并检查是否因为目标太小需要更高分辨率。这里的“玄学”其实是参数和场景的匹配问题。4.5 训练参数速查表如果你要快速遍历不同的配置下面是我在这个数据集上试过的参数组合供参考参数官方默认我推荐原因imgsz640640或960小目标多时用960batch168~16显存允许就16epochs100100早停会自行截断lr00.010.01太高易震荡freeze010先训head再解冻backboneoptimizerautoAdamW对小数据集更稳freeze10是我在迁移学习中常用的技巧前10轮冻结backbone特征提取层只训练head防止一开始反向传播就把预训练权重冲乱。你可以在命令中加freeze10。在第6章我还会继续提这一点。5. 避坑指南训练落石检测模型时最常见的五个问题这一章不打算讲泛泛的道理直接写我在处理类似数据集时踩过的坑。每一条都是“现象 → 原因 → 解决”三段式你如果正好碰到同款可以直接照做。5.1 训练时图片缺失导致中断现象训练刚开始或中途日志报错FileNotFoundError: ...有时候报错图片在某个路径找不到。原因最常见的是VOC数据集中某些图片文件名和XML不匹配。比如图片是IMG_001.JPGXML却是IMG_001.xml或者转换脚本里假设所有图片都是.jpg而实际存在.png、.jpeg后缀。文件大小写、中文名、空格也会引发路径解析失败。解决第一步用第2章的诊断脚本做文件名匹配检查第二步在转换时不要硬编码后缀用Path(image_path).stem作为关联键。我自己会把所有图片统一转换为JPG顺便用rename去掉文件名中的空格一劳永逸。5.2 标注框坐标越界导致训练发散现象模型训练出的宽高异常大甚至出现整个图都被框起来或者归一化坐标大于1。训练时损失不降反升。原因标注时边缘目标可能被工具吸附到图像外导致xmax或ymax超过图片宽度、高度。转YOLO时如果没有钳制归一化后坐标就会超出0-1范围。解决在转换函数里增加钳制逻辑。第3章已经给出了clamp函数但如果你是从别处拿来的转换脚本一定要检查它有没有做这步。另外转换后运行校验脚本统计越界标签比例。如果出现大面积越界那说明标注工具或标注人员有问题需要回炉。5.3 两类样本量严重不均衡现象训练时loss下降但mAP50卡在0.3验证集上落石类Recall高滑坡类几乎预测不出来。原因数据集中落石框可能有几千个滑坡框只有几百个模型被多数类主导。解决在YOLOv8中可以使用类别权重或简单地对少数类做离线复制增强。我更喜欢用增强方式因为能增加样本多样性。比如将滑坡样本从原始图中裁剪出来随机贴在岩石背景上但要注意别把背景贴得过于假。更简单的是在训练命令中加cls_sup或直接调大loss_gain的类别权重。如果你的框架支持class_weights就在配置里设置。5.4 模型过拟合到背景纹理现象训练集mAP很高验证集mAP很低推理时把整齐的岩壁纹理误检为落石。原因这类公路场景中目标与背景的边界模糊且991张图可能来自同一段公路或同一时间段背景重复度高。模型很容易记住背景纹理而不是抽象出“石块”的语义。解决开启更强的数据增强。Mosaic、MixUp、随机HSV扰动是最基础的。针对落石场景我额外加了CoarseDropout随机遮挡和RandomGamma曝光变化。如果条件允许把训练和验证图片按场景分组避免同一段路的图片同时出现在两个集合导致评估虚高。5.5 训练时loss变成nan现象训练几轮后loss变成nan模型权重崩溃日志里出现一堆inf。原因学习率过大或初始锚框不适用于小目标。991张图中很多落石是小目标默认锚框基于COCO生成对岩壁场景不一定合适。解决在yolov5中开启--autoanchorYOLOv8默认会自动计算锚框所以如果你用的是v8这步相对省心。此外学习率不要超过0.01使用Adam时注意权重衰减不要调太高。如果nan出现在第一轮多半是数据里存在空标签或损坏图片需要检查labels目录中是否有0字节txt以及图片文件能否被正常解码。这些坑我都一个个亲测过。特别是5.3和5.4这两个问题经常同时出现很多人误以为是模型结构不行其实是从数据处理就埋下了陷阱。每次看到群里有人发“模型效果差求调参”我都想先问一句你的标签校验过了吗目标分布看过吗如果这些都跳过那后面遇到的问题多半要从数据找原因。6. 进阶技巧数据增强与鲁棒性验证让落石检测不受天气干扰6.1 用Albumentations做针对性增强公路落石检测的难点在于光照和遮挡。991张原始数据覆盖的天气终究有限想提高模型泛化能力我会上Albumentations对训练集做在线增强。下面是一个适合该场景的增强管线示例import albumentations as A from albumentations.pytorch import ToTensorV2 transform A.Compose([ A.RandomResizedCrop(height640, width640, scale(0.8, 1.0), p0.5), A.HorizontalFlip(p0.5), A.RandomBrightnessContrast(brightness_limit0.3, contrast_limit0.3, p0.6), A.HueSaturationValue(hue_shift_limit10, sat_shift_limit20, val_shift_limit20, p0.5), A.GaussNoise(var_limit(10.0, 50.0), p0.3), A.CoarseDropout(max_holes8, max_height80, max_width80, fill_value0, p0.2), ToTensorV2(), ])参数解释RandomResizedCrop模拟不同距离下的目标比例变化同时裁掉部分背景让模型更关注目标CoarseDropout随机删除图像块模拟目标被栅栏、枝叶遮挡GaussNoise增强对传感器噪声的鲁棒性。这些增强都在训练时动态执行不需要提前制作增强后图片因为在线增强可以生成无限组合。6.2 用测试集验证鲁棒性训练结束不要只看mAP。我会额外在恶劣天气、夜晚、强背光这三类图片上单独跑一遍推理统计漏检率。如果你没有额外数据可以人为把测试集图片做色彩偏移和加噪看看mAP掉多少。如果掉幅超过20%说明模型对光照太敏感需要回到增强配置加上RandomGamma和CLAHE。我自己的一个习惯是用预训练权重做微调时强制设置freeze10。有一段时间我图省事没冻结backbone直接训全部层结果前几轮梯度剧烈波动把预训练的特征彻底打乱最后mAP比从头训练还低。从那以后我每次用YOLO做迁移学习都会在训练命令里加上freeze10先让head层学落石、滑坡的差异再解冻backbone做精细调参。这个方法在这个991张的落石数据集上效果尤其明显。希望帮到你。本文还有配套的精品资源点击获取
返回列表