简介:面向深度学习和遥感图像分析需求,这份资源提供大分辨率遥感影像的道路分割数据集,已按标准划分训练集与测试集。训练集包含4981张原始影像及对应Mask标注,测试集包含1245对,前景标注精细,可直接用于U-Net、DeepLabV3等主流分割模型的训练与精度评估。压缩包内共有2000个文件,以1999张JPEG图像为主,另含1个Python可视化脚本,整体压缩包约336.79MB。图像覆盖遥感原图与道路GT掩膜,便于监督学习;脚本可随机抽取样本,一键生成原图、GT及叠加蒙版三合一效果图,方便直观核验标注质量和模型输出。目前已有982人学习下载,特别适合需要现成高分辨率道路数据的研究者、算法工程师与学生,可省去采集、清洗和划分数据的繁琐流程,专注网络设计与调优。
1. 为什么大分辨率遥感影像道路分割数据集值得单独研究:先想清楚它解决什么问题
想象你手头的遥感影像分辨率到了 0.5 米级别,道路在画面上是一条条清晰的浅色带,可模型一测就碎。问题多半不在网络结构,而在数据集本身:大分辨率影像没有合理的裁剪粒度,训练集和测试集划分不干净,标注语义混着多种约定,这些都是道路分割任务里最常见的翻车点。这篇文章直接用“深度学习之图像分割数据集:大分辨率下的遥感影像道路图像分割数据集(划分了训练集和测试集)”当切入点,拆清楚这类数据集该怎么看、怎么做、怎么避坑。没碰过遥感数据的读者可以照着步骤走,已经跑过几轮训练的读者也能在划分逻辑和参数边界上找到值得回头检查的地方。
2. 这类数据集的数据组织方式:标注语义、大分辨率粒度与训练/测试划分的底层逻辑
2.1 道路分割到底分什么:三种标注约定直接影响模型行为
道路分割在遥感任务里通常被建模为二分类语义分割,每个像素要么是道路,要么是背景。但“道路”这个语义并没有想象中统一。常见标注约定有三种:只标路面、标路面加路肩、以及把道路中心线缓冲成面。大多数公开遥感道路数据集的默认约定是“正射视角下可见的路面”,不包含被树冠遮挡的部分,也不包含建筑出入口延伸到院内的区域。这个约定直接决定了掩码的边界形状,换了一套约定,指标对比就没有意义。
我拿到一个数据集后,第一件事就是随手抽几张影像和掩码叠加看,确认它用的是哪一种约定。只标路面的掩码边缘和路面灰度变化贴合紧密,中心线缓冲出来的掩码会明显把路口转角画成圆角,路肩包含进来的掩码往往比可见路面宽出两到三个像素。这个判断并不需要专业知识,肉眼对比就能看出来,但它决定了后续所有评估结论的口径。另一个容易被忽略的维度是类别不平衡:普通城市遥感影像中道路像素通常只占 5% 到 15%,郊区和林区甚至会低于 3%。这会直接影响损失函数选择、训练集划分策略以及评估指标的解释方式,后面我会专门展开。
2.2 大分辨率到底指多大:影像尺寸、地面采样距离与裁剪窗口的三角关系
“大分辨率”这个词在遥感语境里经常被误解。深度学习里一张 1024×1024 的图已经算大,但遥感影像单景常是 12000×12000 起步。真正参与训练时,显存和网络结构决定了输入尺寸只能落在 256 到 1024 之间,那中间这段差距怎么补?靠滑动窗口裁剪,而裁剪的粒度由两个因素决定:目标网络输入尺寸和目标地面采样距离。
地面采样距离(GSD)是每个像素对应的实际地面距离,0.3 米 GSD 的影像里一条乡村土路能占到十几个像素宽,2 米 GSD 的影像里同一段路只剩三四个像素。所以裁剪窗口固定时,窗口内道路的形态完全由 GSD 决定。这个参数不统一的数据集,训练时模型会在“宽路”和“窄路”两种形态之间反复横跳,最终收敛到一个谁都不得罪的中间状态。我见过一个数据集把 0.5 米和 1 米 GSD 的影像混在同一目录下,训练集里 0.5 米的占八成,测试集里 1 米的居多,最后的报告显示验证集指标很好,部署时全碎。查看数据集时,先看元信息里有没有给出统一的 GSD,没有的话就自己从影像的地理范围反推。
裁剪粒度的经验值我一般这样取:训练窗口 512 或 640,滑动步长设为窗口的 75% 到 87.5%,也就是 overlap 64 到 128 像素。过大窗口会让显存压力陡增,过小窗口又会让道路在局部看起来像块状纹理,模型很难学到线性连续性。预测阶段可以用更大窗口加 overlap 平均,因为不再需要反向传播。
2.3 训练集和测试集划分的底层逻辑:为什么空间重叠比样本数量更致命
“划分了训练集和测试集”这句话看起来普通,但遥感影像的划分和普通图像分类划分有本质区别。分类任务追求类别分布一致,遥感分割任务的核心约束是空间不重叠。如果原始大图只有两三张,常见的省事做法是对大图分块后把所有 patch 随机洗牌再分配,训练集和测试集里会出现大量相邻甚至重叠的 patch,两张图的差异只有几十个像素的平移。这种划分下,测试集指标虚高得离谱,模型换到新地形上立刻漏出原形。
正确的划分基准是空间范围:先划定训练区域和测试区域,再在各自区域内裁剪。训练区与测试区之间要有明确的几何边界,比如按经纬度多边形切分、按地块编号切分,或者按单景影像切分。只有一张原始影像时,宁可让训练集小一些,也要保证测试区域与训练区域在地理上分离。一个划分合理的数据集,通常会随数据提供每个 patch 的起始行列号或者原始影像编号,方便使用者回溯空间归属。没有这些元信息的“已划分”数据集,可信度要打个问号。这类空间泄漏问题在训练第 4 章第 3 节还会再次出现,因为即使你拿到的划分没问题,自己二次调整时也可能亲手把泄漏引进来。
3. 从原始影像制作划分好的道路分割数据集:可复现的完整流程
3.1 数据准备:统一 GSD、统一像素尺寸、建立索引表
假设你手里有几景原始影像,可能是带坐标的 GeoTIFF,也可能是普通 PNG 加同名掩码。第一步是把所有影像统一到同一个 GSD 和像素尺寸上。这一步常见做法是栅格重采样,把不同来源的数据对齐到目标分辨率。下面这段脚本用栅格库完成重采样,并保留地理变换信息,便于后续做矢量栅格化对齐。
import rasterio from rasterio.enums import Resampling src_path = "area_12.tif" dst_path = "area_12_unified.tif" target_gsd = 0.5 # 目标地面采样距离,单位:米/像素 with rasterio.open(src_path) as src: scale_x = src.res[0] / target_gsd scale_y = src.res[1] / target_gsd new_width = int(src.width / scale_x) new_height = int(src.height / scale_y) # 用缩放比例更新仿射变换,保持影像地理范围不变 transform = src.transform * src.transform.scale( (src.width / new_width), (src.height / new_height) ) profile = src.profile profile.update(width=new_width, height=new_height, transform=transform) with rasterio.open(dst_path, "w", **profile) as dst: for band in range(1, src.count + 1): data = src.read( band, out_shape=(new_height, new_width), resampling=Resampling.bilinear ) dst.write(data, band)这段代码的核心在于scale和transform的处理:它是按目标 GSD 反推缩放系数,而不是直接把尺寸硬顶到某个值。这样做的好处是影像的地理覆盖范围不变,后续所有 patch 的坐标都能追溯。
重采样算法上,多光谱影像用双线性足够,近红外波段做归一化植被指数时可以考虑三次卷积;但掩码保存时务必要用最近邻,不然道路边缘会被插值成模糊过渡带,二值化以后出现一圈假像素。统一尺寸后,还建议顺手做一遍像素值统计,不同批次的影像灰度分布差异过大的话,需要做直方图匹配或者归一化,避免模型把不同传感器的色调差异当成语义差异。最后把这些元信息整理成一张索引表,记录影像编号、GSD、行列号、像素值范围,这一步能减少后续很多排查时间。
3.2 掩码生成与二值化:矢量栅格化时最容易踩的对齐问题
道路掩码的来源通常有两种:已有地理信息矢量栅格化,或者人工在影像上逐像素勾勒。人工勾勒质量高但成本大,矢量栅格化速度快,但坐标对齐问题非常容易让掩码整体偏移。下面是用矢量面状道路数据生成掩码的脚本。
import geopandas as gpd from rasterio.features import rasterize # 假设已经准备好了统一后的影像,transform 来自该影像 roads = gpd.read_file("road.shp") road_geoms = [geom for geom in roads.geometry if geom is not None] # all_touched 控制边界像素的判定方式:True 表示只要碰到就算 mask = rasterize( shapes=[(geom, 1) for geom in road_geoms], out_shape=(new_height, new_width), transform=transform, fill=0, all_touched=True, dtype=np.uint8 )栅格化脚本里有三个参数值得反复检查:transform必须是最终保存影像的transform,哪怕只差一个像素,掩码整体就会偏移;all_touched设为 True 时道路边界会往外扩一点,设为 False 时由像素中心点判定,道路会略细,这个选择要和模型评估指标的口径统一;dtype用uint8即可,二值掩码不需要更高的位深。
另外,很多人会把掩码和影像分别裁剪后再检查对齐,这是一种低效做法。正确的验证方式是先把掩码栅格化到整幅大图尺度,随后和影像一起按同一窗口裁剪,从像素层面保证二者严格对齐。人工标注路径下,我习惯把影像放大到 300% 到 400% 再沿着路面边缘描边,背景类要顺手把阴影里能看见的路段标出来,否则模型会以为“带有阴影纹理的区域不是道路”。
3.3 按空间区域切分训练集和测试集:一个带防泄漏逻辑的划分脚本
下面这个脚本演示的是单张大图内按区域划分后再裁剪。实际工程中如果有多个影像文件,最优做法是按图编号划分,图与图之间的划分区域天然独立,代码逻辑完全一致。
import random import numpy as np import rasterio from rasterio.windows import Window from pathlib import Path src_path = "area_12_unified.tif" mask_path = "area_12_mask.tif" patch_size = 512 overlap = 64 out_dir = Path("road_split") random.seed(42) with rasterio.open(src_path) as src, rasterio.open(mask_path) as mask: h, w = src.height, src.width # 只演示单张大图:上半部分作为训练区域,下半部分作为测试区域 train_region = (0, h // 2, 0, w) test_region = (h // 2, h, 0, w) def crop_region(region, out_folder, prefix): r0, r1, c0, c1 = region idx = 0 for row in range(r0, r1 - patch_size, patch_size - overlap): for col in range(c0, c1 - patch_size, patch_size - overlap): win = Window(col, row, patch_size, patch_size) img = src.read([1, 2, 3], window=win).transpose(1, 2, 0) lab = mask.read(1, window=win) # 低信息量 patch 直接跳过,避免训练集被纯背景淹没 if np.sum(lab) < 50: continue out_path = out_folder / f"{prefix}_{idx:05d}.npz" np.savez_compressed(out_path, image=img, label=lab) idx += 1 crop_region(train_region, out_dir / "train", "train") crop_region(test_region, out_dir / "test", "test")这段脚本的防泄漏逻辑体现在划分发生在裁剪之前:训练 patch 全部来自上半区域,测试 patch 全部来自下半区域。两个区域的 patch 可能在几何边界处仍有视觉连续性,但这是因为真实道路本身是跨区域的,模型在测试时遇到了和训练区域相邻的地块,这正是遥感任务要面对的真实分布。
参数上,overlap控制相邻 patch 的重叠程度。训练时 overlap 设 64 到 128 就够,重叠更多只能增加样本间相关性,对多样性有害;测试时为了做预测融合,可以把 overlap 提升到 128 甚至 256。过滤阈值 50 是针对 512×512 patch 和 0.5 米 GSD 的经验值,换算成道路像素占比约 0.02%。如果你做的是 1 米 GSD 的赛区,阈值可以提高到 200;但测试集永远不要做这种过滤,因为真实推理时你不知道哪里会有路。
4. 用这套数据训练分割模型:数据管线与损失函数的参数细节
4.1 Dataset 类里必须对齐的通道顺序、像素范围和标签 dtype
拿到划分好的数据集,最容易出问题的其实是加载器里的小细节,而不是网络结构。遥感影像常见的存储格式是 GeoTIFF,里面波段顺序可能是 R、G、B,也可能是 R、G、B、NIR,还有可能是 BGR。用普通图像库读取 PNG 时,还要确认通道顺序是否被隐式翻转。下面是一个通用的 Dataset 实现。
import numpy as np import torch from torch.utils.data import Dataset from pathlib import Path class RoadDataset(Dataset): def __init__(self, folder, normalize=True, augment=False): self.files = sorted(Path(folder).glob("*.npz")) self.normalize = normalize self.augment = augment def __len__(self): return len(self.files) def __getitem__(self, idx): data = np.load(self.files[idx]) img = data["image"].astype(np.float32) label_raw = data["label"].astype(np.int64) # 遥感影像可能带近红外等扩展波段,训练只用前三个波段 if img.ndim == 3 and img.shape[2] >= 4: img = img[:, :, :3] # 标准化到 0-1 区间 if self.normalize: img /= 255.0 img = img.transpose(2, 0, 1) # HWC -> CHW # 标签归一化:很多数据集把道路像素标成 255,这里统一成 1 label = (label_raw > 0).to(torch.int64) return torch.from_numpy(img), label这个加载器的关键点有三个:第一,label_raw先转成int64再做二值化,避免在.npz里存了uint8掩码时被CrossEntropyLoss拒绝;第二,标签先把大于 0 的像素统一归为道路类,这样无论数据集用的是 1 还是 255 都不会出错;第三,transpose必须在转 tensor 之前完成,如果先转 tensor 再转置要么报错要么白转。
注意:如果你手头的数据是 PNG 文件对而不是 npz,加载时要在
__init__里通过文件名把影像和掩码配对,并提前校验缺失文件。避免训练到一半因为某个文件读不出来而中断。
4.2 数据增强在遥感道路上的取舍:翻转、旋转、亮度扰动要克制
遥感影像没有绝对的“上下”方向,因为传感器是从头顶拍摄的,这为数据增强提供了很大空间。但不同增强操作的收益差异不小。随机翻转和 90 度倍数旋转基本无损,能直接扩大数据多样性;任意角度旋转会产生边缘空白区,处理不好会引入黑边伪影;亮度对比度扰动要谨慎,幅度过大会让沥青和水泥地的区分度变得不可靠。
| 增强操作 | 推荐配置 | 注意点 |
|---|---|---|
| 水平/垂直翻转 | 概率 0.5 | 道路方向无关,翻转安全 |
| 90 度旋转 | k 取 1/2/3,概率 0.3 | 配合翻转等价于 8 方向增广 |
| 任意角度旋转 | 避免或做边缘填充 | 黑边会被模型当成背景特征 |
| 亮度扰动 | ±0.1(归一化后) | 过大容易混淆道路材质 |
| 随机裁剪 | 保证道路占比下限 | 裁剪需先检查 mask 非空 |
下面是一个推荐增强函数,旋转只用 90 度倍数,亮度扰动幅度控制在 ±0.1。
import random def augment(img, label): if random.random() < 0.5: img = np.flip(img, axis=0) label = np.flip(label, axis=0) if random.random() < 0.5: img = np.flip(img, axis=1) label = np.flip(label, axis=1) if random.random() < 0.3: k = random.choice([1, 2, 3]) img = np.rot90(img, k, axes=(0, 1)) label = np.rot90(label, k, axes=(0, 1)) if random.random() < 0.5: img += random.uniform(-0.1, 0.1) img = np.clip(img, 0.0, 1.0) return img, label这里容易踩的坑是np.rot90的旋转方向和 k 的含义。它每次只转 90 度,k 参数决定转几次,使用时要注意图像和标签必须用同一个 k,否则训练标签和图像错位之后指标再高也是自欺欺人。亮度扰动我限制在 ±0.1,因为遥感道路分割里,道路和旁边裸露土地的灰度差异本来就小,扰动过大模型会把颜色相近的裸地学成道路。
4.3 训练时让损失函数关注道路边界和类别不平衡
道路像素占比低,普通交叉熵收敛后模型容易输出全背景。常见做法是交叉熵加上 Dice 系数作为辅助损失。几乎所有人第一次跑遥感分割都会遇到这个问题:不加 Dice 时损失下降得很快,但验证集的 IoU 卡在 40% 上不去;加了 Dice 以后,训练曲线变得曲折,但最终 IoU 能明显改善。
class DiceLoss(torch.nn.Module): def __init__(self, smooth=1e-5): super().__init__() self.smooth = smooth def forward(self, pred, target): # pred 为 softmax 后概率图 [B, C, H, W] # target 为 long tensor [B, H, W],道路像素为 1 pred = pred.permute(0, 2, 3, 1).contiguous().view(-1, 2) target = target.view(-1) pred_road = pred[:, 1] target_bin = (target == 1).float() inter = (pred_road * target_bin).sum() union = pred_road.sum() + target_bin.sum() dice = (2.0 * inter + self.smooth) / (union + self.smooth) return 1.0 - diceDiceLoss 实现本身简单,但有两个参数会直接影响训练稳定性。smooth设为 1e-5 在单卡 float32 下足够避免除零,设大了会让 dice 值失真。另一个是概率阈值,训练时我们默认把 softmax 输出中大于 0.5 的类别判为道路,但在最终推理时,遥感道路分割往往需要调低阈值,比如 0.4 甚至 0.35,因为漏检一条路的代价比多预测一小块空地高,业务上通常希望召回优先。
交叉熵和 Dice 的权重比,我常用的起点是 0.7 : 0.3。如果数据里道路占比特别低,可以把 Dice 权重提到 0.5,但要盯着训练集精度曲线,权重过高模型会过度放大道路类,背景误检率上升。训练时的一个小技巧是:先用普通交叉熵跑 10 个 epoch,再加 DiceLoss 联合训,这样可以避免模型一开始被类别不平衡带偏,Dice 部分像是一个精调的辅助把手。
5. 大分辨率遥感道路分割避坑清单:五个高频翻车现场与排查路径
5.1 现象:验证集 IoU 很高,换到新影像上全碎
这类情况在遥感道路分割里占据新手踩坑的头号位置。表现是训练日志里验证集 IoU 已经到 0.75,可是把模型放在一张全新区域的影像上,道路预测要么整段消失,要么把大片农田当成了路。原因基本就两个:一是数据划分存在空间泄漏,测试 patch 和训练 patch 来自同一片区域,模型看到的“新样本”其实在训练时见过长相几乎一致的邻居;二是训练集地表覆盖类型太单一,比如只有城市建成区,没有林地、农田、裸地这些背景。
排查路径很清楚:把预测图和真实掩码叠加显示,如果错误集中在边界处,那多半是标注噪声或数据泄漏;如果整段道路消失或大片误检,那是分布覆盖问题。前者要回溯划分脚本,后者要补充数据或做地域层面的增广。不要急着换网络结构和调 loss,数据分布问题在模型层面怎么调都是隔靴搔痒。
5.2 现象:测试集上类别正确但道路断裂严重
道路断裂表现为本来连续的公路被预测成一段一段的虚线。根本原因是模型的感受野不足以在 patch 内建立长距离连接。512×512 窗口在 0.5 米 GSD 下只覆盖 256 米见方,一条主干道可能横跨整个窗口,模型看到了路面的局部纹理却看不到整条路的走向,断掉就成了必然。
这类问题有一个不算玄学的解决手段:推理时用重叠滑窗加平均预测。滑窗步长设为 128,每个像素会被多个窗口覆盖,取道路类概率的平均值再阈值化,连续性会明显改善。另外可以试试把预测输入放大一级,比如下采样整张大图得到低分辨率全局视图,再和局部窗口预测融合。低分辨率视图提供全局走向,高分辨率窗口提供路面细节,两者互补的效果在这类任务上立竿见影。
5.3 现象:显存不够直接 OOM,训练中断
这种场景很常见:输入尺寸是 512,batch size 调到 4 都放不下。我的建议是不要一味缩小 batch size,小 batch 会让 BatchNorm 统计量波动很大,训练稳定性变差。排查顺序是:先确认是不是模型中间特征图过大,可以通过逐步减少输入尺寸来定位;再用混合精度训练,显存压力通常能降一半。
如果这些都试过还不够,就把训练窗口和推理窗口解耦。训练用 384 或 448,推理用 512 加重叠滑窗。这样做会牺牲一点训练时的感受野,但遥感道路分割对高分辨率细节的依赖高于对大感受野的依赖,退一步往往比强行迭代大模型更划算。要特别注意,改小输入后要相应调低学习率或做 warmup,否则模型会更容易过拟合。
5.4 现象:训练曲线正常但掩码可视化出现系统性错位
预测的道路边界整体比真实道路偏移几个像素,而且偏移方向一致,这是典型的坐标系错位问题。根源通常在数据制作阶段:原始影像和标注矢量用了不同的投影基准,或者栅格化时transform拿错了。这类错位用数据增强、调损失函数都解决不了,模型看到的是“路在真实位置的旁边”,怎么学都对不齐。
排查方式非常直接,把影像和标签以 0.5 的透明度叠加显示,放大到 200%。如果道路边缘的两层轮廓完全平行但不重合,那就是坐标层的问题。解决办法是回到 3.1 和 3.2 的步骤,检查transform来源和重采样方式,重新生成掩码。这类问题一旦发生,训练时间越长浪费越大,所以在划分数据之前做一次叠加检查,比训练完再回头看要省得多。
5.5 现象:划分脚本执行完,训练集和测试集的 patch 高度相似
如果你拿到的“已划分”数据集是用随机洗牌方式生成的,那测试集里很可能混着训练集的近亲。识别方法可以做一个抽样检查:从测试集随机取一个 patch,和训练集所有 patch 做像素差,如果最小差值几乎到零,说明至少存在重复或重叠区域。
避免这个问题的唯一可靠方案就是按空间区域划分。只有一张影像时,把上半部分和下半部分分开是最粗粒度的切法;也可以按照道路网、河流、行政区边界做更复杂的多边形切分,但核心原则只有一个:任何两个属于不同集合的 patch,它们对应的地面区域不能有交集。如果你的业务场景连一张影像也没有,只有若干小块数据,那宁可加大测试集覆盖范围,也别为了凑训练样本把同一区域拆进两个集合。空间连续性远比样本数量更重要。
6. 用半小时验证一个陌生道路分割数据集:动手前的快速体检
6.1 统计道路像素占比,检查数据集是否“虚胖”
很多数据集表面上有几万张 patch,实际道路像素少得可怜。拿到数据后先跑一个统计脚本,把每个 patch 的道路像素占比算出来,看分布。
import numpy as np from pathlib import Path folder = Path("road_split/train") ratios = [] for f in folder.glob("*.npz"): d = np.load(f) label = d["label"] ratios.append(np.sum(label > 0) / label.size) ratios = np.array(ratios) print("min :", ratios.min()) print("median :", np.median(ratios)) print("max :", ratios.max()) print("道路为 0 的 patch 数量:", np.sum(ratios == 0))如果大量 patch 的道路占比为 0,训练时这些样本只能提供背景语义,对道路特征学习没有贡献。正常情况下一个划分合理的道路分割数据集,非空 patch 的比例应该超过 80%,训练集的道路像素占比中位数不应该低于 2%。低于这个值,说明要么裁剪策略太粗暴,要么数据本身标注稀疏,训练时就要靠增强和损失权重去补。
6.2 用一个小网络快速验证数据有没有训练潜力
我的习惯是先不加载完整训练集,而是从测试集取 200 个 patch,再从训练集取 200 个 patch,用一个轻量分割网络跑 30 个 epoch。如果验证集 IoU 在前 10 个 epoch 内能超过 0.3,说明数据标注和划分没有大问题;如果始终在 0.1 附近徘徊,先别怀疑网络,回头查标注和划分。这 30 个 epoch 的代价极小,但能过滤掉大部分数据质量陷阱,比直接开完整训练再发现数据有问题划算得多。
这个方法也适合验证“已划分数据集”是否真的可以放心使用。我之前拿到一份来源模糊的数据,训练集指标一直正常,用 200 张快速子集验证时发现测试集里有三张 patch 和训练集几乎一模一样。重新按空间区域划分后,模型最终指标掉了十几个点,但换到新区域上的表现反而明显变好。数据划分的干净程度,决定了你的算法评测是自欺欺人还是真实可靠。这是我在数据集准备上最重视的一条经验,希望帮到你。
本文还有配套的精品资源,点击获取