简介面向遥感场景下的目标图像语义分割任务这套数据集中包含水体、交通运输、建筑、耕地、草地等8个典型地物类别并提供对应的图像与语义标签适合用于深度学习分割模型的训练与精度验证也适用于算法对比、课程设计和毕业设计。数据已预先划分为训练集和验证集训练集、验证集下均设 images 与 masks 目录能直接导入主流训练框架无需额外切分与整理随包说明文件标注了8个分割类别的名称与索引方便实验前对照。压缩包共2000个文件以1818张PNG图像和180张JPG图像为主体辅以1个类别说明TXT和1个Python可视化脚本总大小约30.81MB。可视化脚本会随机抽取某张图片自动生成原始图、GT图以及GT在原图上的蒙板对比帮助快速检查标注质量、观察模型预测效果。目前已有76人浏览学习比较适合遥感图像分析与语义分割领域的入门学习者及中高级研究者直接下载使用。1. 遥感场景下的目标图像语义分割数据集打开压缩包之后先看什么做遥感语义分割的人应该都经历过这种尴尬论文里写得漂漂亮亮代码跑起来却总在数据环节翻车。要么下载的数据集没有划分好训练集验证集混在一起要么标签是灰度图还是RGB真值图都分不清要么一张张图尺寸参差模型一训练就爆显存。这份遥感场景下的目标图像语义分割数据集约5,000张图片和对应标签是把这些坑提前填掉的那类资源8个类别覆盖水体、交通运输、建筑、耕地、草地等常见地物训练集约1800张、验证集约800张已经分开目录存放还附带可视化脚本方便你抽检质量。适合跑Unet、DeepLabV3、SegFormer这类语义分割模型的读者也适合刚入手遥感方向、想拿一份有标签数据练手的学生。下面我按“先摸清结构、再写加载、再做可视化、最后进模型”的顺序拆开讲。2. 目录结构与标注规范先搞清楚标签是灰度索引还是RGB真值图2.1 拿到压缩包后的第一件事核对目录和组织方式很多下载来的数据集随手扔在桌面解压之后直接开写代码结果路径写错、标签格式没核实白白折腾半天。我一般拿到任何分割数据集第一件事是解压后用一条命令把目录树打出来而不是急着写PyTorch的Dataset类。这份数据集预设的组织方式比较清晰训练集和验证集各自包含images和masks两个目录images放原始遥感影像masks放对应的分割标签。按照常见做法images里是RGB三通道的JPG或PNGmasks里是单通道的索引图或者RGB真值图这一步必须实测确认。unzip segmentation_dataset.zip -d ./remote_seg cd ./remote_seg find . -maxdepth 3 -type d | sort ls train/images | head -5 ls train/masks | head -5第一段命令把压缩包解压到remote_seg目录然后通过find列出三层以内的目录结构确认train和val下确实各自有images和masks。第二段命令抽查train目录下的图片文件名确保图片和标签文件名能一一对应。踩过最典型的坑是数据集作者把原图放在images把标签放在masks但两边的文件名后缀不同一个叫87199.jpg另一个叫87199.png或者前缀里多了_mask。这时候写加载代码就得自己处理后缀差异不能直接字符串替换。验证标签格式用的是一段很短的Python代码把一张标签图读进来打印shape和取值情况from PIL import Image import numpy as np mask Image.open(remote_seg/train/masks/87199.png) arr np.asarray(mask) print(arr.shape, arr.dtype, np.unique(arr))我这里用PIL打开mask后转成numpy数组打印三个关键信息数组形状、数据类型、唯一的取值有哪些。如果打印结果是(H, W)且dtype是uint8、np.unique(arr)返回[0, 1, 2, ...]这样的小整数说明标签是单通道索引图直接在loss里用就行。如果打印结果是(H, W, 3)且取值是[0, 255]或RGB三元组说明是彩色真值图必须再做一步映射才能当训练标签用。2.2 8个类别的映射关系与classes文件关于类别摘要里点名了水体、交通运输、建筑、耕地、草地等还提到具体参考classes文件。这个classes文件一般是txt或yaml格式每行一个类名。以我处理过的多数遥感分割数据集为例常见约定是0表示背景或未标注区域从1开始才是真实类别也可能直接从0开始就是水体。这里不做猜测以压缩包里的classes文件为准但更稳妥的做法是把类别名顺序读出来动态生成索引映射表。with open(remote_seg/classes.txt, r) as f: class_names [line.strip() for line in f.readlines()] print(class_names) id2name {i: name for i, name in enumerate(class_names)} name2id {name: i for i, name in enumerate(class_names)} print(id2name)这段代码把classes.txt里的每行类名读进列表然后生成两个方向的映射字典。训练时用id2name在可视化阶段把预测结果转成类名验证计算mIoU时用name2id过滤不需要参与评分的类别。参数说明enumerate从0开始编号如果你的classes.txt第一行是“background”或者“background”那第0类就是背景如果第一行直接是“water”那就说明这个数据集没有单独的背景类训练时要额外留意因为测试时模型可能会把未知区域强行划分到某个已知类别里。遥感语义分割的标签规范和医学图像领域不太一样医学分割常用三通道RGB掩码红绿蓝分别对应不同器官而遥感领域更偏向直接用灰度索引图因为遥感类别动辄十几类用RGB真值图虽然肉眼看着直观处理起来却多一道色彩映射工序。这份数据集的8类设定大概率是单通道索引图这也是我推荐直接用PIL打开后看np.unique的原因。2.3 标签值编码为什么要区分0和255分割数据集里最隐蔽的坑是背景标成0还是标成255。很多遥感数据集把无标注区域标成2550留给真实背景类别也有反过来用0做背景的。如果我的模型习惯是忽略255结果这份数据集用0表示背景那loss计算时会默认把所有背景都忽略掉训练过程直接失控。python - EOF import numpy as np from PIL import Image import glob masks sorted(glob.glob(remote_seg/train/masks/*.png)) for p in masks[:20]: arr np.asarray(Image.open(p)) print(p, np.unique(arr), arr.max()) EOF这段脚本用glob把所有mask路径排好序循环前20张打印每个标签图的unique取值和最大值。实战里这样扫一遍就能立刻判断背景约定如果arr.max()等于255说明存在255这种特殊像素需要映射到0如果unique值都在7以内说明标签已经是很干净的索引图直接可以用。我曾遇到一个数据集99%的mask里最大值都是7唯独某几张图的边界区域出现255如果不做这步排查训练到一半就会因为“target中的值8超出类别数8”而崩溃。参数层面255的处理方式一般是加载时把255统一替换成0或某个固定的ignore indexarr np.asarray(Image.open(p)) arr np.where(arr 255, 0, arr).astype(np.int64)这里把255强制改成0适用于标签生成时把无标注区域默认填了255的数据集。如果背景类在数组里占多数这种改法会导致背景类样本量爆炸配合类别权重能缓解如果255本来就不是合理像素而是标注软件的填充默认值那改成0就是最省事的做法。至于0和255哪个才是真正的背景取决于数据集作者的标注规则日常处理遥感数据集时一定要在训练前做一次全量扫描不要采样几张就下结论。3. 训练集与验证集的划分逻辑PyTorch Dataset加载与类别权重3.1 数据划分的合理性1800/800和随机划分的区别这份资源直接给出了划分好的训练集和验证集比例大致是1800比800约七三开。对于遥感语义分割这种对分布敏感的任务先划分再交付是负责任的。自己随机拆分最怕的是同一个小区的多张影像同时进了训练集和验证集空间上高度相关验证分数虚高模型一上真实场景就原形毕露。数据集作者按照某种策略已经划分好省掉了这份担心我们拿到后要做的不是重新划分而是确认两个集合的地物类别分布是否接近。实际操作中我会对训练集和验证集分别统计每个类别的像素占比import numpy as np from PIL import Image import glob def compute_class_hist(mask_paths, num_classes8): hist np.zeros(num_classes, dtypenp.int64) for path in mask_paths: arr np.asarray(Image.open(path).convert(L)) hist np.bincount(arr.flatten(), minlengthnum_classes)[:num_classes] return hist train_masks glob.glob(remote_seg/train/masks/*.png) val_masks glob.glob(remote_seg/val/masks/*.png) print(train hist:, compute_class_hist(train_masks)) print(val hist:, compute_class_hist(val_masks))这段代码定义了一个统计函数对每张mask转灰度后做bincount统计每个类别出现的像素总数。核心参数是num_classes要根据classes.txt实际行数修改。用灰度索引图时这里有个隐藏细节如果mask是三通道RGB真值图直接.convert(L)会把不同颜色的对比度打乱统计结果完全不可信所以这个脚本同样依赖上一章已经确认过的“标签是单通道索引图”这个前提。如果两个分布的偏差在几个百分点以内说明划分基本均匀。如果发现验证集里水体像素占比明显高于训练集那模型在验证集上的mIoU会失真。这种情况的处理方式是重新做一次类别平衡采样或者干脆相信作者已经把边界考虑进去了。遥感场景下建筑物和交通运输类在空间上经常混在一起高层建筑密集区和道路网在影像上高度相似两层类别如果分布不均训练时模型容易把楼房屋顶当成道路这是必须心里有数的。3.2 自定义Dataset的写法读图、映射标签、统一尺寸接下来写PyTorch的Dataset类。这部分的要求是路径读取必须稳健、标签映射必须明确、尺寸统一策略必须和后续模型输入一致。import os import numpy as np from PIL import Image from torch.utils.data import Dataset class RemoteSegDataset(Dataset): def __init__(self, images_dir, masks_dir, class_names, image_size(512, 512), use_mappingTrue): self.image_paths sorted(os.listdir(images_dir)) self.mask_paths sorted(os.listdir(masks_dir)) self.images_dir images_dir self.masks_dir masks_dir self.image_size image_size self.class_names class_names num_classes len(class_names) if use_mapping: self.id_map {i: i for i in range(num_classes)} self.id_map[255] 0 else: self.id_map None def __len__(self): return len(self.image_paths) def __getitem__(self, idx): img_name self.image_paths[idx] mask_name self.mask_paths[idx] img Image.open(os.path.join(self.images_dir, img_name)).convert(RGB) mask Image.open(os.path.join(self.masks_dir, mask_name)).convert(L) img img.resize(self.image_size, Image.BILINEAR) mask mask.resize(self.image_size, Image.NEAREST) img_np np.asarray(img).astype(np.float32) / 255.0 mask_np np.asarray(mask).astype(np.int64) if self.id_map is not None: mask_np np.vectorize(self.id_map.get)(mask_np) img_tensor torch.from_numpy(img_np).permute(2, 0, 1).contiguous() mask_tensor torch.from_numpy(mask_np).long() return img_tensor, mask_tensor这份代码的核心有四点。第一图片转RGB、标签转L模式分别保证三通道输入和单通道标签即便数据集里混入了灰度原图也不会出错。第二图片用双线性插值缩放到512×512标签必须用最近邻插值因为标签是离散类别索引双线性会把1和2插值成1.5这样的无效值这是分割任务最容易翻车的地方。第三像素值除以255归一化到0到1区间很多初学者跳过归一化直接喂给网络导致训练初期loss乱跳。第四id_map是低成本的255清理方案用vectorize逐像素映射速度比循环快得多但严格针对本章前面扫描确认过的标签约定如果你的数据集里255不是无标注区域这个id_map就不该启用而是把255当成一个普通类别参与训练或者单独加ignore机制。参数层面的调整空间image_size一般设为512或1024取决于显卡显存。Unet类模型输入512时单卡可以跑得动SegFormer这种Transformer结构要求输入尺寸固定512比较稳妥。如果你的遥感影像原始分辨率是上千像素直接缩到512会丢失大量小目标细节小房子、小汽车几乎不可辨识。更合理的做法是随机裁剪512×512的patch让模型在原分辨率上做局部感知。数据集本身给的是一整张大图训练时用随机crop的收益比全图resize高不少。3.3 类别不平衡的初始处理从统计直方图到cross entropy权重遥感场景下类别不平衡是常态水体、草地、耕地往往占据影像大部分面积建筑和交通运输这些细碎目标只占一小部分。如果不做任何处理模型训练完会发现把所有像素都预测成水体就能拿到很低的lossmIoU惨不忍睹。计算类别权重的标准做法是用像素频率的倒数做归一化train_masks glob.glob(remote_seg/train/masks/*.png) hist compute_class_hist(train_masks) total hist.sum() weights total / (hist 1e-6) weights weights / weights.sum() * len(weights) print(weights)这段代码完全复用了前面统计直方图的函数先算每个类别的像素总数然后用像素总数除以各类别像素数得到倒数值加1e-6防止除零。紧接着做一次归一化让权重均值为1数值范围稳定在0到几之间。最后打印可以直接复制进训练的权重列表。把权重喂给loss是PyTorch里最直接的方式criterion torch.nn.CrossEntropyLoss(weighttorch.from_numpy(weights).float())如果你用的是Unet训练时weight参数生效模型对建筑这类小目标类别的梯度会被放大。但类别权重不是越高越好如果某个类别只有几百个像素权重会被拉到一个很大的数值导致这类样本的loss剧烈震荡模型反而学不好。常规做法是给权重设置上限比如最多不超过平均权重的5倍或者使用平滑后的分布来计算权重。这也算分割训练里的门玄学权重上限调得好mIoU能涨一个点调得不好直接梯度爆炸。4. 可视化脚本拆解原始图、GT、蒙版三图同台验证数据质量4.1 为什么要先可视化再训练训练跑起来之前一定花几分钟做可视化。数据管道里最容易出的问题不是路径写错而是图上内容与标签对不上原图是一处农田mask里对应的却是一片建筑轮廓或者原图整体色调偏暗但mask边缘明显偏移了几个像素。这些问题只有肉眼看过才能发现单靠np.unique这类数字检查根本看不出来。这份数据集附带一个可视化脚本随机抽一张图把原始图片、GT标签图、GT在原图上叠加蒙版的效果三张图展示出来并保存到当前目录。这也是我处理任何数据集的第一步操作功能上相当于给数据做一次“体检”。4.2 脚本核心逻辑切片加载与三图合并下面这段代码是我按同等工作量重写的可视化脚本兼容了“标签可能是索引图也可能是RGB图”两种情况import os import random import numpy as np from PIL import Image import matplotlib.pyplot as plt def visualize_random(images_dir, masks_dir, save_dirvisual_out): os.makedirs(save_dir, exist_okTrue) names sorted(os.listdir(images_dir)) pick random.choice(names) img Image.open(os.path.join(images_dir, pick)) mask Image.open(os.path.join(masks_dir, pick)) if mask.mode ! L: mask mask.convert(L) img_np np.asarray(img) mask_np np.asarray(mask) overlay img_np.copy() overlay[:, :, 0] np.where(mask_np 0, 255, overlay[:, :, 0]) overlay[:, :, 1] np.where(mask_np 0, 0, overlay[:, :, 1]) overlay[:, :, 2] np.where(mask_np 0, 0, overlay[:, :, 2]) fig, axes plt.subplots(1, 3, figsize(15, 5)) axes[0].imshow(img_np) axes[0].set_title(Original) axes[1].imshow(mask_np, cmapgray) axes[1].set_title(GT Mask) axes[2].imshow(overlay) axes[2].set_title(Overlay) for ax in axes: ax.axis(off) plt.savefig(os.path.join(save_dir, fcheck_{pick}.png), bbox_inchestight, dpi150) plt.close() print(fsaved to {save_dir}/check_{pick}.png)这段代码做了三件事随机选图、生成蒙版叠加图、三图并排保存。参数说明np.where(mask_np 0, 255, overlay[:, :, 0])的含义是把mask中所有非0像素对应的位置在原图红色通道上设置为255绿色和蓝色通道设置为0这样非0区域整体呈现红色植被、水体、建筑一目了然。这里把mask0当作“有标注区域”的依据如果你的数据集里0代表水体、1代表建筑这种类别顺序那么红色蒙版代表的是“所有非背景区域”而不是具体某一类。想看单一类别就把mask_np 0改成mask_np 2这类条件。运行这个脚本如果看到三张图内容错位、蒙版轮廓和地物边界明显不吻合那就说明图像和标签存在配准问题这个数据集的可用性需要重新评估。如果看到的是一张干净整齐的蒙版覆盖在建筑物或水域上数据质量基本过关可以放心往下一步走。4.3 保存文件名与批量可视化单张随机检查的偶然性比较大我习惯把随机抽一张改成批量质量抽检遍历整个训练目录每100张抽1张把带文件名缩略图的HTML报告生成出来。不过数据集提供的脚本已经能满足“随机抽一张快速确认”日常用完全足够。如果后续要调大保存图的尺寸把figsize改到(20, 6)dpi调高到200出来的图可以放进论文附录里当数据展示。保存的PNG文件默认在visual_out目录下不会污染原始数据集目录这点设计比较省心。5. 避坑指南遥感语义分割数据加载与训练的四个经典翻车现场5.1 mask彩色图和灰度图的误判现象加载mask时报错或者np.unique(arr)打印出来的全是乱码一样的三元组。把mask用Image.open打开后打印mode是RGB而不是L。原因数据集作者在导出标签时保留了RGB真值图格式每个类别用一种颜色表示而不是把类别索引写进灰度值。这种情况在遥感数据集里很常见因为RGB真值图便于人工目视检查也方便在GIS软件里叠加显示。解决先确认classes.txt里各类别对应的RGB颜色表然后写一个色彩映射函数。如果没有现成颜色表就把np.unique(arr.reshape(-1, 3), axis0)打印出来用类名和颜色手动建立映射关系。我习惯的做法是先打印所有颜色列表from PIL import Image import numpy as np mask Image.open(remote_seg/val/masks/xxxx.png) arr np.asarray(mask) colors np.unique(arr.reshape(-1, 3), axis0) print(colors)这段代码把每个像素的RGB三元组收集起来去重得到的就是这张图里出现的全部颜色。然后用这份颜色列表去对照classes文件把每个RGB颜色替换成对应的类别IDcolor2id {(0, 0, 0): 0, (128, 0, 0): 1, (0, 128, 0): 2} def rgb_to_index(arr): h, w, _ arr.shape out np.zeros((h, w), dtypenp.int64) for rgb, idx in color2id.items(): out[(arr np.array(rgb)).all(axis2)] idx return out这段代码定义了一个颜色到类别ID的字典然后遍历字典把每个RGB像素位置替换成对应的索引值。核心逻辑是(arr np.array(rgb)).all(axis2)它先比较每个像素的三通道是否等于目标RGB颜色再用all确保三个通道同时相等得到布尔mask。这个方案的代价是遍历字典颜色类别少时没问题如果类别有几十种建议改成向量化查表的方式。从那以后我每次收数据集都会先写一段脚本扫mask的unique颜色确定是灰度还是RGB再动手写训练代码。5.2 mask中255像素导致训练报错现象训练到第一个epoch结束前loss计算突然抛出异常提示CUDA error: device-side assert triggered细看是target中的数值超出了类别数范围。原因个别mask图片在标注时使用了255作为“未标注区域”的填充值而模型输出通道数只有8CrossEntropyLoss对target的合法范围有严格要求255不在0~7之间直接触发断言的报错。解决训练前写一次全量扫描查看所有mask中的最大值和unique取值把255统一替换成0或设置ignore_index。建议的做法是加载时做np.where(arr 255, 0, arr)同时在loss里设置ignore_index-1并先把target中255替换成-1或者在数据集__getitem__里做映射。如果不想污染数据也可以把255当作背景处理进类别权重但效果不如直接替换。5.3 resize时标签插值方式错误导致边缘错位现象模型分割建筑物时预测结果的边缘总是沿着一个方向偏移1到2个像素或者有小锯齿。原因图片用了双线性插值缩放到统一尺寸mask也用了同样的双线性插值。双线性插值在类别索引图上会造成中间值比如类别2的像素和类别3的像素交界处插出一个2.5取整后变到2或3边界不确定性增加。更严重的是一张mask从原始尺寸缩放时某个细窄的道路类别可能在插值过程中被抹掉。解决图片用Image.BILINEAR或cv2.INTER_LINEARmask必须用Image.NEAREST或cv2.INTER_NEAREST。这个做法没有例外任何分割任务都适用。如果原始mask是RGB真值图先转索引图再resize避免颜色通道各自插值后颜色值混叠。5.4 验证集mIoU虚高而实际效果差现象训练时验证集mIoU有0.75部署到新的遥感影像上却连0.3都不到。原因验证集和训练集来自同一批影像空间相关性太强。遥感影像里相邻区域的地物分布是高度自相关的如果验证集里包含的训练区域附近的路网、水体纹理特征模型相当于“开卷考试”。另一个常见原因是验证集只挑质量高的图那些有云遮挡、有噪声的图都被排除在外掩盖了模型的真实水平。解决用这份数据集时如果要做公开发表的实验最好自己再按地理位置或者时间信息划分一个测试集不要用原有的val目录做最终精度评估。更简单的做法是在训练中不断用第4章可视化脚本抽样预测结果看模型在陌生影像上的表现。如果发现原有val上mIoU高但视觉质量差把训练策略调整为多尺度训练加随机裁剪能显著提升泛化能力这一招对遥感场景尤其有效。6. 从数据集到模型Unet输入约定与一次性跑通的几个验证技巧数据集本身不包含模型代码但摘要里提到有图像分割网络专栏里面整理过U-Net、SwinUnet等结构的改进。这里把数据到模型的最后一段路程补齐如何用这份数据集快速验证模型是否能够正常训练推理。先把输入侧约定固定下来。我常用的模板是batch size设为8、输入尺寸512×512、类别数从classes.txt读取。训练脚本里写一小段回调每200步从验证集抽4张图做预测并保存效果比只看loss曲线直观得多。可以用如下片段快速接上模型model UNet(in_channels3, num_classeslen(class_names)) img_tensor, mask_tensor next(iter(train_loader)) with torch.no_grad(): logits model(img_tensor) pred logits.argmax(dim1) print(pred.shape, pred.dtype, pred.unique())这段代码用第一张随机图直接过模型logits.argmax(dim1)把网络输出的各类别分数转换成最终的类别索引。打印pred.unique()可以立刻确认输出类别数是否在预期范围内。这样做的价值在于把数据管道的验证和模型的验证解耦如果数据环节有问题模型压根跑不出合理的unique值如果数据没问题这一步秒过。关于mIoU的计算很多人直接拿预测结果和GT对比忽略ignore_index或者把背景也算进指标里。这份数据集默认8类如果某个类别在验证集里没有出现IoU的分母为0程序直接报错或输出nan。我习惯用的方式是from sklearn.metrics import jaccard_score pred_flat pred.cpu().numpy().flatten() gt_flat mask_tensor.cpu().numpy().flatten() valid gt_flat 8 iou_per_class jaccard_score(gt_flat[valid], pred_flat[valid], averageNone, labels[0,1,2,3,4,5,6,7]) print(per-class IoU:, iou_per_class) print(mIoU:, iou_per_class.mean())这里用sklearn的jaccard_score逐类计算IoUaverageNone返回每个类别的IoU数组。valid gt_flat 8这一步把标注里可能残留的无效值过滤掉防止标签中混入255导致计算异常。这个验证逻辑适用于任何分割模型不限于Unet换SwinUnet或者SegFormer时也建议保留。一个容易被忽视的细节训练完成做推理时原图尺寸不是512×512直接resize再predict会产生边缘畸变。遥感影像的常见推理技巧是滑窗裁剪把大图切成512×512的重叠patch预测后拼接回去重叠区域取平均概率能够有效抑制拼接缝。我一般把这一步写成工具函数放进去配套这个数据集会很顺手。从拿到这份数据到模型跑通的完整链路其实就是数据目录核对、标签格式确认、Dataset加载、可视化验证、模型前向。每一步都踩过坑尤其是标签格式的确认看似小事实则是分割项目里最容易翻车的黑匣子。从那以后我每次收到新的分割数据集都会强制把第2章的扫描脚本和第4章的可视化脚本先跑一遍确认标签格式正确再进模型。希望这份拆解能帮你跳过这些弯路也希望你手头这份遥感数据跑出来的mIoU能配得上算法付出的努力。本文还有配套的精品资源点击获取