
简介遥感影像道路分割与多类别图像分割数据集面向深度学习分割任务适合科研人员、算法工程师及高年级学生用于道路提取、地物分类等模型训练与算法验证。数据总量约4000张已统一预处理并完成训练/验证集划分训练集约2800张、验证集约1200张每张遥感图均配有对应mask标签mask中0代表背景、255代表道路可直接输入分割网络迭代训练。资源包共2000个文件以jpg原图与png标签图为主另含classes类别说明txt与可视化脚本py脚本可随机抽取样本将原始图片、GT标签及GT蒙版叠加效果合并展示并保存到当前目录便于快速核对标注质量。压缩包大小147.76MB体量适中便于下载使用。目前已有59人浏览学习适合希望省去数据清洗与划分时间、专注模型结构改进的读者可配合U-Net、SwinUnet等主流分割网络开展实验。1. 遥感影像道路分割最该先搞定的不是模型而是数据集做遥感影像道路分割多数团队推进瓶颈不在模型精度而在数据准备。原始影像动辄几千乘几千像素标签质量参差不齐坐标系和投影不一致格式从 TIFF 到 PNG 再到 GeoJSON 都有——把这些整理成模型能直接吃的样子往往要花掉项目一半的时间。所以当我看到“约4000张数据和标签已处理完可以直接训练多类别图像分割”这类数据集时第一反应是这能省下大量前期时间。这篇笔记围绕这类可直接训练的遥感图像分割数据集展开说明数据怎么组织、训练怎么起步、参数怎么调以及在哪里最容易翻车。适合刚接触遥感影像分割、想快速跑通基线的工程师也适合要在有限算力下做出第一版道路分割结果的项目。2. 一个能直接训练的数据集先看目录、标签和划分再谈模型拿到一个标着“已处理完可以直接训练”的数据集别立刻把训练脚本跑起来。先花10分钟把数据本身检查一遍这十分钟能帮你省掉后面几天的排查时间。一套能被 U-Net、DeepLabV3 这类图像分割算法直接读取的数据最少需要三个部分图像文件、对应的标签文件、以及一份划分好的训练/验证列表。缺了任何一块训练时都要临时补逻辑越补越乱。2.1 目录和命名一切约定都围绕“一一对应”展开常见做法是数据集中包含以下目录data/ ├── images/ │ ├── img_0001.tif │ ├── img_0002.tif │ └── ... ├── masks/ │ ├── img_0001.png │ ├── img_0002.png │ └── ... ├── train.txt ├── val.txt └── test.txt图像的命名和标签文件名完全一致只差后缀。影像用 TIFF是因为遥感影像常带着地理信息或更高的位深标签用 PNG是因为 PNG 支持无损压缩且能存储单通道索引值。train.txt、val.txt 里每行是一个不含后缀的文件名训练脚本按这个名字去 images 和 masks 里找对应文件。这个约定看着简单却决定了训练代码的复杂度。只要保证“同名读图”写数据加载器时就只需要改路径不需要维护额外映射表。反过来如果数据集里文件名带前后缀差异或者标签散落在多个子目录训练脚本里就要加一堆拼接逻辑每加一层就多一个出错的点。判断一个数据集是否真处理干净先看目录结构就够晕头晕脑的多级嵌套和手工改名通常是没清洗干净的前兆。2.2 多类别标签像素值不是颜色而是类别编号这里的关键是区分“可视化标签”和“训练标签”。多类别分割数据集的标签 PNG在普通看图软件里打开是彩色的但文件存储的其实是像素级的类别索引比如 0背景、1道路、2建筑、3植被、4水体、5其他。颜色只是调色板渲染出来的结果训练时模型读到的必须是原始的索引值而不是 RGB。这一点检查不到位后面所有类别统计都是错的。动手前先跑一段统计代码确认标签像素值分布符合预期from PIL import Image import numpy as np import glob for mask_path in sorted(glob.glob(data/masks/*.png))[:20]: arr np.array(Image.open(mask_path)) classes, counts np.unique(arr, return_countsTrue) print(mask_path.split(/)[-1], dict(zip(classes.tolist(), counts.tolist())))这段代码用 PIL 读取前 20 张标签图统计每个像素值出现多少次。如果打印结果里出现 [0, 1, 2, 3, 4, 5] 且每张图的类别分布大体一致说明标签可用。若出现 255 或负数说明标签里混入了忽略值或异常值训练前要处理掉。类别像素值的起点和连续性直接决定了模型输出层类别数。如果像素值有跳跃比如没有 1 直接到 2交叉熵会按类别索引取真值空洞类别会让损失函数报错或让评估指标出现莫名其妙的偏差。2.3 数据划分按瓦片区域切别按文件随机切遥感影像和自然图像最大的区别在于空间自相关性。相邻的瓦片之间地物分布高度相似同一条路跨越好几张瓦片同一条河贯穿相邻图幅。如果按文件名随机打乱做训练/验证划分路和河的两端同时出现在训练集和验证集里验证的 mIoU 会显得特别高但在真正没见过的区域上做推理时精度立刻掉下来。这不叫数据划分这叫泄漏。正确做法是按空间区域划分比如把整个范围内的数据按经纬度分块东部做训练、西部做验证。如果数据集作者已经按这个逻辑交付了 train.txt 和 val.txt直接遵守它不要自行重切。需要验证划分质量时把训练和验证文件的影像缩略图拼出来看一眼如果两边看起来像同一块地方就有泄漏风险。用文件名随机划分是很多图像分割项目虚高指标的根源遥感场景尤其明显。2.4 训练前的最后检查三行命令防呆开始训练之前我建议跑一个小脚本把下面三件事一次确认掉图像和标签数量相等、尺寸一致、类别数不超过模型输出通道数。from PIL import Image import numpy as np n len(open(data/train.txt).readlines()) img np.array(Image.open(data/images/img_0001.tif)) mask np.array(Image.open(data/masks/img_0001.png)) print(train samples:, n) print(image shape:, img.shape, mask shape:, mask.shape) print(mask classes:, np.unique(mask))影像如果是三通道shape 是 (H, W, 3)标签是 (H, W)。二者前两维必须一致。若影像带地理坐标或超过三个通道比如多光谱影像U-Net 第一层要做相应调整这个在第三章讲。到这里数据侧的工作就结束了下面进入训练环节。3. 用 U-Net 把训练脚本搭起来从数据加载到 loss 收敛U-Net 是遥感道路分割的基线选择。它结构对称、参数少、对显存要求低在可见光影像上训练稳定也方便后续换成带预训练权重的变体。下面给出一套最小可运行的训练脚本基于 PyTorch 和 segmentation-models-pytorch这是目前做遥感图像分割最常用也最稳的组合不用自己从零实现编码器解码器结构。3.1 最小训练脚本import os import torch import numpy as np from PIL import Image from torch.utils.data import Dataset, DataLoader import segmentation_models_pytorch as smp class RoadDataset(Dataset): def __init__(self, root, split, crop_size512): self.img_dir os.path.join(root, images) self.mask_dir os.path.join(root, masks) self.names open(os.path.join(root, f{split}.txt)).read().splitlines() self.crop_size crop_size 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 .tif)).convert(RGB) mask Image.open(os.path.join(self.mask_dir, name .png)) # 随机裁剪到固定尺寸遥感大图不能整图进网络 if self.crop_size: w, h img.size x np.random.randint(0, max(1, w - self.crop_size)) y np.random.randint(0, max(1, h - self.crop_size)) img img.crop((x, y, x self.crop_size, y self.crop_size)) mask mask.crop((x, y, x self.crop_size, y self.crop_size)) img torch.tensor(np.array(img)).permute(2, 0, 1).float() / 255.0 mask torch.tensor(np.array(mask)).long() return img, mask model smp.Unet( encoder_nameresnet34, encoder_weightsimagenet, in_channels3, classes6, ) loss_fn torch.nn.CrossEntropyLoss(ignore_index255) optimizer torch.optim.AdamW(model.parameters(), lr1e-4) loader DataLoader(RoadDataset(data, train, crop_size512), batch_size8, shuffleTrue, num_workers4) model.train() for epoch in range(30): total_loss 0 for imgs, masks in loader: imgs, masks imgs.cuda(), masks.cuda() logits model(imgs) loss loss_fn(logits, masks) optimizer.zero_grad() loss.backward() optimizer.step() total_loss loss.item() print(fepoch {epoch:02d} loss {total_loss / len(loader):.4f}) torch.save(model.state_dict(), funet_{epoch:02d}.pth)这个脚本把训练主流程压缩到了 40 行以内。数据加载器读入同名图片和标签随机裁剪到 512×512并归一化到 0-1 区间。模型选用 ResNet34 编码器加 ImageNet 预训练权重特征提取能力比从头训练好很多尤其在数据量只有 4000 张的情况下预训练权重能明显提升收敛速度。损失函数用交叉熵ignore_index 设 255 是为了跳过标签里的无效区域。3.2 参数怎么定crop size、batch size 和学习率这里几个参数说一下我常用的选择。crop size 在 512 到 768 之间最常见道路是细长结构裁剪窗口太小会把道路截断模型学到的是碎片推理时连续性就差窗口太大显存不够尤其还带着 ResNet34 编码器。batch size 在单卡 24GB 显存下512 裁剪配 8 到 12 比较稳。学习率方面带 ImageNet 预训练时 AdamW 用 1e-4 起步从头训练时用 3e-4 到 1e-3 起步。如果发现 loss 在十几个 epoch 后不再下降把学习率降为原来的 1/10这个操作比改模型结构更常见也往往更有效——学习率调参是图像分割里性价比最高的动作远高于纠结哪一层该加卷积。提示多人同时用同一份数据训练时尽量固定住 crop 随机种子否则每次实验的裁剪位置都不一样复现出来的精度浮动会超过 2 个点。3.3 训练过程中的监控别只盯着 loss训练时只看 loss 曲线有明显的误导性。语义分割的 loss 下降不代表道路分割好了可能是背景区域拟合得更好把道路误分成背景的损失摊薄了。每个 epoch 结束后算一次验证集的 mIoU比看 loss 更可靠。一张卡一天内能跑完 30 个 epoch 时每 5 个 epoch 保存一次 checkpoint同时记录当时的 mIoU最后选 mIoU 最高的那一次而不是最后一个 epoch 的权重。这算是语义分割里的常贴心法也顺便解决了最优权重被覆盖的后悔药问题。4. 从 U-Net 换到 DeepLabV3 和 SegFormer适配点在哪里U-Net 跑通基线之后下一步通常是换更强的分割模型提升精度。道路是细长结构加上车辆、阴影和树冠遮挡纯卷积网络经常在断连处翻车。常见升级路径有两条DeepLabV3 或基于 Transformer 的分割模型。两者的适配方式差异不小但选择标准很直接。4.1 DeepLabV3torchvision 自带实现改动量最小torchvision 里已经提供了 DeepLabV3 的实现加载一个带 COCO 预训练权重的模型只要三行import torchvision model torchvision.models.segmentation.deeplabv3_resnet50( weightstorchvision.models.segmentation.DeepLabV3_ResNet50_Weights.COCO_WITH_VOC_LABELS_V1 ) model.classifier[4] torch.nn.Conv2d(256, 6, kernel_size(1, 1), stride(1, 1))这里要改的是最后一层分类器的输出通道数从原本的 21 类VOC 语义分割换成自己的 6 类。DeepLabV3 引入空洞空间金字塔池化用多个不同空洞率的卷积并行捕捉不同尺度的上下文对道路这种跨越大范围连续分布的目标比普通 U-Net 表现更稳。ASPP 各分支的 dilation rates 可调若道路在影像中较窄加大低空洞率分支的权重有帮助但一般项目里默认配置即可不必深改。换模型时注意输入尺寸最好和 U-Net 保持一致避免因为模型变了而连带需要重调 crop size。4.2 换 SegFormer 或 Mask2Former什么时候值得折腾Transformer 分割模型能捕获长距离依赖道路被树冠或建筑物阴影截断时它们更可能根据上下文把断点补起来。但是要注意这类模型对数据量和显存的要求更高。4000 张图的规模SegFormer-B0 还能在 24GB 单卡上勉强跑Mask2Former 就要考虑多卡或进一步减小 crop size。常见选择是 SegFormer-B0/B1 配合 ImageNet 预训练权重。用 transformers 库写训练逻辑时需要把标签从 (B, H, W) 的整数张量直接输入模型输出的 logits 形状和标签对齐后交叉熵计算方式与 U-Net 完全一致数据加载器不用改。这类模型的另一个特性是推理分辨率适应性强。SegFormer 的多层特征在不同分辨率下都能输出稳定结果所以可以训练时用 512 裁剪、推理时用原图切块预测。而 DeepLabV3 的 ASPP 结构对输入尺寸相对敏感训练和推理分辨率差太多时精度会有可感知的抖动。如果项目交付时需要跑不同分辨率的影像这一点值得提前做实验验证别在交付前才发现。4.3 模型选型判断表模型显存需求参数量特点适合场景U-Net低约 30M训练快改造成本最低第一版基线、数据量有限DeepLabV3中约 40M多尺度上下文建模强torchvision 直接调用道路被遮挡、断连明显的场景SegFormer-B1偏高约 45MTransformer 结构长距离依赖大图推理、对道路连续性要求高选型不需要追求最贵的模型。U-Net 跑通后先看验证图上的失败模式如果是碎块分割和噪声点换 DeepLabV3 效果明显如果是道路断连和跨区域漏检再考虑 Transformer 方案。每升一档模型显存和训练时间都会翻一到两倍项目周期要留出这个余量。忠实记录每个模型的 mIoU 和显存占用比反复改论文里的结构更管用。5. 遥感道路分割避坑记录五条用时间换来的教训以下每条都是实际项目中遇到过的。我只写现象、原因和解决路径不写空泛的注意事项。看过这些之后你会发现很多训练事故不是模型不行而是数据集和训练细节埋的雷。5.1 影像和标签尺寸对不上现象训练数据加载阶段报错提示 tensor 维度不匹配偶尔不报错但训练出的 mIoU 极低。 原因部分影像经过重投影、裁剪或重采样后标签生成时没对齐个别图差几个像素甚至几十个像素。语义分割按像素计算损失差一个像素整体错位。 解决写一个批量校验脚本对每一张图比较 image 和 mask 的宽高不一致的单独列出来。宁可删掉这几张也不要在 DataLoader 里强行 resize。强制 resize 会让道路宽度失真精度很难评估。5.2 道路占比不到 1%loss 看着在降道路却全黑现象训练十几个 epoch背景像素拟合得很好但预测图里道路区域几乎是全黑。 原因多类别中道路类别占比很低交叉熵对像素数量多的类别天然倾斜。4000 张图的道路如果只占百分之几模型学到的就是把一切预测为背景。 解决用类平衡交叉熵或者给损失函数加类别权重。权重按类别像素占比的倒数初始化比如道路占 0.5% 就设权重为 200。常见做法是统计全数据集的类别频率再按 frequency balancing 设定权值。代码可以直接用import glob import numpy as np from PIL import Image counts np.zeros(6, dtypenp.float64) for mask_path in glob.glob(data/masks/*.png): arr np.array(Image.open(mask_path)) for c in range(6): counts[c] (arr c).sum() freq counts / counts.sum() weights np.median(freq) / freq print(weights)权重计算完把它传给CrossEntropyLoss(weighttorch.tensor(weights).cuda())即可。注意道路类别的像素频率统计要在所有训练图上做而不是只看几张抽样否则权重会偏。类不平衡是遥感道路分割里最常见也最容易被忽视的问题很多人以为是模型收敛问题实际是损失函数设计的问题。5.3 影像带着 NoData 边或不规则黑边归一化出了问题现象验证集 mIoU 比训练集高很多或者推理时大块区域出现黑色预测。 原因很多遥感影像在范围外有填充值常见是 0 或 255。如果按全图均值做归一化NoData 区域会把均值拉偏如果标签里没把 NoData 标为忽略类模型会学出奇奇怪怪的边界响应。 解决要么把 NoData 值在标签里标为 ignore class要么在裁剪时做有效区域掩码过滤。最省事的做法是训练前把 NoData 区域之外的包围盒裁剪一次把数据集的无效边彻底去掉。对于已处理完可以直接训练的数据集作者通常已经处理了这一步但自己还是要抽查几张确认图片边界没有黑边或白边。5.4 显存爆掉直接把 4000×4000 像素的大图整图塞进网络现象训练开始即报 CUDA out of memory或者 batch size 调到 1 后仍然爆显存。 原因遥感影像原始尺寸通常在几千像素级别分割网络是对全图做特征图运算整图前向一次就占掉几十 GB 显存。 解决不能整图训练。解决方案是用随机裁剪或者切成有重叠的瓦片。推理时同样要切块并做重叠拼接overlap-tile strategy重叠区域按到边缘的距离做加权融合避免接缝处出现断裂。若显存还有余量优先增大 crop size 而不是 batch size因为道路是细长目标crop size 对连续性的影响大于 batch size。这块调整多少带点玄学但按“crop 优先于 batch”的规则来一般都能跑通。5.5 验证集和训练集来自同一片区域mIoU 虚高现象验证集 mIoU 接近 0.9换个区域推理直接掉到 0.5。 原因数据按文件随机划分相邻瓦片的道路连着验证和训练两侧空间信息泄漏。 解决用按区域划分的训练/验证列表或者直接信任数据集作者给出的划分文件不要自己重排。另外可以准备一小块完全没有参与训练的外部区域单独做测试作为最终验收数字。这个外部测试区不来自原数据集的任何瓦片才能代表模型的真实泛化能力。如果你拿到的数据集没有提供 test.txt那就要从图幅编号或经纬度上判断划分方式而不是靠文件名随机分配。6. 最后一步把指标落到图上才能判断道路分割能不能交付mIoU 是数字最终决策靠的还是图。道路分割的验收通常有两个维度几何完整性道路是否连续和拓扑正确性是否错连、多连。我习惯在验证集上随机挑一批图把原图、标签和模型预测三通道拼在一起输出然后人工扫一遍。这个动作比任何指标都更贴近交付方的感受。import matplotlib.pyplot as plt import numpy as np import torch model.eval() with torch.no_grad(): for i in range(10): img, mask val_dataset[i] logits model(img.unsqueeze(0).cuda()) pred logits.argmax(dim1).squeeze(0).cpu().numpy() fig, axes plt.subplots(1, 3, figsize(15, 5)) axes[0].imshow(img.permute(1, 2, 0).numpy()) axes[1].imshow(mask.numpy(), cmaptab20) axes[2].imshow(pred, cmaptab20) axes[0].set_title(image) axes[1].set_title(gt) axes[2].set_title(pred) plt.savefig(fvis_{i:02d}.png, dpi150)把图片存下来后按三个模式快速分类断连、漏检、过分割。断连多就查模型上下文建模能力漏检多就查类别权重和裁剪尺寸过分割多就查标签质量和后处理是否激进。多类别项目里还要额外统计一张每类 IoU 表格整体 mIoU 看着不错可能只是道路这一类的 IoU 在拖后腿。交付前如果涉及矢量线提取就要把道路类别的概率图做阈值分割再做连通域分析和形态学骨架化这个后处理环节对业务效果的影响往往和模型本身一样重要。我的习惯是每次实验只改一个变量并保留那一轮的预测图。这个习惯帮我避开了很多“模型好但效果差”的困惑。数据集的坑不可怕可怕的是坑被埋到模型里才发现。希望这套流程能帮你在同一条路上少踩几个坑。本文还有配套的精品资源点击获取