简介:本资源面向深度学习图像分割方向的学习者与研究者,提供大分辨率遥感影像道路提取任务的完整数据集,适合用于分割网络的训练、测试与效果验证。数据集已预先划分训练集与测试集:训练集包含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 对训练样本怎么读进 DataLoader
2.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, split='train', img_size=512): 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), interpolation=T.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', split='train', img_size=512) train_loader = DataLoader(train_ds, batch_size=8, shuffle=True, num_workers=4) print(f'训练集样本数: {len(train_ds)}') # 应为 4981逻辑说明:img_names & mask_names取交集是关键一步,防止某个目录多出或缺失文件导致索引错位。mask 的 resize 必须用NEAREST插值,用双线性会把二值边缘插出灰色像素,二值化后边缘反而更毛糙。归一化参数用 ImageNet 的标准值,如果你从零训练可以改成数据集自身的均值方差,但用预训练 backbone 时保持一致更稳妥。
参数说明:img_size控制输出分辨率,512 是速度和精度的折中点;batch_size=8在 8GB 显存下跑 U-Net 比较安全,显存够可以往上加;num_workers=4根据 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_weight=0.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_weight=0.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 用 Adam,lr=1e-3,快速把 loss 降下来;后 80 个 epoch 切 SGD,lr=1e-2 配 cosine 退火到 1e-5。如果不想折腾,直接 Adam + lr=1e-4 + cosine 也能跑出可用的结果。
from torch.optim import Adam from torch.optim.lr_scheduler import CosineAnnealingLR model = YourSegModel() optimizer = Adam(model.parameters(), lr=1e-4, weight_decay=1e-4) scheduler = CosineAnnealingLR(optimizer, T_max=100, eta_min=1e-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_decay=1e-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_size=1 仍然 OOM。原因:输入分辨率设成了 1024 或原图尺寸,U-Net 的 skip connection 在浅层特征图很大。解决:把img_size降到 512 或 384,或者改用轻量 backbone(如 MobileNetV2)替换 ResNet50。如果必须用大分辨率,可以试试梯度累积:batch_size=1累积 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, device='cuda'): 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 回看,再决定要不要部署。希望帮到你。
本文还有配套的精品资源,点击获取