遥感影像道路分割数据集处理:切片、划分与训练避坑指南
发布时间:2026/9/28 5:37:54来源:尧图网络
简介遥感影像道路分割数据集基于DeepGlobe Road Dataset整理已划分好训练集与测试集适合深度学习图像分割方向的算法验证、模型调参与基准测试。训练集含4981张图片及对应mask测试集含1245张图片及对应mask前景像素标注质量较高可直接用于分割网络训练与评估也能作为大分辨率遥感场景下的泛化能力测试数据。包体共2000个文件以jpeg格式保存原始影像与标签图目录按images和masks分列结构清晰另附一个Python可视化脚本可随机抽取样本并叠加展示原图、GT与蒙版效果便于快速检查标注质量。资源整体约336.79MB7z压缩包形式。目前已有977人学习使用。对于需要高分辨率遥感道路数据、希望省去数据收集与划分工作的研究者和开发者这份资源能提供规范的数据组织方式和现成的可视化工具显著缩短数据准备周期帮助快速开展分割实验与效果对比。1. 大分辨率遥感影像道路分割数据集的用法决定模型上限拿到一份已经划分好训练集和测试集的遥感道路图像分割数据集不代表可以直接开训。大分辨率遥感影像与自然图像数据集有本质差异一幅图动辄上万像素道路在整幅图中往往只占不到 10% 的像素训练集和测试集如果不是按场景划分而是按切片随机切最后得出的指标基本是自欺欺人。很多人把模型效果差归咎于网络结构真正卡住进度的往往是数据加载、切片策略、划分逻辑和评估方式这几件事。下面这些内容会围绕这套数据集的读取、切片、划分、训练和评估走一遍完整流程适合正在做遥感图像分割或者刚拿到这类数据集准备入门的从业者。2. 拆解数据集的底层构成格式、标签语义与目录组织先说结论拿到数据集的第一天不要急着写模型先花半天把数据读进内存、可视化几张图、统计标签分布。这个习惯能帮你省掉后面几周的调试时间。遥感影像数据集的坑有一大半在读图和标签阶段就埋下了等训练跑起来再回头查格式问题代价要高得多。2.1 文件格式与读取工具先分清 TIFF、PNG 和位深遥感影像的常见格式是 GeoTIFF也就是带地理坐标的 TIFF。单幅影像可能是几千乘几千像素3 个波段或 4 个波段RGB 加近红外。普通图像分割数据集里常见的是 PNG 和 JPG而遥感道路数据集大多用 TIFF 存原图、PNG 存标签因为 PNG 是无损压缩标签经不起 JPEG 那种有损压缩造成的边缘污染。import rasterio from PIL import Image import numpy as np image_path dataset/train/images/scene_001.tif mask_path dataset/train/masks/scene_001.png with rasterio.open(image_path) as src: img src.read() # 形状 (C, H, W) print(波段数:, src.count, 尺寸:, src.height, x, src.width) print(数据类型:, src.dtypes, CRS:, src.crs) mask np.array(Image.open(mask_path)) print(标签值域:, np.unique(mask))这里用 rasterio 读 GeoTIFF 是标准做法它对大图支持窗口读取不会一次性把整幅载入内存这也是大分辨率影像和普通图像在工程实现上最大的区别。print 的 src.dtypes 会暴露常见的第一个坑很多遥感原图是 uint16 存储按 uint8 直接显示会变成一片黑。标签用 PIL 读没问题但遇到 16bit PNG 时返回的 dtype 是 uint16 而不是 uint8后续计算要统一转换。参数说明src.count 代表波段数3 就按 RGB 处理如果是 4近红外波段对道路分割很有价值因为道路和裸土在近红外波段上差异更明显但网络输入通道要相应改成 4。src.crs 存在说明影像有地理坐标后续按区域划分训练集和测试集时可以用上。2.2 标签语义0、1、255 分别是什么道路分割数据集的标签语义看似简单不同发布者的定义不完全一样。最常见的是 0 背景、1 道路有些数据集在道路边缘标注了不确定区域用 255 表示忽略极少数会把背景写 255、道路写 0或者背景 0、道路 255。import numpy as np mask np.array(Image.open(dataset/train/masks/scene_001.png)) unique, counts np.unique(mask, return_countsTrue) for v, c in zip(unique, counts): print(f像素值 {v}: 占比 {c / mask.size * 100:.2f}%)这一步必须做。如果发现 255不要强行归成背景或道路正确做法是在损失函数里设 ignore_index255让这些像素不参与梯度计算。我曾见过有人把所有非 0 像素都当成道路结果模型在边缘不确定区域反复震荡训练 loss 一直下不去。道路标注的宽度通常只有几个到十几个像素边缘稍微错位几个像素IoU 就会掉好几个点所以边缘语义必须较真。补充一点这类数据集很多是从公开渠道下载的原始影像自己标注或半自动生成的不同来源的标签规范差异很大。拿到手先统计值域再决定归一化方式比对着 README 猜要可靠。2.3 目录组织与命名规则先画一张数据地图常见组织方式是 train/images、train/masks、test/images、test/masks文件名一一对应。但大分辨率数据集有时会在 images 下按场景分子目录mask 放在另一个平铺目录里比如 train/images/scene_001_00.tif 对应 train/masks/scene_001_00.png。先用脚本把全部文件名拉出来核对一遍。from pathlib import Path root Path(dataset) for split in [train, test]: img_files sorted((root / split / images).glob(*.tif)) mask_files sorted((root / split / masks).glob(*.png)) img_stems {f.stem for f in img_files} mask_stems {f.stem for f in mask_files} print(split, 影像数:, len(img_files), 标签数:, len(mask_files)) print(缺失标签的影像:, img_stems - mask_stems) print(多余标签:, mask_stems - img_stems)这一步会暴露两类问题一是文件名不匹配二是部分影像没有对应标签。出现不匹配时先核对是不是子目录结构造成的 stem 不一致比如影像叫 scene_001_00.tif、标签叫 scene001_00.png看起来是同一个场景却不是同一个名字。遇到这种情况直接重命名对齐别等 DataLoader 运行到一半才报 FileNotFoundError。目录核对完之后再统计一遍所有影像的尺寸分布。如果训练集里混着 1024×1024 和 4096×4096 两种尺寸后面切片步长、归一化参数都要跟着调整最怕的是测试集尺寸和训练集不一致预处理逻辑会在评估时静默出错。from collections import Counter size_counter Counter() for split in [train, test]: for img_file in (root / split / images).glob(*.tif): with rasterio.open(img_file) as src: size_counter[(src.height, src.width)] 1 for size, cnt in size_counter.most_common(5): print(size, cnt)2.4 可视化验证原图与标签叠加一眼看出坐标是否对齐格式、命名、值域都检查完之后还需要做一次可视化确认。把原图、标签、叠加图并排打印出来这一步能同时发现位深异常、标签值异义和坐标偏移三类问题。import matplotlib.pyplot as plt import numpy as np from PIL import Image import rasterio def visualize_pair(image_path, mask_path): with rasterio.open(image_path) as src: img src.read([1, 2, 3]).transpose(1, 2, 0) if img.dtype np.uint16: img (img / 257).astype(np.uint8) # 16bit 转 8bit mask np.array(Image.open(mask_path)) if set(np.unique(mask)) {0, 255}: mask (mask 255).astype(np.uint8) plt.figure(figsize(14, 4)) plt.subplot(1, 3, 1) plt.imshow(img) plt.title(image) plt.subplot(1, 3, 2) plt.imshow(mask, cmapgray) plt.title(mask) plt.subplot(1, 3, 3) plt.imshow(img) plt.imshow(mask, cmapReds, alpha0.4) plt.title(overlay) plt.show()叠加之后如果道路没有沿着像素边缘对应而是整体偏移了几个像素甚至几十个像素说明影像和 mask 之间存在坐标偏移。这在人工标注的数据集里并不少见必须在预处理阶段校正不能拖到训练时让网络自己学。3. 把大图切成训练样本滑窗切片与数据增强的落地代码遥感影像的单幅图太大没法像自然图像那样整张喂进网络。公开数据集里常见的做法是先把大图切成固定大小的切片再进入训练流程。切片这一步做得好不好直接影响模型能不能看到完整的道路结构和上下文。3.1 滑窗切片窗口大小与步长的选择逻辑窗口大小最常见的是 512×512 和 256×256。512 更适合 U-Net、DeepLabV3 这类常见分割网络的输入尺寸显存占用和感受野比较均衡256 对细窄道路更友好因为道路在窗口里的像素占比更高但窗口之外的上下文信息丢失较多。道路是细长结构横跨窗口边界是常态我一般用 512 做主训练尺寸。步长决定了切片总数和数据冗余。stride 等于窗口大小代表无重叠数据量最小stride 取窗口一半比如 512 窗口配 256 步长代表 50% 重叠切片数量约为无重叠的 4 倍。遥感道路分割场景下我通常选 50% 重叠因为道路跨越窗口边界时无重叠切片会直接把路切断模型看到的都是半截路推理时边界断裂问题会非常明显。窗口大小步长重叠率4096×4096 影像切片数适用场景5125120%64快速试跑、资源紧张51225650%225推荐兼顾效果与数据量51212875%841追求边界连续性时间充裕除了窗口和步长还有一个必须处理的细节遥感影像四周经常有黑色边框、云层遮挡或纯背景区域。这些区域切成块后没有道路信息大量喂给网络会让模型偏向预测背景拖慢收敛。3.2 节的脚本会一并处理。3.2 影像与标签同步裁剪一段可以直接用的切片脚本切片脚本的核心要求有两个影像和标签必须按同一个窗口裁剪不能各切各的大图不能整体读进内存要用窗口读取。import rasterio from rasterio.windows import Window import numpy as np import cv2 from pathlib import Path def crop_scene(image_path, mask_path, out_root, win_size512, stride256): 把一幅大图和对应标签切成带重叠的小块。 win_size: 切片边长遥感分割常用 512 stride: 滑窗步长256 表示 50% 重叠 out_root Path(out_root) (out_root / images).mkdir(parentsTrue, exist_okTrue) (out_root / masks).mkdir(parentsTrue, exist_okTrue) name Path(image_path).stem count 0 with rasterio.open(image_path) as src_img, rasterio.open(mask_path) as src_mask: h, w src_img.height, src_img.width for top in range(0, h - win_size 1, stride): for left in range(0, w - win_size 1, stride): window Window(left, top, win_size, win_size) img src_img.read(windowwindow) # (C, H, W) msk src_mask.read(windowwindow) # (1, H, W) img np.transpose(img, (1, 2, 0)) # 转 HWC msk np.squeeze(msk, axis0) # 转 HW # 过滤纯背景块没有道路像素的块没有训练价值 if msk.max() 0: continue # 过滤黑边块无效值占比过高会扰乱归一化 if np.mean(np.all(img 0, axis-1)) 0.3: continue if img.dtype np.uint16: img (img / 257).astype(np.uint8) cv2.imwrite(str(out_root / images / f{name}_{count:05d}.png), img) cv2.imwrite(str(out_root / masks / f{name}_{count:05d}.png), msk) count 1 print(f{name}: 生成 {count} 个切片) return count代码逻辑说明rasterio 的 Window(left, top, win_size, win_size) 参数是列号、行号第一个参数是列起点第二个是行起点和 OpenCV 里先 x 后 y 的习惯是反的写错了就直接切错位置。read 出来的数组形状是 (C, H, W)先转成 HWC 才能让 cv2.imwrite 直接写。两个过滤条件分别针对纯背景块和黑色无效区域具体阈值可以按数据集的实际情况微调。参数说明win_size512 在 8GB 显存的卡上配 U-Net 能跑 batch_size4 左右win_size256 可以翻倍但窗口内道路上下文变少。stride256 让切片数量约为无重叠的 4 倍如果训练时间吃紧可以调成 384。16bit 转 8bit 用 257 而不是 256是因为 65535 / 257 255这样能保留完整的亮度范围直接除以 256 整体会偏暗。还有一个小提醒保存的 mask 如果值是 0 和 1用看图软件打开会显示接近全黑这是正常的不代表数据丢了。要人工检查时先乘 255 再显示。3.3 数据增强的边界哪些操作对遥感影像安全遥感道路分割的数据增强比自然图像要保守。安全的是几何类左右翻转、上下翻转、旋转 90 度、180 度、270 度。道路没有固定的上下方向这些变换不会改变语义。要小心的是亮度、对比度、色彩抖动。同一场景不同时相的光照差异真实存在做适量扰动确实能增强泛化但扰动过强会把沥青路面的灰色调偏到植被的绿色上模型会把路认成树。不建议做的是弹性形变和随机裁切。弹性形变会扭曲道路形状让细长道路被打断随机裁切等价于更小的窗口把本来就只有几个像素宽的道路切得更碎这里我吃过亏。import random import numpy as np def sync_augment(img, msk): 图像与标签用同一个随机状态做几何变换保证像素级对齐。 if random.random() 0.5: img np.fliplr(img) msk np.fliplr(msk) if random.random() 0.5: img np.flipud(img) msk np.flipud(msk) k random.choice([0, 1, 2, 3]) img np.rot90(img, k) msk np.rot90(msk, k) return img, msk这段代码最容易翻车的点是变换不同步。如果分别用 torchvision 的 RandomHorizontalFlip 处理 image 和 mask两次随机调用不一定得到同一个随机结果图像翻转了标签没翻转训练时 loss 会一直震荡。把两个数组放进同一个 if 分支用同一个随机状态做变换才能保证对齐。4. 训练集与测试集划分的正确姿势避免同源数据泄漏标题里强调划分了训练集和测试集这正是遥感数据集和普通数据集最不一样的地方。划分的粒度错了后面的评估全部失真。4.1 随机划分在大分辨率遥感影像上为什么会翻车很多入门教程教的是把所有图片路径放进一个列表然后 train_test_split 按比例随机切。这个做法在自然图像数据集上凑合能用在大分辨率遥感影像上是灾难。遥感影像的单幅图往往是一个场景一景卫星影像覆盖几平方公里。滑窗切片之后同一场景切出的几百个切片在光照条件、传感器型号、地物类型上高度相关。如果随机把这些切片分到训练和测试测试切片等于在和它高度相似的训练切片裸考。模型并没有见过新的地理环境指标却虚高换个城市立刻崩。正确做法是场景级划分把一张完整大图整体划入训练集或测试集无论它之后会被切成多少块。如果一张图地理跨度极大比如同时包含城区、郊区、农田最好先按地理瓦片拆开再划分确保同一瓦片下的切片不会跨集合。4.2 按场景划分训练集和测试集的脚本我一般拿到原始大图后先按场景统计信息再做场景级划分。import random from pathlib import Path import json rng random.Random(42) raw_images sorted(Path(raw/images).glob(*.tif)) scene_names [p.stem for p in raw_images] # 场景级随机划分完整一张图只进一个集合 rng.shuffle(scene_names) test_ratio 0.2 test_count int(len(scene_names) * test_ratio) test_scenes set(scene_names[:test_count]) train_scenes set(scene_names[test_count:]) with open(split.json, w) as f: json.dump({ train: sorted(train_scenes), test: sorted(test_scenes) }, f, indent2)这里的核心是 shuffle 发生在 scene_names 上而不是切片文件列表上。如果数据集作者已经切好片文件名保留了场景前缀比如 scene_001_0000.png就先按前缀聚合再对聚合后的场景名做划分。如果文件名前缀被抹掉只能靠文件名特征或影像特征去反推场景归属工作量会成倍增加这也是我反复强调先检查命名规则的原因。如果影像带地理坐标可以更进一步把场景的经纬度范围画出来把空间相邻的场景分到同一侧避免地理位置泄漏。实际操作可以按经纬度网格分桶再把桶划分到训练和测试。这个在大规模遥感工程里属于常规操作。4.3 划分后必须做的独立性与分布一致性检查当数据集作者已经划分好训练集和测试集时我不直接信任先跑一遍验证重点看四件事文件名是否有交叉。训练集和测试集的影像尺寸分布是否一致。两边的道路像素占比是否接近。把训练和测试的缩略图拼在一起肉眼对比确认没有同一区域重复出现。import numpy as np from PIL import Image from pathlib import Path def road_ratio(mask_path): m np.array(Image.open(mask_path)) return float((m 1).sum() / m.size) for split in [train, test]: ratios [road_ratio(p) for p in (Path(dataset) / split / masks).glob(*.png)] print(split, 道路占比均值: {:.2f}%, 最小: {:.2f}%, 最大: {:.2f}%.format( np.mean(ratios) * 100, np.min(ratios) * 100, np.max(ratios) * 100))road_ratio 函数假设标签是 0/1如果数据集用的是 0/255先归一化再统计。这个统计非常关键如果训练集道路占比 5%、测试集 15%模型在测试集上指标一定会偏低因为训练时偏向少预测道路而测试集道路又多精召之间怎么调都不对。这种情况要在切片划分时做分层抽样按道路占比分布去切。检查文件级泄漏的脚本更简单train_masks set(p.stem for p in (Path(dataset) / train / masks).glob(*.png)) test_masks set(p.stem for p in (Path(dataset) / test / masks).glob(*.png)) overlap train_masks test_masks print(同名文件重叠数:, len(overlap)) train_prefixes {s.rsplit(_, 1)[0] for s in train_masks} test_prefixes {s.rsplit(_, 1)[0] for s in test_masks} print(跨集合场景数:, len(train_prefixes test_prefixes))同名文件重叠是最简单的泄漏但更隐蔽的是不同名的相邻切片跨集合。比较文件名前缀就能发现大部分问题。如果前缀也大量重合说明数据集作者做的确实是切片级随机划分这份数据集的评测结论在严格意义上不可信。5. 避坑大分辨率遥感道路分割数据集使用中的常见问题排查这一节列的是我在处理大分辨率遥感道路数据时踩过、或帮别人排查过的高频问题。每一条都按现象、原因、解决三段来写可以直接对照排查。5.1 训练 Loss 正常下降验证指标却剧烈震荡现象训练 loss 平滑下降验证 IoU 每个 epoch 上下跳 10 个百分点以上。原因最常见的是验证集切片的空间相关性太强。验证集的几百张切片全部来自同一幅大图且恰好包含一个密集路网区域。loss 统计的是像素级平均IoU 是按类别统计两者对样本分布的敏感度完全不同。另一个常见原因是验证集本身太小几十张困难切片就会把指标拉低。解决验证集至少准备几百张切片按场景采样不要按切片采样固定随机种子让每个 epoch 评估同一组数据。如果验证集足够大但指标仍然震荡去检查验证集和训练集的道路占比分布差异大概率是两边差异太大。5.2 GPU 显存不足Batch Size 调到 1 还是爆现象把 batch_size 设成 1关闭了所有中间缓存训练时仍然 OOM。原因显存爆掉的常见原因不是 batch_size 太大而是输入尺寸太大。把整幅大图直接喂进网络是典型错误。另外如果模型带 BatchNormbatch_size1 时 BN 的统计量非常不稳定可能导致损失震荡甚至不收敛。解决确认 DataLoader 输出的是切片而不是整图。512×512 的切片在常见 U-Net 上 batch_size2 到 4 可以跑想跑更大 batch 就用梯度累积。batch 特别小时把 BN 换成 GroupNorm 或 InstanceNorm遥感道路分割对小 batch 并不敏感小 batch 下 GroupNorm 明显更稳定。显存优化这一项做语义分割和做图像分类的侧重点完全不同不能照搬。5.3 预测结果里道路边缘呈锯齿状、有拼接缝现象测试时用滑窗逐块预测再把结果拼回大图拼接处道路断裂出现一条条明显的方块接缝。原因预测时步长等于窗口大小相邻窗口没有重叠窗口边缘的信息被模型忽略了拼接时如果直接取 argmax两个窗口对边界像素的预测不一致就会产生接缝。解决预测时把步长减小到窗口的一半甚至四分之一重叠区域对每个像素的概率取平均再取 argmax。训练时也使用重叠切片让模型适应道路被窗口截断的情况推理时边界连续性会好很多。如果训练时无重叠切片推理时突然加重叠效果提升有限这点要注意。5.4 测试集 IoU 很高但目视检查漏检严重现象测试集 IoU 达到 0.8 以上把整幅预测图叠到原图上发现一条主干道漏掉了一半窄一点的巷路几乎全断。原因IoU 对类别不均衡非常不敏感。背景占 95%、道路占 5% 时模型只要少预测道路、做保守预测IoU 不会掉太多。细窄道路的线性退化对像素级 IoU 影响也不大所以像素指标高不代表道路拓扑完整。解决评估时分开看 Precision、Recall 和 F1特别是道路类的 Recall把预测结果按道路连通域拆分统计断裂段数量。如果测试集中的道路明显比训练集细或宽先查数据增强里有没有尺度扰动遥感影像分辨率不一致时这个问题很常见。5.5 标签图读出来一片黑或一片白现象训练加载时打印标签值全是 0 或全是 255可视化出来不是纯黑就是纯白。原因像素值 0/255 可能表示的是未标注区域而不是背景或者标签是 16bit PNGPIL 读取后是 uint16没归一化到 uint8 显示就会异常。还有一种情况是 mask 保存成了 JPEG有损压缩后道路边缘已经糊掉读出来的值域也乱了。解决读标签后先 np.unique 统计值域把标签规范化到 0/1有未标注区域则保留 255 并设 ignore_index。确认 PNG 位深后统一转 uint8。这类转换放在预处理脚本里做不要每次训练循环里重复做否则 IO 开销会拖慢整体速度这是最容易被忽视的性能瓶颈。6. 基线验证与进阶用滑窗重叠预测和连通域评估确认数据集可用性6.1 滑窗重叠预测把大图拼回来还能保持道路连续拿到划分好的数据集第一轮实验不是追 SOTA而是用一个小网络快速跑一个基线把评估链路打通。我建议用 U-Net 这类常见的卷积模型输入 512×512 切片先训练约 100 个 epoch然后做整幅预测。推理时的滑窗和训练时的切片不同训练时切片是独立样本推理时要把所有切片拼成一幅完整大图。直接拼接会在窗口边界出现接缝解决办法是让相邻窗口重叠把重叠区域的预测概率取平均。import numpy as np def predict_full_image(model, image, win_size512, stride128): 重叠滑窗推理返回整幅图的道路概率图。 h, w image.shape[:2] prob_sum np.zeros((h, w), dtypenp.float32) weight np.zeros((h, w), dtypenp.float32) for top in range(0, h - win_size 1, stride): for left in range(0, w - win_size 1, stride): crop image[top:top win_size, left:left win_size] prob model.predict(crop) # 你的模型推理函数返回 (H, W) 概率 prob_sum[top:top win_size, left:left win_size] prob weight[top:top win_size, left:left win_size] 1 prob_map prob_sum / np.maximum(weight, 1) return prob_map参数说明stride128 表示重叠 75%推理时间约为无重叠的 16 倍。实际使用根据时间预算放在 128 到 256 之间128 的平滑效果已经足够好。weight 记录每个像素被预测的次数用它做平均加 1 的保护是为了防止大图边缘出现未覆盖区域。真正用 PyTorch 实现时记得 model.eval() 和 torch.no_grad()把 crop 转成 tensor 放到对应设备。6.2 指标怎么读IoU 不是终点道路拓扑完整性才是对道路分割来说IoU、F1 之外还要看拓扑。一条路被截成三段IoU 可能只下降零点几个点但对下游应用是致命问题。进阶做法是训练后对预测图做连通域分析from scipy import ndimage import numpy as np def count_road_breaks(pred_mask, min_length10): 统计预测道路的连通段数段数越多说明断裂越严重。 labeled, n_component ndimage.label(pred_mask) sizes ndimage.sum(pred_mask, labeled, range(1, n_component 1)) long_segments (sizes min_length).sum() return n_component, long_segmentsn_component 越大道路碎片化越严重long_segments 是像素数超过阈值的连通段数。一个理想结果里n_component 应该接近真实道路拓扑中独立道路的数量。如果想更严格可以先对预测图做骨架化提取道路中心线再统计断裂次数这是工程上更接近真实需求的指标。这个指标的用途有两个一是对比不同实验判断模型是在变细还是变断二是作为训练监控如果 IoU 在涨但 long_segments 在涨说明模型在走保守路线需要调整损失权重或增强策略。类的权重我一般按道路像素占比来定道路占比 5% 时把 Dice Loss 和交叉熵按 0.3 比 0.7 混合比单纯用加权交叉熵更稳。我已经养成一个习惯每次拿到新数据集第一周不写模型先把读图、切片、划分、评估四件事跑通并写进项目文档。数据链路是通的后面换模型、调参都是在同一个可信地基上迭代数据链路没打通训练脚本再漂亮也只是在黑盒上撞运气。希望这套思路能帮你在遥感道路分割这个方向少踩几个坑。本文还有配套的精品资源点击获取
网站建设高端定制企业官网