
简介面向遥感图像分析与地质灾害监测场景这份航拍滑坡数据集包含4315张512×512分辨率的无人机航拍影像及对应标注可帮助计算机视觉初学者、滑坡识别算法研究者减少数据采集与标注成本快速开展目标检测实验。数据来源于开源仓库datasets_sl仅标注landslide一个类别共11315个矩形框由labelImg工具制作同时提供Pascal VOC与YOLO两种通用格式并已做增强可直接输入YOLO、SSD等主流检测框架每张影像均配有对应的xml与txt标注文件便于对照学习两种标注格式的差异。压缩包共2000个文件以1999个XML标注文件为主另附说明文档整体约201MB目录结构清晰。目前已有35人学习下载。需注意数据集未划分训练、验证、测试集使用者应自行拆分发布方不对训练精度作保证适合用于理解数据集组织、标注格式转换及模型性能对比等完整流程。1. 航拍滑坡数据集4315 张双格式标注能直接开工吗拿到航拍滑坡数据集这份 4315 张的双格式标注包时我第一反应不是去数文件而是先确认标注里的“滑坡”到底怎么定义。航拍视角下滑坡体像扇面和裸土、公路边坡颜色接近人眼都容易看错标注口径不一致会直接拖垮后面所有训练。这份资源的价值不在于图像数量而在于同一份数据同时给出 VOC 和 YOLO 两套标注省掉前期格式搬运的时间适合做地质灾害普查、灾后快速制图或者跑遥感目标检测实验的从业者。接下来从目录解析开始把训练、验证、排错全流程走一遍。2. 拆开压缩包VOC 与 YOLO 两种标注格式的目录对照2.1 先看目录再谈训练拿到任何标注数据包我习惯先解压后看目录而不是直接开训练。滑坡检测最怕的是“数据在、标签不在”的假完整unzip 航拍滑坡数据集4315张VOCYOLO格式.zip -d landslide_data ls -la landslide_data du -sh landslide_data常见做法是压缩包里会有 JPEGImages、Annotations、ImageSets/Main 三个 VOC 标准目录另外配一个 labels 目录专门放 YOLO 的 txt。上面那条 du 命令用来确认图片总体积如果 4315 张标注图加起来只有不到 200MB说明图像分辨率可能不高或者压缩率很大的 JPG 居多后面训练时 imgsz 就要适当给低一点。目录或文件作用常见问题JPEGImages存放航拍原图文件名与 XML 对不上Annotations存放 VOC 格式 XML有对象但 size 字段缺失ImageSets/Main存放 train/val/test 文件列表没有则需自己划分labels存放 YOLO 格式 txt空 txt 表示该图无目标我当时会先执行 find 确认目录层级再看 ImageSets/Main 里有没有划分文件。若只有前三个目录而没有 labels就用第 4 章的脚本转换若 labels 和 Annotations 都有就直接进入训练。2.2 VOC XML 解析一个对象五个字段VOC 格式的核心是每个对象对应一个 XML 文件。拿一份典型的滑坡标注 XML 来说annotation filenameUAV_20231012_001.jpg/filename size width1920/width height1080/height depth3/depth /size object namelandslide/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin300/xmin ymin400/ymin xmax800/xmax ymax700/ymax /bndbox /object /annotation注意几个字段size.width 和 size.height 是整张图像的宽高后面归一化坐标全靠它bndbox 的四个值是像素坐标xmin/xmax 是左右边界ymin/ymax 是上下边界。很多解析脚本喜欢直接读 object 的 name 当类别这里滑坡类别名可能是 landslide也可能是 slip、collapse先统一成一份类别表再批量转换能省掉后面类别名不一致的破事。2.3 从 VOC 到 YOLO坐标换算与归一化YOLO 的标注文件每一行是类别ID 中心点x 中心点y 宽度w 高度h前四个数都是 01 的归一化值。换算关系很简单x_center ((xmin xmax) / 2) / width y_center ((ymin ymax) / 2) / height box_w (xmax - xmin) / width box_h (ymax - ymin) / height用上面 XML 的例子算一遍在 1920×1080 的图上框从 (300,400) 到 (800,700)xmin, xmax, ymin, ymax 300, 800, 400, 700 w, h 1920, 1080 xc (xmin xmax) / 2 / w # 0.2865 yc (ymin ymax) / 2 / h # 0.5093 bw (xmax - xmin) / w # 0.2604 bh (ymax - ymin) / h # 0.2778这四个小数就是 YOLO txt 里要写的四个数。注意计算时全部用浮点数不要先 int 再除否则小目标框换算后边界会偏移。VOC 转 YOLO 的脚本里最容易被忽略的是输出精度txt 里保留 6 位小数基本够用做浮点转换时不要四舍五入成整数再参与运算。2.4 用统计代替拍脑袋先摸清目标尺度在训练之前先统计一下整包标签的目标尺度能替你后续省很多试错时间。import xml.etree.ElementTree as ET from glob import glob xmls glob(landslide_data/Annotations/*.xml) boxes [] for p in xmls: root ET.parse(p).getroot() w float(root.find(size/width).text) h float(root.find(size/height).text) for obj in root.findall(object): bb obj.find(bndbox) bw (float(bb.find(xmax).text) - float(bb.find(xmin).text)) / w bh (float(bb.find(ymax).text) - float(bb.find(ymin).text)) / h boxes.append((bw, bh)) avg_w sum(b[0] for b in boxes) / len(boxes) avg_h sum(b[1] for b in boxes) / len(boxes) print(fboxes: {len(boxes)}, avg_w: {avg_w:.4f}, avg_h: {avg_h:.4f})这段脚本把每个框的宽和高分别除以整图宽高得到归一化尺度。如果平均框宽不到整图宽度的 20%说明目标偏小训练 imgsz 至少要给到 1280不然滑坡边界细节会在缩放时被抹掉。这里统计的是相对尺度不受图像分辨率影响比直接看像素值更可靠。3. 从数据集到训练YOLO 流程与关键参数3.1 划分训练集与验证集先看分布再切分如果 ImageSets/Main 里已经有 train.txt 和 val.txt直接使用即可没有就自己做独立划分。航拍滑坡图有个特点同一个航带里的多帧图像背景高度重复按文件名随机乱切容易造成同一片区域同时出现在训练集和验证集mAP 虚高。我一般会把文件名按前缀分组比如 UAV_20231012_001、UAV_20231012_002 属于同一条航带只允许整组进入同一侧import os, random, glob from collections import defaultdict imgs sorted(glob.glob(landslide_data/JPEGImages/*.jpg)) groups defaultdict(list) for p in imgs: base os.path.basename(p) # UAV_20231012_001.jpg prefix base.split(_)[0] _ base.split(_)[1] # UAV_20231012 groups[prefix].append(p) val_keys set(random.sample(list(groups.keys()), max(1, len(groups) // 5))) train_imgs, val_imgs [], [] for k, v in groups.items(): if k in val_keys: val_imgs.extend(v) else: train_imgs.extend(v) with open(landslide_data/train.txt, w) as f: f.write(\n.join(train_imgs)) with open(landslide_data/val.txt, w) as f: f.write(\n.join(val_imgs))这里按文件名前缀分组前缀相同的归为同源航带。random.sample 从组里抽 20% 的航带作为验证集避免同一批图被拆进两个集合。实测中这样做出来的 val mAP 比乱切低一些但更接近真实泛化水平。如果你的文件名没有可见前缀就用图像感知哈希做相似度聚类按聚类结果分组再划分。3.2 生成 data.yaml拿到 YOLO 格式数据后训练前要有一份 data.yaml。目录结构常见有两种一种是把图片拆成 images/train 和 images/val另一种是维持扁平的 JPEGImages再用文件列表指定划分。我这里按第二种写兼容性更好path: ./landslide_data train: train.txt val: val.txt nc: 1 names: 0: landslidepath 是相对于当前工作目录的根路径train/val 指向文件列表。nc 表示类别数这个数据集如果只有滑坡一类就写 1如果还包含泥石流、崩塌等类别要按实际类别表补齐 names。yaml 里的缩进必须用空格不能用 Tab这是新手最容易翻车的地方。注意不要直接把 JPEGImages 目录写成 train否则模型会把整个目录当输入后面验证时没有任何标签文件可读。如果你的数据组织是 images/ 和 labels/ 配对train 就写 images/train 的路径程序会自动匹配同名 txt用文件列表的方式则更明确不容易踩路径坑。3.3 模型选型n/s/m 怎么选数据量 4315 张属于中小规模我建议优先从 YOLO 模型的 small 或 medium 型号开始。用 nano 起步虽然训练快但航拍小目标表达力不够用 large/xlarge 在单卡上容易爆显存。型号显存占用推理速度小目标表现适用场景n低最快一般嵌入式设备、快速初筛s中快中等单卡实验、批量推理m较高中较好精度优先的离线制图我的习惯是先在桌面级显卡上用 s 跑一个 40 epoch 的短训练确认 loss 能降下来再决定是否升到 m。不要一开始就堆大模型数据集的标注噪声和背景干扰会掩盖模型容量的收益。滑坡检测的难点集中在“小目标”和“边界模糊”上模型容量给到 m 以后提升更多来自数据和推理策略而不是继续加大网络。3.4 训练启动命令与结果检查确认 data.yaml 无误后用 ultralytics 的 CLI 训练yolo taskdetect modetrain \ modelyolov8s.pt \ datalandslide_data/data.yaml \ epochs100 batch16 imgsz1280 \ lr00.01 optimizerSGD \ cacheTrue workers4参数说明imgsz1280 是航拍小目标检测的关键缩到 640 会把小型滑坡压成几个像素batch 根据显存调整16G 显存跑 s 模型一般没问题。lr0 用 0.01 配 SGD比 Adam 更容易收敛到稳定的边界框epochs 先试 100看 results.csv 里的 val 曲线是否还在下降。训练完的结果在 runs/detect/train 下weights/best.pt 和 last.pt 是最重要的两个权重。我会用一个小脚本直接读 results.csvimport pandas as pd df pd.read_csv(runs/detect/train/results.csv) print(df[[epoch, metrics/precision(B), metrics/recall(B), metrics/mAP50(B), metrics/mAP50-95(B)]].tail())看最后 10 行的 mAP50 和 mAP50-95 是否同步上升。如果 mAP50 高但 mAP50-95 低说明框的位置不够准后续要重点检查标注边界是否贴合滑坡边缘。4. 格式转换与可视化核查四个边界坑4.1 一个可靠的 VOC 到 YOLO 转换脚本如果包内只有 VOC 标注或想重新生成一份干净的 labels我一般这样写转换脚本import os import xml.etree.ElementTree as ET from glob import glob CLASS_NAMES [landslide] # 按实际类别顺序 def convert_voc_to_yolo(xml_path, out_txt_path): tree ET.parse(xml_path) root tree.getroot() size root.find(size) img_w float(size.find(width).text) img_h float(size.find(height).text) lines [] for obj in root.findall(object): name obj.find(name).text.strip() if name not in CLASS_NAMES: continue cls_id CLASS_NAMES.index(name) box obj.find(bndbox) xmin max(float(box.find(xmin).text), 0.0) ymin max(float(box.find(ymin).text), 0.0) xmax min(float(box.find(xmax).text), img_w) ymax min(float(box.find(ymax).text), img_h) if xmax xmin or ymax ymin: continue x_center ((xmin xmax) / 2.0) / img_w y_center ((ymin ymax) / 2.0) / img_h box_w (xmax - xmin) / img_w box_h (ymax - ymin) / img_h lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}) with open(out_txt_path, w) as f: f.write(\n.join(lines) \n if lines else ) xml_paths glob(landslide_data/Annotations/*.xml) for xp in xml_paths: txt_path os.path.join(landslide_data/labels, os.path.basename(xp).replace(.xml, .txt)) os.makedirs(os.path.dirname(txt_path), exist_okTrue) convert_voc_to_yolo(xp, txt_path)这段脚本有三个关键设计一是用 float() 而不是 int() 解析 XML 里的数字兼容所有框数值记录为浮点字符串的情况二是把 xmin/ymin 下限裁到 0、xmax/ymax 上限裁到图片宽高防止标注越界三是检测到 xmaxxmin 或 ymaxymin 时直接跳过避免产出面积为零的坏框。生成的 txt 行保留 6 位小数后面训练不会因为精度问题报错。4.2 转换后的可视化抽查转换脚本跑完必须做可视化抽查。很多问题在数字层面看不出画在图上立刻暴露import cv2 def draw_yolo_label(image_path, txt_path, out_path): img cv2.imread(image_path) h, w img.shape[:2] with open(txt_path) as f: for line in f: parts line.strip().split() if len(parts) ! 5: continue _, xc, yc, bw, bh map(float, parts) x1 int((xc - bw / 2.0) * w) y1 int((yc - bh / 2.0) * h) x2 int((xc bw / 2.0) * w) y2 int((yc bh / 2.0) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 0, 255), 2) cv2.imwrite(out_path, img) draw_yolo_label(landslide_data/JPEGImages/UAV_20231012_001.jpg, landslide_data/labels/UAV_20231012_001.txt, check_001.jpg)这段代码把 YOLO txt 还原成像素框并画回原图。抽查时我会重点看三类图框明显偏大或偏小、框跑到图片边缘、图里明明有滑坡但 txt 为空。空 txt 不一定是错误但要统计空标签比例如果超过 10%说明标注规范里很可能把部分滑坡标成了 difficult需要决定是补标注还是保留为背景。4.3 四个边界坑汇总坑现象处理方式越界框框超出图片边界转换时裁剪到图内面积为零转出的 w/h 为 0直接丢弃该对象类别名不一致landslide/landslip/slip 混用先统一类别表再转换宽高不匹配XML 的 size 与图片实际尺寸不同以 XML size 为准转换后抽查最后一个坑最隐蔽有些 XML 里的 size 是从设备信息里读到的旋转前尺寸而 JPEG 实际像素是旋转后的。我遇到这种情况会先挑一张图做转换测试用上面 4.2 的可视化脚本画出来对比确认像素坐标对得上再批量跑。批量转换后还应该检查 labels 目录里的 txt 总数是否和 Annotations 目录里含目标的 XML 数量一致。5. 常见问题与排查航拍滑坡检测里最容易翻车的 5 个环节5.1 Loss 迟迟不降背景占比太大现象训练前 20 个 epoch 的 loss 曲线几乎平躺mAP 不超过 0.05。原因航拍图里大面积是山体、植被和裸露黄土滑坡目标常只占整张图的 2%5%模型把几乎全图当成背景正样本过少梯度被负样本淹没。解决先用脚本统计每个框面积与整图面积的比例若多数比例小于 0.02就把训练 imgsz 调到 1280 以上并在数据增强里开启 mosaic 和 mixup。如果仍然不降把大图切成 640×640 的 tile只保留至少含一个滑坡框的 tile 进训练集背景比例会立刻恢复正常。5.2 验证集 mAP 虚高同源航带被拆散现象训练时 mAP 到 0.85验证集 mAP 也到 0.82看起来很好换到另一批新航拍数据后直接掉到 0.5。原因同一条航带的多张图片背景高度重叠随机按图片划分会让同一场景既出现在训练又出现在验证模型相当于记住了场景而不是学会检测滑坡。解决按航带前缀分组组内整组划分。若压缩包内文件名看不出航带信息可以先用图像感知哈希对全图做去重聚类把相似度超过阈值的图归为同一组再划分。从那以后我只用这种划分方式验证曲线才勉强配得上真实效果。5.3 转出来的框整体偏移坐标换算出错现象YOLO 训练结果里预测框总是在真实滑坡位置的右下方且偏移量固定。原因最常见是把归一化分子算错像用 (xmax - xmin) 当中心点偏移或者 XML 的 height 被误填成 width。VOC 转 YOLO 的脚本在批量跑之前没做单图验证。解决抽查一张 4.2 的还原图与原始 XML 可视化并排对比。若发现固定偏移优先检查 size 节点取的是不是 width 和 height若框的大小对但位置错逐行 debug 中心点公式。5.4 小滑坡淹没在背景里缩放与模型容量现象原图检测能看到小滑坡把 imgsz 降到 640 后漏检率明显上升。原因目标只有 20×20 像素缩放到 640 后只剩 7×7 像素特征图上的响应被周围纹理淹没检测头难以输出有效框。解决航拍场景我提两个方案一是强行把训练和推理 imgsz 提到 1280 或 1536代价是显存和速度二是用滑动窗口推理把大图先切成若干 640 的 tile直接在小比例尺下检测。后面第 6 章会给出滑动窗口的具体代码。5.5 mAP50 高、mAP50-95 低边界框贴合度差现象mAP50 到 0.8mAP50-95 只有 0.45。原因滑坡体边界模糊尤其是滑面与裸土相接处标注员画的框和模型预测的框之间经常有 510 像素的差异IoU 在 0.5 附近没问题但要达到 0.75 就难。解决这类问题不是单靠训练能解决的。我一般会检查标注边界用 4.2 的可视化脚本把标注框换成半透明填充肉眼凡是看到框没包住滑坡舌部起点就重新修标。此外训练时开启 CIoU 损失并用更多轮次微调能在一定程度上让回归更紧。6. 进阶技巧滑动窗口推理与伪标注迭代6.1 滑动窗口推理代码大图直接推理对小目标不友好我把一张 5000×4000 的航片切成长宽 1024、重叠 20% 的窗口分别推理再把框坐标映射回大图import cv2 import numpy as np from ultralytics import YOLO model YOLO(runs/detect/train/weights/best.pt) img cv2.imread(big_ortho.jpg) H, W img.shape[:2] tile_size 1024 overlap 0.2 step int(tile_size * (1 - overlap)) all_boxes [] for y in range(0, H, step): for x in range(0, W, step): y2 min(y tile_size, H) x2 min(x tile_size, W) tile img[y:y2, x:x2] results model(tile, conf0.35, imgsz1024) for box in results[0].boxes.data.cpu().numpy(): tx1, ty1, tx2, ty2, conf, cls box all_boxes.append((x tx1, y ty1, x tx2, y ty2, conf))切图时 x2 和 y2 用了 min 防止越界但窗口会比 tile_size 小最右列和最下行的目标会被截断。更好的做法是给滑窗加 padding在最边缘补一圈灰色像素推理后再把坐标平移回原图。上面的代码只保留 conf0.35 的框这个阈值我会用验证集的 PR 曲线来定而不是拍脑袋。6.2 伪标注迭代让模型自己筛数据当我手上有一批完全没有标注的新航拍图时会用已有模型先推理一遍把置信度高于 0.9 且框面积落在统计区间的结果当成伪标注加入训练集。这个做法不是无脑自训练关键是把伪标注单独放一个目录训练时用一份独立的 data.yamlpath: ./pseudo_data train: pseudo_train.txt val: val.txt nc: 1 names: 0: landslide伪标签只做初筛绝不能和人工标注混合在一起跑全量训练否则模型会把自己错误固化。我一般先训练两轮对比加入伪标注前后在真实标注验证集上的 mAP如果涨了 12 个点就保留否则删掉这批伪标注。6.3 验证的小习惯从那以后我每次拿到新航区的数据都会强制先走一遍统计目标面积分布、按航带划分、转换后可视化抽查这三步再决定要不要进入训练。这份航拍滑坡数据集让我把所有流程完整踩了一遍也养成了用数据质量控制代替盲目调参的习惯。希望帮到你。本文还有配套的精品资源点击获取