简介:面向遥感图像语义分割任务的深度学习数据集,围绕山川、湖泊等全景地物提供像素级标注,适合目标检测与分割方向的初学者及研究者直接用于模型训练与效果验证。包内共2000个文件,主要为1999张JPEG格式的影像及对应mask标注图,另含1个Python可视化脚本,整体压缩包约158.56MB,采用清晰的文件夹结构存放,无需额外清洗即可接入现有训练流程。数据集已划分为训练集与测试集,其中训练集包含12814张图像及同等数量掩码,测试集包含3203张图像及掩码,覆盖多种地形场景,可直接支撑U-Net、DeepLabV3等常见分割网络的训练和评估。附带脚本可随机抽取一张影像,将原始图、GT掩码图以及GT叠加到原图的展示结果保存到当前目录,便于快速预览标签质量。目前已有738人学习使用,适合需要获取高质量遥感分割数据并快速搭建实验的开发者。
1. 深度学习图像分割数据集为什么要挑遥感场景:先看边界再看参数
做深度学习图像分割的人,十有八九在公开数据集上踩过同一个坑:下载下来发现标注是坏的、类别对不上、训练集和测试集混在一起,光清洗数据就花掉两三天。这个遥感图像下山川、湖泊全景分割数据集,我最看重的不是它 170MB 的体积,而是它直接给好了 train/test 划分,训练集 12814 张、测试集 3203 张,每张都有对应的 mask。也就是说,你拿到的是「已经处理完」的状态,不是半成品。
数据集的内容是遥感视角下的山脉、湖泊全景图,属于典型的语义分割任务——把每个像素归到「山」「水」「其他」这类类别里。它适合三类人:正在入门图像分割、需要一个干净数据集跑通 UNet 或 DeepLab 全流程的人;做遥感相关课题、需要与自然场景不同的数据分布来验证模型泛化能力的人;以及想拿一个中等规模数据集测试自己数据增强、loss 改进效果的熟手。在往下拆目录和脚本之前,先记住这个结论:这份资源的价值在「划分好 + 格式完整 + 附带可视化脚本」,而不是数据量本身有多大。
2. 数据目录与标注格式:images 与 masks 的对应关系怎么读
2.1 先看目录结构:为什么说它拿到就能训练
一个图像分割数据集能不能直接喂给模型,不看图片多不多,看的是目录结构和命名规则。常见的数据集要么是 VOC 的 JPEGImages + SegmentationClass 结构,要么是 COCO 的 json 标注,要么是 Cityscapes 的 gtFine 多级目录。这个数据集用的是最朴素也最稳妥的 images + masks 双目录结构:
data/ ├── train/ │ ├── images/ # 12814 张 jpeg 原图 │ │ ├── 000001.jpeg │ │ └── ... │ └── masks/ # 12814 张 mask,与 images 同名一一对应 │ ├── 000001.png │ └── ... └── test/ ├── images/ # 3203 张 jpeg 原图 └── masks/ # 3203 张 mask我一般拿到任何数据集,第一件事是写三行代码检查文件和 mask 名字能不能对上、数量是否一致。别觉得多余,我见过不止一次 images 比 masks 多几张、或者 mask 是灰度图但通道数是 3 的情况。做分割任务时,mask 必须是单通道标签图,类别用整数编码——如果 mask 是三通道彩色图,多数模型的CrossEntropyLoss直接报错。
2.2 mask 像素值的含义:分割任务最容易翻车的地方
说一个血泪经验:很多新手拿到遥感图像分割数据集,以为 mask 里颜色一样的像素就代表同一类,直接拿 RGB 值去算损失。这在可视化时没问题,但训练时必须把 RGB 映射成类别索引。遥感山川湖泊的 mask 常见的标注方式有两种:一种是像 ADE20K 那样的调色板模式,每种类别对应一个固定 RGB 三元组;另一种是直接用灰度值 0、1、2 编码类别。
这个数据集的 mask 以 png 格式提供,建议按「灰度标签图」处理,而不是按彩色图处理。拿到后先跑一个统计脚本,确认 mask 里到底有哪些像素值,再决定类别数。我通常的做法是:
from PIL import Image import numpy as np import glob mask_paths = sorted(glob.glob("data/train/masks/*.png"))[:10] unique_vals = set() for mp in mask_paths: mask = np.array(Image.open(mp).convert("L")) # 强制转单通道灰度 unique_vals.update(np.unique(mask).tolist()) print("当前前10张mask包含的像素值:", sorted(unique_vals)) # 期望输出类似 [0, 1, 2],分别代表 背景/山脉/湖泊这段代码有两个关键点。convert("L")是把 png 转成单通道灰度,避免后续通道维度错乱;np.unique统计像素值分布,直接告诉你类别数量。如果跑出来像素值不是从 0 开始的连续整数,就要注意后续 dataloader 是否需要做映射。参数上唯一要改的是mask_paths的路径前缀,Windows 用户要小心反斜杠,建议统一用绝对路径或os.path.join拼接。
2.3 训练集与测试集的划分逻辑:不用自己再切
这个数据集已经按 12814 / 3203 的比例切好了 train 和 test,省掉了一个非常容易出错的步骤:随机切分造成的类别分布不均衡。遥感图像里「山」和「水」的占比天然不均——一张全景图里可能 80% 是山、5% 是水、15% 是其他,如果随机切分,可能某一折测试集里几乎没有水,导致 mIoU 虚高或虚低。
既然数据集自带划分,我建议 train 和 test 直接按目录分别加载,不要自己重新 shuffle 合并再切。原因很简单:你已经不知道原作者的划分依据是什么,可能是按区域、按时间、按场景类型分的,自己重切反而引入分布偏差。测试集的价值就是「模型没见过的数据」,用现成的划分最能反映真实泛化效果。
3. 基于该数据集训练 UNet:预处理、数据加载与关键参数
3.1 预处理怎么做:遥感图像的均值方差和自然图像不一样
遥感图像和 ImageNet 自然图像的光谱分布差别很大。用 ImageNet 上算出来的 mean=[0.485, 0.456, 0.406] 和 std=[0.229, 0.224, 0.225] 去做归一化,不能说错,但肯定不是最优。我自己碰到的实践经验是:遥感影像因为植被、水体、裸地的光谱特性很明显,数据集的 RGB 均值和标准差常常会偏向绿色和暗色通道。
如果不想自己算统计量,起步阶段直接沿用 ImageNet 归一化参数也没问题;但如果想榨干这个数据集的性能,花 30 秒跑一下全量统计是值得的:
import numpy as np from PIL import Image import glob from tqdm import tqdm img_paths = sorted(glob.glob("data/train/images/*.jpeg"))[:2000] # 抽样2000张足够 pixel_sum = np.zeros(3) pixel_sq_sum = np.zeros(3) count = 0 for p in tqdm(img_paths): img = np.array(Image.open(p).convert("RGB")).astype(np.float32) / 255.0 pixel_sum += img.sum(axis=(0, 1)) pixel_sq_sum += (img ** 2).sum(axis=(0, 1)) count += img.shape[0] * img.shape[1] mean = pixel_sum / count std = np.sqrt(pixel_sq_sum / count - mean ** 2) print("Mean:", mean.round(3)) print("Std:", std.round(3))这段代码用了抽样统计而不是全量统计,因为 12814 张图全跑一遍要几分钟,抽样 2000 张的均值和全量均值误差通常在 0.01 以内,完全够用。count累计的是像素总数而不是图片数,因为每张图的尺寸可能不完全一致。算出来的 mean 和 std 直接替换掉 dataloader 里的 ImageNet 参数即可。这是性价比极高的一个优化——几乎零成本,但对收敛速度和最终精度有实打实的影响。
3.2 数据加载:用 Dataset 类把 images 和 masks 绑在一起
PyTorch 训练分割模型时,最核心的自定义逻辑都在Dataset类里。你需要保证两个维度不出错:一是图片和 mask 要同时变换、且变换方式一致,不能图做了随机裁剪而 mask 没做;二是 mask 的像素值是整数标签,不能经过归一化或 normalization 操作。
一个我常用的骨架如下:
import torch from torch.utils.data import Dataset from PIL import Image import numpy as np import glob import os class RemoteSegDataset(Dataset): def __init__(self, image_dir, mask_dir, transform=None, mask_transform=None): self.image_paths = sorted(glob.glob(os.path.join(image_dir, "*.jpeg"))) self.mask_paths = sorted(glob.glob(os.path.join(mask_dir, "*.png"))) assert len(self.image_paths) == len(self.mask_paths), \ f"images {len(self.image_paths)} != masks {len(self.mask_paths)}" self.transform = transform self.mask_transform = mask_transform def __len__(self): return len(self.image_paths) def __getitem__(self, idx): image = Image.open(self.image_paths[idx]).convert("RGB") mask = Image.open(self.mask_paths[idx]).convert("L") if self.transform is not None: # 注意:这里只对image做变换 image = self.transform(image) if self.mask_transform is not None: # mask用独立的transform,保证插值方式是最近邻 mask = self.mask_transform(mask) mask = torch.as_tensor(np.array(mask), dtype=torch.long) return image, mask这个类里有三个设计细节值得留意。第一,assert检查文件数量一致性,宁可训练前崩溃也不能训练到一半发现数据错位。第二,image 的 transform 和 mask 的 transform 分开传参,这样后续可以给 mask 单独设置InterpolationMode.NEAREST——mask 不能做双线性插值,否则边界会出现「类别混血」的像素值,比如类别 1 和类别 2 之间插值出 1.4,计算损失时直接报错或乱分。第三,convert("L")转灰度后,再交给torch.as_tensor(..., dtype=torch.long)保证标签是 long 类型,这是 PyTorchCrossEntropyLoss的硬性要求。
3.3 transform 配置:Mask 变换的边界坑
训练分割模型,transform 配置的坑比模型结构还多。torchvision.transforms里很多操作默认插值是双线性,这对图片没问题,但对 mask 是灾难。我推荐的配置方式如下:
from torchvision import transforms from torchvision.transforms import InterpolationMode image_size = 512 image_transform = transforms.Compose([ transforms.Resize((image_size, image_size), interpolation=InterpolationMode.BILINEAR), transforms.RandomHorizontalFlip(p=0.5), transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2), transforms.ToTensor(), transforms.Normalize(mean=[0.4, 0.45, 0.35], std=[0.2, 0.18, 0.22]) # 遥感图经验值 ]) mask_transform = transforms.Compose([ transforms.Resize((image_size, image_size), interpolation=InterpolationMode.NEAREST), transforms.RandomHorizontalFlip(p=0.5), transforms.ToTensor() # 这里只做数值归一化,不做Normalize ])参数上有几个关键点。image_size=512对这个 170MB 的数据集来说是个甜点值——显存不够的话可以降到 384 或 256,但山川湖泊的边界细节比较依赖分辨率,256 会明显损失边缘质量。RandomHorizontalFlip在 image 和 mask 都要加、参数一致,但注意RandomVerticalFlip对遥感图要谨慎——虽然遥感图没有「上下颠倒」的概念,但因为光照方向问题,垂直翻转可能引入虚假的阴影方向变化。ColorJitter是亮点但容易被忽略:遥感影像的光照、大气条件差异大,适当的色彩扰动能显著提升泛化能力。
Mask 变换里我特意没有写Normalize——因为 mask 是类别标签不是图像,归一化后整数变成浮点数,标签语义就被破坏了。这是一个新手高频翻车点,看到 loss 在 1 附近波动不下降时,先检查 mask 里有没有非整数像素值。
3.4 损失函数与训练参数:从交叉熵到 mIoU 的权衡
对山川湖泊这类二三类分割任务,CrossEntropyLoss仍然是第一选择。这个数据集如果像素标签是 0/1/2 三分类,直接用torch.nn.CrossEntropyLoss()不需要做任何特殊处理。唯一要注意的是类别不均衡——如果水体的像素占比只有 10%,模型可能学到「全预测为背景」的局部最优解。
遇到这种情况,一个低成本方案是给损失函数加类别权重。先统计全数据集的像素占比,然后反比设置权重:
from collections import Counter # 先统计train masks中的类别频率(可以用前1000张近似) counter = Counter() for mp in mask_paths[:1000]: mask = np.array(Image.open(mp).convert("L")) counter.update(np.unique(mask).tolist()) total = sum(counter.values()) weights = [total / counter.get(i, 1) for i in range(num_classes)] weights = torch.tensor(weights, dtype=torch.float32) criterion = torch.nn.CrossEntropyLoss(weight=weights)这段代码的思路是:如果类别 0(背景)有 100 万像素,类别 1(山)只有 20 万,那么类别 1 的权重就是 5,相当于把山的像素「放大」5 倍参与损失计算。注意counter.get(i, 1)的兜底逻辑——如果某个类别在抽样的 1000 张里没出现,给权重 1 而不是 0,避免除以零和「完全不学习该类别」两个极端。
训练参数上,我建议统一用 AdamW 而不是 SGD,learning rate 从 3e-4 起步配合余弦退火;batch size 按显存能扛的最大值来,通常 8~16;epoch 数看训练曲线,一般 40~60 轮左右能看到 mIoU 上涨进入平台期。这个数据集 170MB、每张图片分辨率未知,如果原图很大,先缩放到 512 再训练,否则一个 epoch 的时长会非常感人。
4. 可视化验证脚本:把自己从黑匣子里解放出来
4.1 脚本能做什么:原图、GT、叠加图三视图对比
摘要里提到这个数据集附带一个可视化脚本,能随机提取一张图片,把「原始图片、GT 图像、GT 叠加在原图上的蒙板图」三张图展示出来并保存在当前目录。这看起来是个小工具,实际在分割任务里它是验证数据有没有读错的唯一快捷方式。
很多人在训练跑到一半的时候开始怀疑人生——loss 在降但看不出模型学到了什么。这时候最该做的不是改模型,而是先确认喂进去的 GT 长什么样。我自己拿到任何分割数据集的第一动作永远是:随机可视化 3 张图,肉眼确认 mask 和图片对得上、类别颜色不混乱、物体轮廓不错位。这个脚本就是干这个的。
如果脚本丢失或想自己写一个更顺手的,核心逻辑不超过 20 行:
import matplotlib.pyplot as plt import numpy as np from PIL import Image import random, os use = "train" img_dir = f"data/{use}/images" mask_dir = f"data/{use}/masks" save_dir = "vis_out" os.makedirs(save_dir, exist_ok=True) # 随机挑一张 idx = random.choice(os.listdir(img_dir)) img = np.array(Image.open(os.path.join(img_dir, idx)).convert("RGB")) mask = np.array(Image.open(os.path.join(mask_dir, idx.replace(".jpeg", ".png"))).convert("L")) # 生成叠图:把mask边缘叠加到原图上,透明度0.4 overlay = img.copy() overlay[mask > 0] = (overlay[mask > 0] * 0.6 + [255, 0, 0] * 0.4).astype(np.uint8) fig, axes = plt.subplots(1, 3, figsize=(15, 5)) axes[0].imshow(img); axes[0].set_title("Original") axes[1].imshow(mask, cmap="gray"); 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, f"vis_{idx}.png"), bbox_inches="tight", dpi=150) print(f"已保存至 {save_dir}/vis_{idx}.png")这段代码有两个细节值得说明。第一,idx.replace(".jpeg", ".png")是 images 和 masks 命名规则一致时的保险写法——文件名相同、扩展名不同已经是这类数据集的事实标准。如果原作者用的命名不完全一致,这一步就会报错,也能提前暴露数据对应关系的问题。第二,叠加图用overlay[mask > 0]只对非背景像素上色,如果是三分类且想看不同类别的区分,建议改成给每个类别映射不同颜色再做混合。
4.2 可视化结果怎么解读:从图里看出数据质量问题
跑完可视化脚本,别只瞟一眼就关掉。我总结过四类能从可视化里直接看出来的数据问题:
类别边界锯齿严重,说明 mask 分辨率比原图低,或者标注时用的画笔不够精细。对于山川湖泊这种边缘比较自然的场景,如果 GT 边界跟刀切一样整齐,反而可能是标注工具产生的伪影,训练时模型会学到这种「锯齿感」。原图和 GT 有明显的位置偏移(比如山的轮廓在 GT 里向右偏了十几个像素),说明标注坐标和图像坐标没有对齐,这种情况直接训练的后果是模型怎么都收敛不到高精度。mask 里有大面积的椒盐噪声、孤立像素点,多为标注残留,可以通过后处理时做一次中值滤波或形态学开闭运算清理掉。多个类别的像素值在可视化里颜色很接近,人眼区分困难——这说明是调色板映射的问题,不是数据问题,模型不受影响。
上面这些检查,正常流程应该在做任何训练之前完成一遍。这一步能拦下 80% 的「训练半天 loss 降不下去」问题。数据没有问题之后,再去调模型结构和超参数,才有意义。
5. 遥感分割数据集避坑清单:五个最容易翻车的点
5.1 图像不对称:mask 和原图分辨率不一致导致对齐失败
现象:DataLoader 加载时报 tensor 维度不匹配,或者训练时 loss 在下降但可视化预测图整体偏移。
原因:部分遥感数据集的 mask 由不同工具生成,可能与原图的尺寸不是严格一一对应,甚至存在 2 倍缩放关系。原作者手动 divide 或 resize 后,mask 和 images 的行列数不一致。
解决:在__getitem__里对 image 和 mask 统一做一次对齐检查,以 image 的尺寸为准,用Resize((H, W), INTERPOLATION_NEAREST)强制拉齐 mask。注意必须用最近邻插值,不要用双线性。另外一个取巧的办法是训练时不直接返回原始 mask 尺寸,而是把 image 和 mask 同时Resize到固定的 512×512,天然规避分辨率差异。
5.2 类别索引不是 0 开头:CrossEntropyLoss 直接崩掉
现象:loss 输出形状异常,或者训练时梯度为 NaN,backbone 提取的特征正常但 head 输出混乱。
原因:mask 里的像素值可能是 1、2、3 而不是 0、1、2。CrossEntropyLoss要求类别索引从 0 开始连续编码,如果出现标签值是 3 而num_classes=3,torch在计算损失时会把 3 当成越界索引直接报错,或者静默产生 NaN。
解决:在数据预处理阶段做一次类别映射,先把np.unique(mask)全部列出来,逐值映射到 0~N-1。常见做法是构造一个字典{old_value: new_index},用np.vectorize或列表索引的方式转换。这步必须是确定性转换,不能随机映射,否则训练集和测试集之间类别对应关系就乱了。
5.3 数据集自带划分但不能直接用:训练分布和测试分布相差过大
现象:训练集 mIoU 能到 0.7,测试集只有 0.3。
原因:图像分割数据集的划分如果按「图幅」或「地理区域」切分,同一区域的不同地块之间特征接近,跨区域后特征差异拉大。山川湖泊尤其明显——山的纹理、水的颜色在不同地区可能有完全不同的视觉表现。
解决:先用这个数据集自带的划分跑一轮基线,再按「图片名前缀哈希」或「场景关键字」自己切一版对比。如果自带测试集精度明显偏低,可以考虑把测试集并入训练集后按 9:1 重新划分。在正文里明确写出这一点,比读者自己反复试错要省时间。
5.4 normalize 参数照搬 ImageNet:遥感色彩分布错位
现象:loss 收敛缓慢,前 10 个 epoch 基本不下降,或者精度始终徘徊在低值。
原因:遥感图像的光谱统计特性和自然图像差异极大。很多遥感图是近红外假彩色合成,R、G、B 通道的均值和方差跟 ImageNet 的猫咪小狗照片完全不是一个数量级。用 ImageNet 的 mean/std 归一化后,输入数据的分布被「扭曲」到不合理的范围。
解决:训练前花几十秒抽样统计数据集自己的 mean/std(前面 2.1 节写过统计脚本),用数据自己的统计值做归一化。如果不想跑统计,给一个经验初始值:mean=[0.4, 0.44, 0.36],std=[0.22, 0.2, 0.24],这个值域区间对大部分遥感 RGB 影像适用。
5.5 可视化面具代码把 mask 当成彩色图处理:忽略通道语义
现象:可视化时图像正常,但保存的 mask 是「黑底彩色线条」,训练时模型通道数不一致导致 error。
原因:有些可视化脚本用plt.imshow(mask)直接展示,如果 mask 是单通道灰度值但 colormap 默认用了 jet 或 viridis,会显示成彩色。这本身没影响,麻烦的是脚本如果顺手把可视化结果savefig下来,再被后续脚本误读为彩色 GT,就会产生通道灾难。
解决:在可视化脚本里给imshow(mask)显式指定cmap="gray",并且在保存时用Image.fromarray(mask.astype(np.uint8)).save(...)单独导出原始 mask,不要依赖matplotlib生成的图像。一个更稳的习惯是:检查 GT 真实格式永远看np.unique(mask)的取值集合,而不是用肉眼去看颜色。
6. 训练后如何验证与迭代:从一张图到一个批次的完整闭环
6.1 单图推理的代码怎么写
模型训练完,别急着算 mIoU。先做一次「肉眼测试」——随机抽一张测试集图片,跑一次推理,把预测图和 GT 叠在一起,用肉眼判断边界质量、小目标召回情况。这一步花一分钟,但能极大加速你的迭代节奏。
import torch import numpy as np from PIL import Image from torchvision import transforms import matplotlib.pyplot as plt # 假设model是你训练好的UNet,已经load_state_dict img_path = "data/test/images/009047.jpeg" gt_path = img_path.replace("images", "masks").replace(".jpeg", ".png") img = Image.open(img_path).convert("RGB") gt = np.array(Image.open(gt_path).convert("L")) transform = transforms.Compose([ transforms.Resize((512, 512)), transforms.ToTensor(), transforms.Normalize(mean=[0.4, 0.45, 0.35], std=[0.2, 0.18, 0.22]) ]) model.eval() with torch.no_grad(): input_tensor = transform(img).unsqueeze(0).cuda() output = model(input_tensor) # (1, num_classes, 512, 512) pred = output.argmax(dim=1).squeeze(0).cpu().numpy() # (512, 512) # 若测试集原图尺寸不是512,把pred resize回原尺寸做对比 pred_img = Image.fromarray(pred.astype(np.uint8)) pred_img = pred_img.resize(gt.shape[::-1], Image.NEAREST) pred = np.array(pred_img) fig, axes = plt.subplots(1, 3, figsize=(15, 5)) axes[0].imshow(img); axes[0].set_title("Input") axes[1].imshow(gt, cmap="gray"); axes[1].set_title("GT") axes[2].imshow(pred, cmap="gray"); axes[2].set_title("Pred") for ax in axes: ax.axis("off") plt.tight_layout() plt.savefig("inference_sample.png", dpi=200)最后那个resize步骤不能省。训练时模型吃的是 512×512,预测时输入的原始图片如果被Resize过,输出的 pred 坐标就是 512 下的,直接和原尺寸 GT 对比会看到边缘错位。resize用NEAREST而不是双线性,正好保证类别标签不被插值污染。
6.2 全测试集的量化评估
单图能看明白之后,再跑全量 mIoU。这里建议不要只算一个全局 IoU,而是每个类分开算,然后看混淆矩阵。山川湖泊这种任务场景,很容易出现「山」类 IoU 高、「水」类 IoU 低的情况——因为水体边界不规则、可能被山体遮挡,标注本来就更难。如果某个类 IoU 明显低,优先检查 GT 是否有漏标,再考虑给 loss 加权重。
6.3 后续迭代的方向
这个数据集本身 170MB、16000 多张图,规模决定了它是一个「基线验证型」数据集,不太适合做大的预训练。我一般会根据它的特点往两个方向迭代。一是轻量网络验证:因为图片本身纹理相对规律,用 MobileNetV3 或 ShuffleNet 做 backbone,配上 ASPP 头,在 512 分辨率下能跑到接近 UNet 的精度但推理速度快好几倍。二是把「山脉 / 湖泊 / 背景」三分类推广到「细分水域」——比如把湖泊按面积大小、形状类别细分,这就已经进入新的标注任务了,数据集的价值在扩展阅读。
从那以后我每次拿到新的分割数据集,都强制走一遍「目录检查 → mask 像素值统计 → 抽样可视化 → 训练基线 → 单图推理验证」这套流程。顺序不乱、每步都不跳,数据问题基本在训练前就能暴露干净。希望帮到你。
本文还有配套的精品资源,点击获取