简介:面向遥感图像道路分割的数据集,适合需要训练语义分割模型的研究者、算法工程师与学生。数据规模约4000张,包含原图与对应掩膜,且已按训练集约2800张、验证集约1200张完成划分,打开即可用于深度学习训练。压缩包为7z格式,共2000个文件,其中JPG原图797张、PNG标签1201张,另含类别说明TXT与可视化脚本PY各1个,整体体积147.76MB,轻量易部署。标签体系以0表示背景、255表示道路,适合道路提取与地物分割等典型场景,也可作为多类别分割任务的初始数据。附带的可视化脚本可随机抽取图片,将原始图、GT图、GT在原图上的蒙版同时展示并保存,便于快速核对标注质量与训练效果;目前已有59人学习下载,对有遥感分割需求的学习者而言是一份可直接上手的数据资源。
1. 为什么说“已处理完可以直接训练”是遥感分割数据集最稀缺的承诺
做遥感影像道路分割的人,几乎都被同一件事折磨过:模型选型半天就能定,数据却要耗掉两周。原始影像要匀色、要裁剪、要配准,标签要么是矢量多边形要栅格化,要么是别的坐标系要重投影,等你真正把数据喂进网络,热情早被磨光了。所以看到“约4000张数据和标签,已处理完可以直接训练,多类别图像分割”这句话,识货的人会先松一口气。这个标题的意思很直白:数据集已经把道路、建筑、植被、水体这类多类别标签全部 rasterize 成和影像像素对齐的格式,你不用再做坐标转换和标签修复,解压就能进训练流程。这篇文章我就按自己的实操习惯,把这个数据集从拆包检查、框架选型到训练评估的完整路径讲一遍,顺便把遥感多类别分割里最容易翻车的地方指出来。
2. 拿到数据集先别急着训:文件结构、标注格式与类别分布(配一套检查脚本)
2.1 解压之后的目录长什么样:一份通用的清点清单
我拿到任何图像分割数据集,第一件事不是打开训练脚本,而是用终端把目录结构完整列出来。这种标题下,影像和标签一般按文件夹分层放,常见的结构是images/和labels/(或masks/)平行存放,文件名一一对应。先把结构看清楚,再写代码核对配对关系。
# 在数据集根目录下执行,看整体目录结构 tree -L 2 -d # 统计 images 和 labels 下的文件数量 ls images | wc -l ls labels | wc -l # 抽查前 10 个文件名,确认命名规则一致 ls images | head -n 10 ls labels | head -n 10这里tree只看目录层级的-d选项,避免几千个文件名刷屏。数量统计是第一个警戒线:images和labels的文件数不一致,说明有缺失或多余文件,后面训练时DataLoader会在某一步直接崩掉。文件名抽查则是为了确认两边是否能按字典序或同名规则配对,如果一个是img_0001.tif,另一个是label_0001.png,那就要写一个显式映射关系,不能依赖默认排序。
2.2 标签的读取方式:单通道 PNG 还是 RGB 伪彩色
多类别分割数据集的标签有两种常见存法:一种是单通道灰度 PNG,每个像素值就是类别索引,0 是背景、1 是道路、2 是建筑;另一种是 RGB 伪彩色 PNG,每个类别用一种颜色表示,读取时要查色表转成索引图。标题里说“多类别图像分割”,大概率是前者,因为它省去了一轮颜色映射,但我仍会写一段脚本确认像素值范围。
# 用 OpenCV 读取一张标签图,检查通道数和像素值分布 import cv2 import numpy as np from collections import Counter label_path = "labels/train_0001.png" label = cv2.imread(label_path, cv2.IMREAD_UNCHANGED) print("shape:", label.shape) print("dtype:", label.dtype) print("unique values:", np.unique(label))如果输出是(512, 512)单通道,dtype 是uint8,unique values 里是[0, 1, 2, 3, 4]这类连续小整数,这就是标准的索引图,训练时直接当torch.LongTensor喂给 CrossEntropyLoss。如果 shape 是(512, 512, 3),说明是 RGB 标签,得先做一层映射:
# RGB 伪彩色标签转索引图,颜色表可自定义 color_map = { (0, 0, 0): 0, # 背景 (255, 255, 255): 1 # 道路 # 其余类别按实际标签颜色补充 } rgb = cv2.imread(label_path) rgb = cv2.cvtColor(rgb, cv2.COLOR_BGR2RGB) h, w = rgb.shape[:2] index_map = np.zeros((h, w), dtype=np.uint8) for color, idx in color_map.items(): mask = (rgb == np.array(color)).all(axis=-1) index_map[mask] = idxRGB 标签本身不是大问题,但每转一次就多一个出错点——颜色阈值重叠、未收录的颜色被归到背景、反色通道顺序搞错,这些我都踩过。所以我建议不管标题怎么描述,都先跑一遍像素统计再进训练。
2.3 训练前抽检:统计像素类别分布和一张“对不齐”的边界
类别分布统计是遥感分割最容易忽略的一步。道路在整幅影像里占比通常不到 10%,建筑多一点,背景往往占 60% 以上。这种不均衡会直接拉偏训练方向,如果不做类别权重,模型会学成“把一切预测成背景”的偷懒模式。
import glob import numpy as np # 遍历全部标签,统计每个类别的像素占比 from collections import defaultdict counter = defaultdict(int) total_pixels = 0 for idx, mask_path in enumerate(glob.glob("labels/*.png")): mask = cv2.imread(mask_path, cv2.IMREAD_UNCHANGED) if mask is None: print("无法读取:", mask_path) continue unique, counts = np.unique(mask, return_counts=True) for cls, cnt in zip(unique.tolist(), counts.tolist()): counter[cls] += cnt total_pixels += mask.size print("类别像素分布:") for cls in sorted(counter.keys()): print(f"类别 {cls}: {counter[cls]:>10} 像素, 占比 {counter[cls]/total_pixels:.2%}")这段代码的输出要重点看两个东西:一是有没有类别占比低于 1% 的情况,如果有,后面训练必须给这个小类别高权重,否则它的梯度会被大类别淹没;二是有没有出现不在预期范围内的像素值,比如标签写的是 0~4,统计却出现了 6,这就是脏标签,要去原图中定位修复。训练前花二十分钟做完这件事,比训练完再发现 mIoU 上不去要划算得多。
从这章开始,你的数据和标签就已经彻底“盘”清楚了。接下来要回答的问题才是关键:用什么框架、什么模型结构来处理这些多类别数据效率最高?
3. 训练方案选型:MMSegmentation 还是自建 DataLoader,预训练权重怎么接
3.1 框架选择:MMSegmentation 的成熟度与 Mask2Former 的新范式
遥感影像语义分割的框架选择,我在不同项目里试过三条路:MMSegmentation、PaddleSeg、以及自己写 PyTorch DataLoader 加模型。对这个约 4000 张的中等规模多类别数据集,我一般会按团队情况二选一。
MMSegmentation 的好处是模型库齐全,从 FCN 到 SegFormer、Mask2Former 都有现成实现,配置项覆盖了数据增强、优化器、学习率策略。它的配置机制上手有门槛,但一旦跑通,换模型只是换配置文件的成本。PaddleSeg 的集成度更高,但在纯 PyTorch 技术栈的团队里引入 Paddle 生态,反而增加维护负担。自己写 DataLoader 则适合已经明确模型结构的情况——比如你确定只用 U-Net 变体,而且数据格式特殊,需要做大量自定义预处理。
Mask2Former 是最近被反复讨论的多类别分割范式,它把语义分割统一为 mask 分类任务:先由 Transformer 解码器生成一组二值 mask 和对应的类别预测,再做最优匹配。这个思路在遥感多类别场景下有天然优势:不同类别地物的尺度差异大,道路细长、建筑密集、水体连片,基于 mask 的表示比逐像素分类更能保持目标的全局一致性。
3.2 预训练权重要不要勉强“对齐”:Cityscapes 权重的复用逻辑
很多人的第一反应是拿 ImageNet 预训练权重直接微调,但遥感影像和自然影像的分布差异很大,直接用效果并不好。更稳妥的做法是拿在 Cityscapes 或 Mapillary Vistas 上训练过的语义分割权重做初始化,因为这些数据同样包含道路、建筑、车、人这类城市地物,底层特征和中层结构特征与遥感场景更接近。
MMSegmentation 的做法是直接把model.backbone.init_cfg指向预训练权重文件,解码器部分从头训练。这里有一条经验:如果数据集类别数和 Cityscapes 的 19 类完全不同,分类头(decode head 的num_classes)必须改掉,但骨干网络权重可以完整加载。此时要留意加载时strict参数的设置。
# MMSeg 中加载预训练权重的伪代码,实际使用 config 文件中配置 # model = init_segmentor(config, checkpoint, device='cuda:0') # 如果 backbone 结构一致但分类头类别数不同,将 checkpoint 加载时的 strict 设为 Falsestrict=False的含义是允许部分键不匹配,比如 decode head 的conv_cls.weight形状变了,加载器会跳过这一层,只保留 backbone 的参数。如果你用的是 Mask2Former,还需要额外注意:它的 query embedding 和类别分类器与数据集类别数相关,同样需要重新初始化,不能图省事硬加载全部参数,否则会直接报 shape 错误。
3.3 损失函数与类别权重:多类别不均衡怎么处理
多类别数据最常见的现象就是背景类碾压前景类。我在这个数据集上默认使用带ignore_index的 CrossEntropyLoss,配合类别权重向量,权重一般按类别像素占比的倒数设置:
# 假设上一步统计出的类别像素占比为 ratio_dict num_classes = len(ratio_dict) total_pixels = sum(ratio_dict.values()) weights = torch.zeros(num_classes) for cls, ratio in ratio_dict.items(): weights[cls] = total_pixel_counts[cls] / total_pixels # 实际取倒数再归一化常见做法是取weights[cls] = 1.0 / sqrt(ratio[cls]),而不是直接取倒数。直接取倒数会让占比极小的类别权重过大,模型在早期震荡得很厉害。开根号是一个折中:背景权重降低但不至于完全失去梯度,小类别权重升高但不会过度放大噪声。
如果训练中 mIoU 一直上不去,还可以换成 Dice Loss 和 CrossEntropy 的组合,这个组合在道路分割这种前景稀疏的任务中稳定得多。说到底,框架选型在数据准备好之后只占 30% 的工作量,真正花时间的还是数据读取增强和训练监控。
4. 直接训练的落地清单:划分、装载、评估与参数备忘
4.1 数据划分:按瓦片而不是按文件随机分
遥感影像数据集经常来自同一区域的大图裁剪,相邻瓦片之间高度相似。如果按文件随机划分训练集和验证集,验证集里就会出现训练集“邻居”的影子的情况,导致指标虚高,部署到新区域时能力掉一半——这就是典型的数据泄露翻车现场。
正确做法是按瓦片来源分组。假设文件名里带了瓦片编号,例如tile07_x1024_y2048.png,划分时以tile07为粒度:
import glob from collections import defaultdict files = sorted(glob.glob("images/*.png")) tile_groups = defaultdict(list) for f in files: basename = f.split("/")[-1] # 形如 tile07_x1024_y2048.png tile_id = basename.split("_")[0] # tile07 tile_groups[tile_id].append(f) # 取前 80% 的瓦片作训练,其余作验证 all_tiles = list(tile_groups.keys()) split_idx = int(len(all_tiles) * 0.8) train_tiles = all_tiles[:split_idx] val_tiles = all_tiles[split_idx:]这比随机划分多写不了几行代码,却能从根源上避免“验证集指标好看、上线就翻车”的问题。如果你不确定文件名是否含有瓦片信息,可以用图像左上角坐标来分组,或者做一个简单的空间去重:计算相邻图像的平均像素差异,把相似度过高的图像放进同一组。
4.2 一个可直接改的 PyTorch Dataset 代码
这个数据集格式规整,我一般直接手写一个 Dataset 类,抛弃框架自带的数据接口。这样可以完全控制图像的读取、归一化和增强策略,也方便调试。
import cv2 import torch from torch.utils.data import Dataset import numpy as np IMG_MEAN = [0.485, 0.456, 0.406] IMG_STD = [0.229, 0.224, 0.225] class RemoteSensingDataset(Dataset): def __init__(self, img_paths, label_paths, image_size=512, augment=False): self.img_paths = img_paths self.label_paths = label_paths self.image_size = image_size self.augment = augment def __len__(self): return len(self.img_paths) def __getitem__(self, idx): img = cv2.imread(self.img_paths[idx]) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) mask = cv2.imread(self.label_paths[idx], cv2.IMREAD_UNCHANGED) # 随机裁剪到固定尺寸 h, w = img.shape[:2] if self.augment: start_h = np.random.randint(0, h - self.image_size) start_w = np.random.randint(0, w - self.image_size) else: start_h, start_w = (h - self.image_size) // 2, (w - self.image_size) // 2 img = img[start_h:start_h+self.image_size, start_w:start_w+self.image_size] mask = mask[start_h:start_h+self.image_size, start_w:start_w+self.image_size] # 归一化处理 img = img.astype(np.float32) / 255.0 img = (img - IMG_MEAN) / IMG_STD img = torch.from_numpy(img).permute(2, 0, 1).float() mask = torch.from_numpy(mask.astype(np.int64)) mask = mask.long() return img, mask这里IMG_MEAN和IMG_STD用的是 ImageNet 统计值,如果数据集的影像直方图差异很大,我建议先用自己的训练集重新统计。随机裁剪固定到 512×512 是为了控制显存,遥感原图如果直接整体进网络,一张 4096×4096 的图会直接让 24GB 显存爆掉。验证时用中心裁剪即可,不需要做增强。
4.3 mIoU 评估:按类别算,别被 90% 的背景淹没
训练时的 loss 下降不能直接证明模型好用,在遥感分割里唯一的权威指标是 mIoU(逐类别的 IoU 平均值)。这里最容易出现的错觉是:整体准确率到了 98%,看起来很好,但背景类占 95% 的像素,道路 IoU 只有 20%。所以评估代码必须按类别分开统计。
def compute_miou_per_class(pred, gt, num_classes): """pred 和 gt 都是整数类别索引的 numpy 数组""" ious = [] for cls in range(num_classes): pred_mask = (pred == cls) gt_mask = (gt == cls) intersection = np.logical_and(pred_mask, gt_mask).sum() union = np.logical_or(pred_mask, gt_mask).sum() if union == 0: iou = float('nan') # 该类在 GT 中不存在,不计入平均 else: iou = intersection / union ious.append(iou) return ious注意union == 0的情况,有的类别在当前影像中完全没有出现,这时如果记为 0 会拉垮整个验证集指标。我的习惯是不计入平均,在日志里单独标记为“缺失”。每类 IoU 单独打印,方便判断模型到底在哪一类上弱:道路类 IoU 低通常是边缘细长结构被下采样吞掉了;植被类 IoU 低往往是纹理复杂导致边界溢出。
4.4 训练监控和参数备忘
参数配置方面,我给这份约 4000 张的数据集一个保守的起点:Batch Size 8(取决于显存)、初始学习率 0.0001、里程碑式衰减策略(30 轮减半一次)、总训练 100 轮左右。如果用的是 AdamW,权重衰减设为 0.01;如果 RAdam,权重衰减可以设成 0.02。这里说的是“起手式”,不是精确最优值——数据分布变了参数就要动。
训练监控我一般每 100 个 iteration 打印一次 loss,每个 epoch 结束在验证集上跑 mIoU。可视化上随手保存几张验证集的预测叠加图,比看 loss 曲线更能发现模型问题。例如某个类别完全预测不出来,可视化图上会直接就是黑块,这种问题靠 loss 数字是看不出来的。
5. 避坑/常见问题:遥感多类别分割最容易翻车的 5 个地方
5.1 标签图和影像图数量一致,但文件名排序对不上
现象:训练时 loss 正常下降,但验证集 mIoU 异常低,可视化预测结果发现“预测的道路”位置和真实道路对不上。
原因:DataLoader里用了两个独立排序的sorted()列表,而影像和标签文件名并不是完全相同的字符串前缀,排序结果错位了一部分。
解决:不要依赖排序,用文件名映射构造 pair。
pairs = [] for img_path in img_list: basename = os.path.basename(img_path).replace(".tif", ".png") label_path = os.path.join(label_dir, basename) if os.path.exists(label_path): pairs.append((img_path, label_path))5.2 训练正常,但预测结果出现大量“椒盐噪声”小碎块
现象:道路的主干预测正确,但图像上到处都是散落的小碎块,后处理要花大量时间。
原因:训练时用了较大的学习率且没有配合多尺度增强,模型学到了高频噪声,缺少对地物一致性的约束。
解决:开启随机缩放增强(0.75 到 1.25 倍),将初始学习率降为原来的 1/2,并把损失函数换成CrossEntropy + DiceLoss的组合,能有效压制碎块。这算一分玄学,但在这个场景下屡试不爽。
5.3 所有类别 IoU 都很低,但训练集 loss 已经收敛到很小
现象:训练 loss 很低,验证 mIoU 却上不去,两者差距大。
原因:训练时做了随机裁剪,而验证时用了中心裁剪,裁到的区域内容分布不一致。遥感影像中心往往是道路交叉口或建筑密集区,边缘则可能是纯背景,验证结果自然波动。
解决:验证时做滑动窗口推理或整图缩放推理,不要用单次中心裁剪。尤其对道路这类细长结构,中心裁剪会把一条完整的路裁成两端,导致评估失真。
5.4 Mask2Former 训练时显存溢出,Batch Size 只能设为 2
现象:Mask2Former 的 Transformer 解码器非常吃显存,4000 张遥感图的高分辨率原图几乎无法直接训练。
原因:输入分辨率太高,默认的 10 个 object query 加上大尺度特征图,显存墙是必然的。
解决:把训练尺寸缩小到 512×512 或 640×640,Batch Size 设为 2 并用梯度累积模拟 8 的等效批次。梯度累积这一步在遥感大图场景里几乎是标配:
# 梯度累积配置示意 iter_to_accumulate = 4 optimizer.zero_grad() for i, (imgs, masks) in enumerate(train_loader): loss = model(imgs, masks) loss = loss / iter_to_accumulate loss.backward() if (i + 1) % iter_to_accumulate == 0: optimizer.step() optimizer.zero_grad()5.5 加了类别权重后,小类别反而出现大量误检
现象:道路占比低,加了倒数权重后路是找到了,但建筑和背景被大量误判成道路,precision 直线下降。
原因:权重给得过大,模型为了降低 loss 倾向于把边界模糊的像素都归入高权重类别,产生了过拟合式的激进预测。
解决:把weights[cls] = 1.0 / ratio[cls]改成1.0 / sqrt(ratio[cls]),或者在 loss 里引入一个边界像素的惩罚项。另一个有效手段是对高权重类别做随机侵蚀,让标签边缘少量像素不参与 loss 计算,防止模型过度依赖“猜它是路”来刷分数。
6. 进阶技巧:用 Mask2Former 做多类别分割时,我建议你多盯这三个指标
Mask2Former 把语义分割从“逐像素分类”变成“mask 分类 + 匹配”,在遥感影像多类别场景下表现确实好,但它也带来了新的“黑匣子”。我从踩坑经历里总结出三个必须额外盯住的点。
第一个是类别标签的置信度分布。逐像素分类模型出错很直观,Mask2Former 的预测是每个 mask 提供一组置信度,有时候 mask 的形状预测得很好,但类别标签置信度是软分布,不取 argmax 根本看不出模型在犹豫什么。建议在验证集上把每个 mask 的 top-2 类别置信度差小于 0.1 的样本单独打印,这些就是最容易混淆的道路与裸地、建筑与阴影。
第二个是 query 的有效利用率。Mask2Former 的 transformer decoder 会用定数量的 query 去匹配目标,如果训练集中背景占绝对主导,大量 query 会被训练成“空 query”,在推理时产生空 mask 或低面积 mask,白白浪费计算量。我习惯把推理结果里面积小于 10 像素的 mask 单独统计出来,如果超过三成,就需要增加背景类的辅助监督,或者把背景类的损失权重提高,让 query 学到更明确的“不做预测”信号。
第三个是类别粒度对匹配策略的影响。Mask2Former 在训练时要算预测 mask 和 GT mask 的二分图匹配,匹配代价里包含类别准确度。当类别数从二分类涨到五分类以上时,匹配代价的权重需要重新调,否则早期训练会被类别信息主导,mask 形状学不出来。我的经验是把 mask 损失权重(Dice 和 CE)从默认的 5.0 调高到 10.0 左右,类别损失权重保持 2.0 不变,这样模型先学会把形状分割对,再学贴标签。
这套“盯指标”的习惯帮我避免了不止一次模型上线后的返工。毕竟对 4000 张遥感影像这样一个已经处理妥当的数据集,模型选型和调参才是真正的临门一脚。数据预处理这部分的功夫已经被省下来了,我更希望把省下来的时间都用在这种刀刃上。如果你照这套流程走一遍,你会发现遥感道路分割没有想象中那么玄学,绝大部分问题都能被清晰地归因到数据、加载或训练策略上。希望帮到你。
本文还有配套的精品资源,点击获取