
简介面向大分辨率遥感影像道路分割任务的深度学习数据集基于DeepGlobe-Road数据构建已划分训练集与测试集适配语义分割网络的训练、验证与测试。训练集包含4981张遥感图像及对应精细mask模板测试集包含1245张图像及对应mask前景像素标注准确完整可直接作为道路分割模型评估的标准数据也无需额外整理即可接入主流分割框架。资源包内共2000个文件以jpeg格式的原始影像和标注掩膜为主另附一个Python可视化脚本可随机抽取某一样本将原图、GT图像、GT在原图上蒙板的效果一并展示并保存到当前目录便于快速检查标注质量与数据分布。整个压缩包约336.79MB浏览学习人数已达982适合需要规范数据划分、高质量道路标注的遥感图像分割实验、模型比较与论文复现。1. 遥感影像道路分割数据集别急着训练先检查train/test是怎么切出来的做遥感影像道路分割实验最容易翻车的不是网络结构而是图像分割数据集本身的组织方式。大分辨率遥感影像道路分割数据集单景影像动不动就是4096×4096直接全图塞进网络显然不现实更隐蔽的问题是train/test的划分方式——如果只是把大图随机切碎再随机分配同一段道路很容易同时出现在训练集和测试集里验证IoU虚高得吓人换到新区域马上被打回原形。这篇笔记从数据集读取、裁剪、训练到推理验证把大分辨率场景下的完整用法和踩坑点一次讲清楚。适合正在用公开遥感道路数据集跑分割实验、或者准备自建道路标注数据的从业者新手可按步骤跑通熟手可以直接对照参数和避坑记录。2. 先认识数据集再动手目录结构、像素约定与划分合理性检查数据集拿到手第一步不是开训练脚本而是确认三件事目录结构是否完整、mask的像素值约定是什么、train/test的划分是否干净。很多公开数据集的说明文档只给一句话但这里一旦出错后面所有实验指标都建立在错误前提上。遥感道路数据集的规模差异也很大有的只有几百张512×512小图有的是几十张2万×2万的大瓦片两种数据的处理方式完全不同。动手前花半小时做数据体检比多跑一版模型更值。2.1 目录组织与ID文件train.txt/test.txt到底管什么常见的遥感道路分割数据集长这样images/放原始影像masks/放标注train.txt和test.txt每行一个样本IDID对应两个目录里的同名文件。有的数据集还会多一个val.txt有的则把验证集嵌套在train里让你自己切。第一步先写个脚本把样本数量和类别占比摸清楚这决定了后续采样策略和损失函数怎么选。import os from PIL import Image import numpy as np data_root ./remote_sensing_road train_ids open(f{data_root}/train.txt).read().strip().splitlines() test_ids open(f{data_root}/test.txt).read().strip().splitlines() print(ftrain: {len(train_ids)}, test: {len(test_ids)}) # 抽样统计道路像素占比判断类别不平衡程度 road_pixels 0 total_pixels 0 for idx in train_ids[:20]: mask np.array(Image.open(f{data_root}/masks/{idx}.png)) total_pixels mask.size road_pixels (mask 0).sum() # 用 0 而不是 1防0/255陷阱 print(froad ratio: {road_pixels / total_pixels:.4f})这里有几个容易看走眼的点。train.txt里的ID不带扩展名拼路径时别自作主张加.png或.tif先看目录里实际文件名再拼。读取mask时统一用mask 0转成二值标签不要纠结标注值是1还是255这两种我都见过写死 1会导致统计结果全为0白白浪费时间排查。road ratio如果低于0.05说明道路是极度稀疏类别后续损失函数不能只用CrossEntropy第4章会专门讲。2.2 大分辨率TIF与mask的读取波段、位深和0/255陷阱遥感影像和普通照片有个本质区别它不一定是三通道8bit。很多公开道路数据集的原图是四波段RGB近红外甚至多波段位深可能是uint16。PIL的Image.open对TIF支持有限高分辨率多波段图建议用tifffile读取。uint16的图如果直接除以255像素范围会整体偏移模型输入分布跟预训练权重的预期不一致收敛变慢甚至不收敛。import tifffile # 读取单景大图检查shape和dtype img tifffile.imread(f{data_root}/images/0001.tif) print(img.shape, img.dtype) # 可能输出 (4096, 4096, 4) uint16 # 四波段时取前三个波段作为RGB输入 if img.shape[-1] 4: img img[..., :3] # uint16转uint8用百分位截断再缩放避免直接除以65535导致整体偏暗 lower, upper np.percentile(img, (2, 98)) img np.clip((img - lower) / (upper - lower), 0, 1)百分位截断是遥感影像转可视化的常用做法。直接除以65535往往让图像整体偏暗因为大部分像素堆在低值区间百分位截断相当于做一个全局对比度拉伸保留道路和背景的区分度。如果原数据集已经预处理成8bit RGB则跳过这步。另外注意有些数据的mask也是TIF格式读取方式相同但TIF可能带压缩或tiled存储循环读取几百张时速度会差很多建议一次性转成PNG缓存后面训练时IO压力小一个量级。2.3 检查划分质量同路段泄漏最容易让验证分数虚高这是整个数据集里最值得花时间的检查项。随机切片再随机分配train/test从代码角度没错但从遥感角度是错的一条连续的道路被切成若干块后属于同一物理道路的切片如果落进不同集合模型在测试时等于见过这条路的“邻居”验证分数虚高。等到部署到完全陌生的城区同样的网络结构mIoU可能下跌5~10个点。检查方法有两个第一个是数据来源层面的如果数据集的划分是按文件名前缀随机分的而不是按地理区块或瓦片编号划分就要警惕。def analyze_split(id_list, phase): ratios [] heights, widths [], [] for idx in id_list[:50]: # 抽样检查即可 mask np.array(Image.open(f{data_root}/masks/{idx}.png)) h, w mask.shape heights.append(h) widths.append(w) ratios.append((mask 0).mean()) print(f[{phase}] mean_size{int(np.mean(heights))}x{int(np.mean(widths))}) print(f[{phase}] mean_road_ratio{np.mean(ratios):.4f}) analyze_split(train_ids, train) analyze_split(test_ids, test)这段代码统计两个集合的影像尺寸和道路占比是否一致。如果训练集道路占比0.03、测试集0.15说明测试集偏向了道路密集区域评测出来的precision会偏低召回率虚高跟真实分布脱节。更严重的泄漏靠这个脚本看不出来需要回看数据集的构建说明或人工抽查几张跨集合切片的边缘是否对齐。如果确认数据集划分不干净常见做法是忽略自带的train/test按地理网格或影像原始编号重新划分把同一地理范围的数据放进同一个集合。这一步虽然费时间但换来的验证结果才是可信的。3. 大分辨率影像怎么喂给网络裁剪、增强与Dataset实现大分辨率遥感影像直接训练是行不通的显存放不下网络感受野也覆盖不了全图。滑窗裁剪是标准解法但裁剪参数直接影响训练效果和显存压力。这章把裁剪、增强、Dataset三件事串起来给出一套能直接跑的最小实现。核心原则是训练阶段的裁剪尺寸不必一味求大把全局上下文信息留给推理阶段的重叠拼接去补。3.1 滑动窗口裁剪窗口和stride怎么选裁剪的两个参数是crop_size和stride。crop_size决定网络输入分辨率stride决定相邻patch之间的重叠程度。训练时最常用的组合是crop_size512、stride256也就是重叠50%。重叠太少模型对道路边缘的连续性感知不足重叠太多数据量翻倍训练时间拉长。显存和精度的取舍参考下面这张表。crop_size显存估算batch8, float32适用场景512约8~10GB多数单卡训练的起点稳定768约16~20GB需要更大上下文时的折中1024约24~32GB通常只用于fine-tune或小batch使用大分辨率数据集时我一般会把训练和推理的crop_size分开训练用512或768推理用1024甚至更大配合高重叠的滑动窗口把道路的长距离连续性补回来。写裁剪函数时有一个容易被忽略的边界问题大图的长边经常不能被stride整除如果循环只跑到h - crop_size 1就停图像右侧和下侧会漏掉一条窄带这些窄带里可能恰好有道路。下面的实现把这部分补上了。def slide_crop(img, mask, crop_size512, stride256): h, w img.shape[:2] patches, label_patches [], [] # 标准的从上到下、从左到右滑窗 for y in range(0, h - crop_size 1, stride): for x in range(0, w - crop_size 1, stride): patches.append(img[y:ycrop_size, x:xcrop_size]) label_patches.append(mask[y:ycrop_size, x:xcrop_size]) # 右侧窄带处理从右边缘往左取一个完整窗口 if (w - crop_size) % stride ! 0: x w - crop_size for y in range(0, h - crop_size 1, stride): patches.append(img[y:ycrop_size, x:xcrop_size]) label_patches.append(mask[y:ycrop_size, x:xcrop_size]) # 下侧窄带处理 if (h - crop_size) % stride ! 0: y h - crop_size for x in range(0, w - crop_size 1, stride): patches.append(img[y:ycrop_size, x:xcrop_size]) label_patches.append(mask[y:ycrop_size, x:xcrop_size]) return patches, label_patches注意右侧窄带处理里x固定为w - crop_size此时再配合从左到右的循环两张patch之间没有任何重叠边界处的道路被硬生生切了一刀。如果对边界完整性要求高可以把stride临时调小再补几个窗口但一般没必要右侧和下侧各补一刀已经能保证全图都被覆盖。裁剪后的patch数量可以估算一张4096×4096的图按512/256裁剪大约产生225张patch数据集里如果有一百张大图光训练样本就有两万多张够跑了。3.2 增强策略哪些操作对遥感道路分割有效遥感道路数据和自然图像不一样增强策略要克制。随机水平/垂直翻转、随机旋转90度或90/180/270度整倍数都是安全的因为道路方向本身任意翻转不会破坏语义。亮度、对比度、色调的小幅扰动也建议加不同时相、不同传感器的影像光照差异大适当扰动能让模型更鲁棒。但有两类增强要避开随机擦除Random Erase会把道路区域挖成一块黑洞等于人为制造错误标注大幅度的色彩抖动会让道路与周围裸土、水泥地的边界变得不可分辨模型被迫去拟合噪声。旋转有一个实现细节常规的随机旋转比如旋转30度会引入插值mask边缘会出现非0非1的中间值训练时如果忽略这些值就等于在标签里掺了噪声。所以遥感分割的旋转增强我只用90度的倍数配合numpy的rot90操作标签零损失。另外很多公开数据集的mask边缘本身就有一两像素的标注误差增强后这些误差会被放大训练时不要追求像素级完美重点看整体IoU。3.3 一个能直接跑的Dataset类裁剪、重映射、归一化下面这个Dataset把裁剪、翻转、归一化、标签重映射串在一起。设计上刻意保持了简单方便你在这个骨架上加自己需要的逻辑。import random import numpy as np import torch from torch.utils.data import Dataset class RoadSegDataset(Dataset): def __init__(self, crop_list, crop_size512, augmentTrue): self.crops crop_list # 每个元素是 (img_patch, mask_patch) self.crop_size crop_size self.augment augment def __len__(self): return len(self.crops) def __getitem__(self, i): img, mask self.crops[i] # 如果patch比crop_size大随机裁剪 if img.shape[0] self.crop_size: y random.randint(0, img.shape[0] - self.crop_size) x random.randint(0, img.shape[1] - self.crop_size) img img[y:yself.crop_size, x:xself.crop_size] mask mask[y:yself.crop_size, x:xself.crop_size] if self.augment: if random.random() 0.5: img np.flip(img, axis1) mask np.flip(mask, axis1) if random.random() 0.5: k random.choice([1, 2, 3]) img np.rot90(img, kk, axes(0, 1)) mask np.rot90(mask, kk, axes(0, 1)) # 归一化到[0,1]转CHW标签重映射为0/1 img torch.from_numpy( img.transpose(2, 0, 1).astype(np.float32) / 255.0 ) mask torch.from_numpy((mask 0).astype(np.int64)) return img, mask这里的关键点有三个。第一归一化直接除以255适用于8bit影像如果是uint16且做过百分位截断截断后已经是0~1浮点就不应再除以255。第二mask 0这一步统一了0/1和0/255两种标注约定不管原始mask怎么编码最终标签都是0和1。第三翻转和旋转对img和mask用同一套随机参数np.flip固定axis1表示水平翻转垂直翻转是axis0两者都加会让增强覆盖更全面。需要扩展色彩扰动时在转Tensor之前对img做像素级操作注意保持mask不动。4. 模型与损失函数怎么选从U-Net到Transformer的落地配置数据集准备好了下一步是选网络和损失函数。遥感道路分割的主流方案已经比较成熟卷积网络如U-Net、DeepLabV3仍然是稳定的baselineTransformer类的SegFormer、Swin-UNet在精度上限上更高但对显存和训练时间的要求也上了一个台阶。选型时第一条原则是输入分辨率优先于网络复杂度一个能塞进1024×1024的U-Net往往比被迫降分辨率到512×512的SegFormer更实用因为道路是长条形结构看得远比看得深更重要。4.1 网络结构取舍卷积网络与Transformer的分辨率博弈遥感道路分割的难点在于道路的连续性和细长形态。512×512的窗口里一条主干道可能横跨整个patch网络需要足够的感受野才能理解“这条路的走向”。U-Net的encoder-decoder结构天然适合分割任务DeepLabV3通过空洞卷积扩大感受野这两类在道路场景下依然是性价比最高的选择。Transformer类的优势在于全局建模能力长距离依赖的捕捉能力更强但训练和推理分辨率不一致时性能会明显下降微调时需要重新对齐位置编码。给一个直观建议先用U-Net系跑通整条pipeline得到baseline再换SegFormer做精度上限测试不要一开始就在大模型上调参。显存是更现实的问题。道路数据集crop_size512、batch_size8时U-Net约占用8~10GB很多单卡能跑SegFormer-B2在同样配置下显存占用高出30%~50%往往被迫降低batch_size或crop_size。我的习惯是维护一张实验记录表把每次实验的crop_size、batch_size、显存占用、mIoU记下来对比时才不会凭感觉判断。别忘了crop_size512的SegFormer和crop_size512的U-Net看到的信息量相同模型再强也弥补不了输入信息本身的缺失。4.2 损失函数设计与类别不平衡处理道路像素占比通常只有3%~8%直接用CrossEntropy训练模型会严重偏向背景预测结果里道路大面积漏检。最常见也最有效的做法是CrossEntropy和Dice Loss加权组合。Dice Loss直接优化Dice系数对类别不平衡不敏感适合道路这种稀疏目标。下面给出可直接用的实现。import torch.nn.functional as F def dice_loss(pred, target, smooth1.0): # pred: (N, C, H, W) 网络输出未经过softmax # target: (N, H, W) 标签1表示道路 pred torch.softmax(pred, dim1)[:, 1] # 取道路通道 pred pred.contiguous().view(pred.size(0), -1) target target.contiguous().view(target.size(0), -1) intersection (pred * target).sum(dim1) dice (2. * intersection smooth) / \ (pred.sum(dim1) target.sum(dim1) smooth) return 1 - dice.mean() # 训练时这样组合CE压底 Dice修边 loss F.cross_entropy(logits, label) dice_loss(logits, label)两个实现细节值得注意。smooth常数的默认值取1.0作用是防止分母为0但smooth过大会让梯度变小、训练变慢如果发现损失震荡厉害可以试着降到0.1。另一种可选的损失是Focal Loss它让模型更关注难分类的样本对树荫遮挡、阴影下的道路有一定帮助但它有两个超参数alpha和gamma需要调初期不建议直接上手CEDice组合已经能覆盖多数场景。当道路占比特别低低于2%时可以在采样阶段做负样本过滤保证每个batch里至少有几个patch含道路像素这比单纯调损失函数更直接。4.3 训练参数基准配置从crop_size到学习率的联动关系大分辨率数据集的训练参数和普通分类任务差别很大。核心联动关系是crop_size越大batch_size必须越小学习率也要相应调整。batch_size从16降到4时梯度噪声变大同一学习率下训练更容易震荡通常的做法是固定初始学习率1e-4左右用AdamW优化器配合Poly学习率策略让后期lr缓慢衰减。下面这组配置可以作为起点显存不够时优先降batch_size而不是降crop_size。# 训练配置crop_size512, batch_size8 model UNet(in_channels3, num_classes2).cuda() optimizer torch.optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-4) # Poly策略总步数固定power控制衰减曲线 total_steps len(train_loader) * epochs scheduler torch.optim.lr_scheduler.PolynomialLR( optimizer, total_iterstotal_steps, power0.9 ) # 每轮训练前确认学习率曲线 # 建议每隔10个epoch在验证集上打印mIoU和loss参数说明weight_decay1e-4是分割任务常用值太大容易欠拟合小数据集Poly的power0.9是比较保守的选择前期lr下降平缓后期快速衰减适合长训练。epochs建议从100起步遥感道路数据集经过裁剪后有上万张patch100轮足够看到收敛趋势。如果验证集mIoU到80轮还没动过检查学习率是否被scheduler降得太低或者数据泄漏导致的假性收敛。另一点是预热warmupTransformer类模型建议前10%的步数做线性warmup卷积模型不做影响不大。训练日志里除了loss建议同时记录道路IoU和背景IoU道路IoU才是这个任务的真实指标。5. 遥感道路分割的五个高频翻车点现象、原因与排查第2章到第4章是按正常流程走的这章专门讲我实际跑遥感道路分割数据集时反复踩过的坑。每条按现象、原因、解决的顺序写你可以直接对照自己遇到的情况排查。这些问题分布在数据、指标、采样、特征和推理五个环节每一条都曾让我多花过一两天时间。5.1 训练loss正常下降但验证IoU虚高先怀疑数据泄漏现象训练损失稳步下降验证集mIoU很快涨到0.85以上模型看起来非常“聪明”但把训练好的模型拿到另一批没见过的遥感图上推理输出惨不忍睹mIoU直接跌到0.6以下。原因train/test划分是随机切片做的同一段道路被切成多个patch一部分进train一部分进test模型通过train里的“邻居”像素学会了test里的道路位置本质上是记忆了数据。解决回到数据源头确认划分是否按地理区块或原始影像边界完成如果自带划分不可靠用第2.3节的方法重新划分宁可训练集少一点也要保证物理隔离。5.2 精度显示98%但路根本没分割出来指标用错了现象训练日志里accuracy一直很高模型看起来性能很好可是可视化输出mask道路断断续续大部分道路没被识别出来。原因道路像素占比只有百分之几模型把所有像素预测为背景accuracy照样有95%以上这个指标完全没有参考价值。解决分割任务只看mIoU、Dice、F1不看accuracy。如果你的训练脚本里只打印了accuracy立刻把它改掉改为按类别分别计算IoU重点关注道路IoU。如果道路IoU明显低于背景IoU说明模型在背景上过拟合检查损失函数权重和负样本比例。5.3 patch里全是背景模型学成“全都预测背景”负样本失衡现象训练loss下降正常但验证时预测结果几乎全黑道路完全没有输出。排查发现训练集里大量patch是纯背景图可能是因为滑窗裁剪时整张图的道路区域本来就少抽取出的patch里没有一个含道路像素。原因全背景patch占比过高模型学会“输出全零”这条捷径交叉熵损失下这种策略的loss很低模型没有动力去学道路特征。解决裁剪时统计每个patch是否含道路像素按比例过滤掉一部分全背景patch或保证每个batch里至少混入一定比例含道路的patch。常见的做法是数据加载时单独维护一个“含道路patch索引”和一个“背景patch索引”每个epoch按7:3或8:2的比例混合采样。5.4 阴影和水体被当成了道路特征混淆现象预测结果里道路的IoU尚可但大面积的水域和建筑物阴影也被预测成道路可视化后场景看起来很“脏”。原因水体在遥感影像上呈深色阴影区域也是深色与沥青道路的光谱特征相似如果模型的感受野不足只看到局部纹理就会把深色区域误判为道路。解决第一优先增加特征通道使用RGB近红外四波段输入水体在近红外波段的吸收特性与沥青差别很大其次在推理后处理阶段用形态学操作去除面积过小、长宽比异常的孤立区域如果数据集只有RGB可以考虑用道路的几何先验约束比如道路通常连通且具有一定宽度但这类后处理只治标不治本。5.5 大影像推理拼接出现棋盘格overlap不足与合并方式粗暴现象单张大图推理完成、拼接后图像上出现明显的方块边界道路在patch接缝处断开或产生亮度突变。原因推理时stride设得过大patch之间没有重叠每个patch边缘的感受野不足预测在边界处不稳定或者合并时直接取argmax而不是概率平均造成边界跳变。解决推理阶段把stride降到crop_size的1/4甚至更小让相邻patch有大量重叠每个像素位置会收到多次预测把这些预测的概率值取平均后再取argmax道路类别在边界处的稳定性会明显提升。具体实现见第6章的推理脚本这一条是推理阶段最值得先改的参数。6. 推理与进阶重叠合并、TTA与形态学修边训练和验证做完后真正检验模型的是对一整张大分辨率影像的推理能力。推理脚本的核心是两个参数重叠度和合并策略。我一般把推理stride设为crop_size的1/4例如crop_size512时stride128这样每张patch的边界都会被相邻patch覆盖多次边界处模型“犹豫”的区域可以通过平均概率来缓解。合并时对概率取平均而不是对argmax投票后者在类别边界处容易产生锯齿。下面是一个可用的推理骨架。def predict_full_image(model, image, crop_size512, stride128): h, w image.shape[:2] prob np.zeros((h, w, 2), dtypenp.float32) count np.zeros((h, w, 1), dtypenp.float32) model.eval() with torch.no_grad(): for y in range(0, h - crop_size 1, stride): for x in range(0, w - crop_size 1, stride): patch image[y:ycrop_size, x:xcrop_size] patch_tensor torch.from_numpy( patch.transpose(2, 0, 1).astype(np.float32) / 255.0 ).unsqueeze(0).cuda() out torch.softmax(model(patch_tensor), dim1).cpu().numpy()[0] prob[y:ycrop_size, x:xcrop_size] out.transpose(1, 2, 0) count[y:ycrop_size, x:xcrop_size] 1 prob / np.maximum(count, 1) # 被覆盖次数为0的位置避免除零 return np.argmax(prob, axis-1)TTA测试时增强在道路分割上值得一试推理时把patch做水平翻转和90度旋转分别预测后再把概率映射回原方向取平均计算量增加3~4倍通常能带来1~2个点的IoU提升。形态学后处理方面先做闭运算填补道路内部的细小空洞再用连通域分析去掉面积低于阈值的孤立预测块这一步能明显改善道路的连续性。我自己做这类数据集时养成了一个习惯每次训练完先拿一张没参与训练的整幅大图推理把预测结果叠加到原图上人工看一眼再决定要不要继续调参。这种可视化检查比任何指标都快地暴露泄漏、特征混淆和拼接问题。希望这篇笔记帮你在同样的地方少花点时间。本文还有配套的精品资源点击获取