简介本资源面向深度学习图像分割方向的学习者与研究者提供大分辨率遥感影像道路提取任务的完整数据集适合用于分割网络的训练、测试与效果验证。数据集已预先划分训练集与测试集训练集包含4981张图像及4981张对应mask测试集包含1245张图像及1245张对应mask前景像素标注质量较高可直接作为分割模型的测试数据使用。压缩包共约2000个文件以jpeg图像与mask标注为主另附1个Python可视化脚本整体大小约336.79MB采用7z格式打包。该脚本可随机抽取一张图片将原始影像、GT图像以及GT在原图上的蒙板叠加结果一并展示并保存至当前目录便于直观检查标注质量与模型输出。目前已有977人学习下载适合需要遥感道路分割数据、快速搭建实验流程或验证算法性能的读者参考使用。1. 遥感道路分割数据集513MB 里藏着 6226 对图先搞清楚它能不能喂进你的网络遥感影像里的道路提取翻车最多的环节往往不是模型结构而是数据。你拿到的原始卫星图动辄 4000×4000 像素道路只占其中几个像素宽直接 resize 到 512×512 喂给网络细窄的路面直接糊成背景IoU 掉到 0.3 以下都不奇怪。这份 DeepGlobe Road Dataset 就是冲着这个痛点来的513MB训练集 4981 对 image/mask测试集 1245 对前景像素标注干净mask 是二值化的道路区域适合直接拿来做分割网络的训练和验证。它适合三类人想跑通遥感道路分割 baseline 的深度学习入门者、需要一份干净数据做对比实验的研究者、以及拿它当测试集验证自己模型泛化能力的工程师。文件名是数字 ID 加_mask后缀配对关系明确省去了自己写匹配脚本的功夫。2. 数据组织与配对逻辑4981 对训练样本怎么读进 DataLoader2.1 目录结构与命名规则拿到数据集先别急着写模型花五分钟把目录结构摸清楚后面能省掉大量 debug 时间。这份数据的组织方式很直白dataset/ ├── train/ │ ├── images/ # 4981 张原始遥感图 │ │ ├── 495744_mask.jpeg # 注意原始图也带 _mask 后缀 │ │ ├── 497990_mask.jpeg │ │ └── ... │ └── masks/ # 4981 张对应的二值 mask │ ├── 495744_mask.jpeg │ ├── 497990_mask.jpeg │ └── ... └── test/ ├── images/ # 1245 张 └── masks/ # 1245 张这里有个容易踩的点原始图片文件名里也带了_mask后缀和 mask 目录下的文件名完全一致。配对逻辑因此变得极其简单——同名文件直接对应不需要做任何字符串替换。常见做法是用os.listdir分别读取两个目录取交集后排序保证 image 和 mask 一一对应。2.2 用 Dataset 类做配对加载下面这段代码是我常用的配对加载模板直接抄就能用import os from PIL import Image from torch.utils.data import Dataset, DataLoader import torchvision.transforms as T class RoadSegDataset(Dataset): def __init__(self, root, splittrain, img_size512): self.img_dir os.path.join(root, split, images) self.mask_dir os.path.join(root, split, masks) # 取两个目录的文件名交集确保配对 img_names set(os.listdir(self.img_dir)) mask_names set(os.listdir(self.mask_dir)) self.names sorted(list(img_names mask_names)) self.img_size img_size # 图像用 ImageNet 均值方差归一化mask 只做 resize self.img_tf T.Compose([ T.Resize((img_size, img_size)), T.ToTensor(), T.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) self.mask_tf T.Compose([ T.Resize((img_size, img_size), interpolationT.InterpolationMode.NEAREST), T.ToTensor() ]) def __len__(self): return len(self.names) def __getitem__(self, idx): name self.names[idx] img Image.open(os.path.join(self.img_dir, name)).convert(RGB) mask Image.open(os.path.join(self.mask_dir, name)).convert(L) img self.img_tf(img) mask self.mask_tf(mask) # 二值化像素值大于 0.5 视为道路 mask (mask 0.5).float() return img, mask # 使用示例 train_ds RoadSegDataset(root./dataset, splittrain, img_size512) train_loader DataLoader(train_ds, batch_size8, shuffleTrue, num_workers4) print(f训练集样本数: {len(train_ds)}) # 应为 4981逻辑说明img_names mask_names取交集是关键一步防止某个目录多出或缺失文件导致索引错位。mask 的 resize 必须用NEAREST插值用双线性会把二值边缘插出灰色像素二值化后边缘反而更毛糙。归一化参数用 ImageNet 的标准值如果你从零训练可以改成数据集自身的均值方差但用预训练 backbone 时保持一致更稳妥。参数说明img_size控制输出分辨率512 是速度和精度的折中点batch_size8在 8GB 显存下跑 U-Net 比较安全显存够可以往上加num_workers4根据 CPU 核数调整设太大反而会因为进程切换拖慢速度。2.3 可视化脚本先看数据再训模型数据集自带一个可视化脚本随机抽一张图把原图、GT 和 GT 叠加在原图上的蒙板效果展示出来并保存到当前目录。这个脚本的价值在于训练前跑一遍能快速判断标注质量是否符合预期。我一般会先跑它确认三件事——mask 是否二值干净、道路是否连续、有没有整张全黑的废样本。如果发现某张 mask 全黑但原图明显有路那就是标注遗漏训练时可以考虑剔除或降权。3. 训练配置与损失函数选择道路分割为什么不能只用交叉熵3.1 类别极不平衡下的损失函数遥感道路分割最核心的挑战是前景背景极度不平衡。一张 512×512 的图里道路像素可能只占 3% 到 8%剩下全是背景。这种情况下如果只用二元交叉熵BCE网络会倾向于把所有像素预测成背景准确率看起来有 92% 以上但 IoU 接近 0模型完全没用。血泪经验是BCE 的 loss 曲线很漂亮但验证集上的 mask 一片黑。常见做法是 BCE 和 Dice Loss 组合import torch import torch.nn as nn class BCEDiceLoss(nn.Module): def __init__(self, bce_weight0.5): super().__init__() self.bce nn.BCEWithLogitsLoss() self.bce_weight bce_weight def forward(self, pred, target): # pred: (B, 1, H, W) logits, target: (B, 1, H, W) 0/1 bce_loss self.bce(pred, target) # Dice Loss pred_sigmoid torch.sigmoid(pred) intersection (pred_sigmoid * target).sum(dim(2, 3)) union pred_sigmoid.sum(dim(2, 3)) target.sum(dim(2, 3)) dice (2 * intersection 1e-6) / (union 1e-6) dice_loss 1 - dice.mean() return self.bce_weight * bce_loss (1 - self.bce_weight) * dice_loss逻辑说明BCE 提供稳定的逐像素梯度Dice 直接优化重叠度两者互补。bce_weight0.5是常用起点如果训练初期 loss 震荡大可以调到 0.7 让 BCE 主导如果 IoU 涨得太慢调到 0.3 让 Dice 发力。平滑项1e-6防止除零不要省。参数说明BCEWithLogitsLoss内部集成了 sigmoid所以网络输出不要再加 sigmoid 层否则梯度会出问题。Dice 计算在 batch 维度上取 mean如果显存吃紧可以改成逐样本计算再平均。3.2 学习率与优化器配置道路分割任务上Adam 比 SGD 收敛快但最终精度 SGD 配合 cosine 退火往往更好。我一般分两阶段前 20 个 epoch 用 Adamlr1e-3快速把 loss 降下来后 80 个 epoch 切 SGDlr1e-2 配 cosine 退火到 1e-5。如果不想折腾直接 Adam lr1e-4 cosine 也能跑出可用的结果。from torch.optim import Adam from torch.optim.lr_scheduler import CosineAnnealingLR model YourSegModel() optimizer Adam(model.parameters(), lr1e-4, weight_decay1e-4) scheduler CosineAnnealingLR(optimizer, T_max100, eta_min1e-6) for epoch in range(100): model.train() for img, mask in train_loader: img, mask img.cuda(), mask.cuda() pred model(img) loss criterion(pred, mask) optimizer.zero_grad() loss.backward() optimizer.step() scheduler.step()weight_decay1e-4对分割任务够用太大反而会让边缘预测变差。T_max设成总 epoch 数eta_min是最小学习率别设成 0留一点让模型在后期微调。3.3 数据增强的边界遥感图像和自然图像不同翻转和旋转是安全的但颜色抖动要谨慎。道路的纹理和颜色在遥感图里有实际物理意义过度抖动会让模型学到错误的颜色先验。我一般只用水平翻转、垂直翻转和 90 度旋转不做随机裁剪——因为道路是连续结构裁掉一段会让标签变得不完整。4. 避坑与排查训练遥感道路分割时最容易翻车的五个地方4.1 mask 读出来全是 255 或全是 0现象可视化时 mask 一片白或一片黑但原图正常。原因PIL 读 JPEG 格式的 mask 时如果 mask 保存时用了非标准灰度映射convert(L)后像素值可能集中在 0 和 255 两端但阈值判断写成了 0而不是 127。解决统一用(mask 127).float()做二值化或者先打印mask.min()和mask.max()确认分布。4.2 训练 loss 下降但验证 IoU 不动现象训练集 loss 从 0.8 降到 0.1验证集 IoU 始终在 0.2 附近。原因训练集和测试集的道路宽度分布差异大或者模型过拟合了训练集的特定纹理。解决先跑可视化脚本对比训练集和测试集的 mask 统计确认道路像素占比是否一致如果差异大考虑在训练时加入测试集风格的样本做域适应。4.3 DataLoader 报 “image file truncated”现象训练到一半突然报错提示某张 JPEG 文件损坏。原因数据集在下载或解压过程中个别文件不完整。解决写个脚本遍历所有图片用Image.open().verify()检查完整性把损坏的文件从列表里剔除。这个数据集 6226 张图偶尔有一两张损坏是正常的。4.4 显存溢出但 batch_size 已经调到 1现象batch_size1 仍然 OOM。原因输入分辨率设成了 1024 或原图尺寸U-Net 的 skip connection 在浅层特征图很大。解决把img_size降到 512 或 384或者改用轻量 backbone如 MobileNetV2替换 ResNet50。如果必须用大分辨率可以试试梯度累积batch_size1累积 8 步再更新。4.5 预测结果边缘锯齿严重现象模型输出的 mask 边缘呈阶梯状不光滑。原因上采样用了最近邻插值或者损失函数里 Dice 权重过高导致边缘梯度不稳定。解决解码器上采样改用双线性插值最后加一个 3×3 卷积平滑损失函数里把 Dice 权重从 0.5 降到 0.3让 BCE 提供更细粒度的边缘梯度。5. 从测试集到实际部署用 1245 对样本验证模型泛化能力训练完模型测试集怎么用才不浪费很多人只算一个全局 IoU 就完事了但遥感道路分割的泛化能力要看分层指标。我一般会把测试集按道路像素占比分成三档低密度5%、中密度5%-15%、高密度15%分别统计 IoU 和 F1。这样能看出模型是不是只在道路密集的区域表现好稀疏区域直接摆烂。import numpy as np from PIL import Image def evaluate_by_density(model, test_loader, devicecuda): model.eval() results {low: [], mid: [], high: []} with torch.no_grad(): for img, mask in test_loader: img, mask img.to(device), mask.to(device) pred torch.sigmoid(model(img)) pred_bin (pred 0.5).float() # 逐样本计算 IoU for i in range(img.size(0)): p, m pred_bin[i], mask[i] inter (p * m).sum().item() union p.sum().item() m.sum().item() - inter iou inter / (union 1e-6) density m.mean().item() if density 0.05: results[low].append(iou) elif density 0.15: results[mid].append(iou) else: results[high].append(iou) for k, v in results.items(): print(f{k} density: mean IoU {np.mean(v):.4f}, samples {len(v)})这段代码的关键在于density m.mean().item()用 mask 的均值直接反映道路像素占比。跑完之后如果发现 low density 的 IoU 比 high density 低了 0.2 以上说明模型对稀疏道路的召回不够可以考虑在损失函数里对稀疏样本加权或者用 OHEM在线难例挖掘把稀疏区域的误分类样本挑出来重点训。还有一个实用技巧把测试集里 IoU 最低的 20 张图单独拿出来看。这些图往往暴露了模型的系统性缺陷——比如立交桥区域、隧道出入口、被阴影遮挡的路段。我习惯把这些图存成一个 “bad case” 文件夹每次模型迭代后重新跑一遍看新模型有没有把这些硬骨头啃下来。从那以后我每次训完分割模型都强制走一遍分层评估加 bad case 回看再决定要不要部署。希望帮到你。本文还有配套的精品资源点击获取
