☰
遥感语义分割数据集从预处理到训练的完整实战指南
2026/10/9 3:58:03 网站建设 项目流程

简介:面向遥感影像语义分割的高分辨率数据集,包含7个类别,覆盖背景、土地、建筑、农田、植被与水体等常见地物,可用于建筑提取、土地覆盖分类等细粒度分割任务。资源包共375个文件,以186张jpg原图与187个png掩膜标签为主体,另附类别说明txt和辅助Python脚本,整体体积约47MB。数据已明确划分为训练集与验证集,训练集149张图片及对应掩膜,验证集37张图片及对应掩膜,可直接投入FCN、U-Net等主流分割网络训练;图像分辨率统一为1024×1024,保留丰富空间细节,有利于地物边界精细分割。配套类别文本和脚本可简化标签映射、数据预处理与格式转换,完整标签映射见classes.txt。数据集来自真实遥感场景,样本覆盖山地、建筑、农田等多种地物,有助于评估模型泛化能力。已有166人浏览学习,适合遥感与计算机视觉方向的研究者作为标准数据集开展算法验证与工程实践。

1. 一套7类遥感分割数据集:为什么“解压就能训”是错觉

拿到一份“图像分割数据集:遥感背景下的建筑、山地、植被等语义分割(7类)”,大多数人的第一反应是解压、看一眼文件夹、直接开训。实际上这套数据决定你模型上限的,不是那几行训练代码,而是标签文件怎么编码、类别怎么映射、影像和标签的坐标是否对齐。错过了这几点,后面每个环节都会还回来。这篇笔记会带你把这类数据集从头拆到尾:先搞清7类标签体系和文件组织,再走完标注、切片、增强的预处理流水线,最后用一个能直接跑的 PyTorch Dataset 把训练拉通,并把最常见的翻车点一次说清。适合正在做遥感影像语义分割、需要为语义分割算法准备训练集的工程师。

2. 先读懂数据内部结构:7类标签体系与文件组织

2.1 七类地物是怎么定的,以及为什么要关心类别顺序

遥感语义分割里的“7类”通常是建筑、山地、植被,再搭配道路、水体、农田、裸地之类的组合,具体类别由项目需求决定。标签文件(mask)里每个像素的数值就是类别编号。常见做法有两种流派:一种是0留作背景,1~7对应七类地物;另一种是从1到7直接用七类,0留作 ignore_index——这个细节直接决定你训练代码里ignore_index参数写几。我拿到一份新数据第一件事,是先算标签的像素分布,确认最大索引是不是真的连着7,而不是中间跳空。跳空本身不致命,但会在计算类别权重时把不存在的类也放进 Softmax,导致训练时 logits 有7个,标签里却只有3类,表现上就是 loss 下降正常、mIoU 就是上不去。

更隐蔽的是“空类问题”。比如这张遥感图里没有山地,标签最大值小于7;但验证集里又有山地。交叉熵对这种缺类的样本并不会报错,却会让模型在背景和山地之间来回摇摆。我处理的办法是训练时把所有缺类的切片过滤掉,或者确保每个 batch 里至少有一张含全主要类别,而不是只靠随机采样。还有一个常被忽略的点:类别内部的像素占比往往极不平衡。建筑屋顶、道路都是细碎小块,植被和山地经常占画面的六成以上。这种不平衡是遥感数据集的天性,后续损失函数和采样策略都会围绕它做文章。

2.2 标签文件常见的三种编码:灰度PNG、RGB伪彩色、GeoJSON矢量

最省心的标签文件是单通道8位 PNG,每个像素存类别索引。训练时PIL打开转long张量直接用。次常见的是RGB伪彩色掩膜,人看着舒服,但如果你直接把三通道当输入喂进损失函数,训练代码会静默把它当成三分类问题去算,类别数直接从7变成几百。先转索引再训练是铁律。颜色表通常在一个class_dict.csv里,记录每个类别对应的 RGB 值。

GeoJSON 矢量是 GIS 标注的产物,边界是经纬度坐标。这种格式不能直接进网络,必须先栅格化成像素掩膜。常见做法是交给rasterio.features.rasterize,用影像的 GeoTransform 把矢量面画到和影像一样大小的整型栅格上。这里有一个容易踩的坑:矢量是投影坐标(米),影像也是投影坐标,但两者 CRS 不一致时栅格化结果会整体偏移,画出来的面和你肉眼看影像里的建筑对不上。

给一个我最常用的读取逻辑。假定标签是8位灰度PNG,颜色表单独存:

import numpy as np from PIL import Image def read_label(path, color2index=None): lbl = np.array(Image.open(path)) if lbl.ndim == 3: # RGB 伪彩色掩膜:必须先用色表映射成单通道索引 mapping = np.full((256, 256, 256), 255, dtype=np.uint8) for idx, (r, g, b) in color2index.items(): mapping[r, g, b] = idx lbl = mapping[lbl[:, :, 0], lbl[:, :, 1], lbl[:, :, 2]] return np.asarray(lbl, dtype=np.uint8)

这里面np.full的默认值 255 就是ignore_index,意思是不认识的颜色直接忽略,而不是归到背景类。只把这一步做对,就能避免很多“loss 收敛得好好的,可视化一看全是噪点”的局面。文件组织上,常见的数据集会给你images/、labels/两个目录,加一个train.txt和val.txt分别列出训练、验证的文件名。如果缺val.txt,我一般会自己按 8:2 随机分一次,并固定随机种子,保证每次实验的验证集一致,否则模型对比会失真。

2.3 CRS与GeoTransform:数据集里最容易被忽视的元数据

普通图像分割数据集没有“坐标系”这个概念,遥感数据集有。影像如果是 GeoTIFF,它的头部带着 GeoTransform,记录左上角坐标和像素分辨率。标签文件如果是 PNG,则什么地理信息都没有。所以训练、切片之前,必须先确认影像和标签的坐标参考系统(CRS)是否一致。

我最常用的检查命令:

gdalinfo train_images/area_01.tif gdalinfo train_labels/area_01.tif

重点看Coordinate System is和Origin/Pixel Size两行。如果影像来自 Sentinel 或高分卫星,标签用经纬度画,而影像本身是 UTM 投影,两者在像素上天然错位——除非你在标注时就知道了分辨率换算,否则一定要先重投影。常见做法是用gdal.Warp把矢量或栅格统一到影像的投影,再用同一套 GeoTransform 栅格化。像素对齐这件事做完之后,训练代码里根本不会走地理坐标,走的是行列号;但当你需要把预测结果拼回大图、写成 GeoTIFF 再放进 GIS 做变化检测时,CRS 是唯一能把结果“放回地球”的依据。

如果你的数据集里同时给了images/和labels/,目录名一致、文件名一致,还要警惕一件事:文件名相同不等于内容对齐。有的数据集来自公开标注成果,原始影像被旋转或翻转过,标签却没有同步变换。记住,几何变换必须同时作用于影像和标签,否则错位积累到像素级。我一般的校验方法是随机抽三张图,把标签透明叠加在影像上存成 PNG,肉眼确认建筑轮廓贴合,再进训练。

3. 从原始遥感影像到训练样本:标注、切片与预处理流水线

3.1 标注工具怎么选:QGIS、Labelme与半自动预标注

数据集的另一半是标注。面对遥感影像,我见过三套主力方案。第一套是 QGIS 的画图工具,直接在 WGS84 或 UTM 投影下画多边形,导出 GeoJSON 或 Shapefile。优点是有地理参考,便于后续和影像叠加校验;缺点是每个地物都要手描,标注量大。第二套是 Labelme,适合在影像上画完导出 json,再转成掩膜。它不关心投影,适合做纯像素级分割,但转掩膜时需要先把 json 里的多边形坐标按影像分辨率换算成像素坐标,不然会出现整体缩小或偏移。第三套是半自动预标注,用一个较强的分割模型(DeepLabV3+、SegFormer 之类)先跑一遍,生成伪标签,再用标注工具人工修正边界。标注成本能降一半以上,但前提是边缘情况得人工筛选一遍,伪标签里的噪声如果混进训练集,往往表现为类别边缘“糊”在一起。

不管用哪种工具,我有几个经验。第一,把“边界咬合到像素级”写进标注规范,避免出现跨越屋顶边缘的斜线,否则模型学出来全是锯齿。第二,山区阴影里的建筑、被树冠遮挡的屋顶,要在规范里明确落到“建筑”还是“植被”,否则不同标注员会给你两个版本的答案,训练集内部就打架了。第三,每一类至少做一次交叉质检,两个人标同一块区域,比对像素级 IoU,低于 0.85 就返工。标注规范的细节,最后会百分之百反映在模型的边界质量上。

3.2 滑窗切片脚本:把大影像变成GPU吃得下的补丁

遥感影像动不动就是 1万×1万像素,直接塞进 GPU 是自杀。最稳的做法是滑窗切片,切成 512×512 或 768×768 的补丁,并且把影像和标签同步裁剪。我这里给一份可以直接改的小脚本:

import numpy as np from PIL import Image from pathlib import Path def sliding_window_crop(img_path, lbl_path, out_dir, size=512, stride=384): img = np.array(Image.open(img_path)) # (H, W, C) lbl = np.array(Image.open(lbl_path)) # (H, W) H, W = lbl.shape[:2] out_dir = Path(out_dir) out_dir.mkdir(parents=True, exist_ok=True) n = 0 for y in range(0, H - size + 1, stride): for x in range(0, W - size + 1, stride): img_patch = img[y:y+size, x:x+size] lbl_patch = lbl[y:y+size, x:x+size] # 跳过纯背景或只有一类的切片,避免喂给网络大量无效样本 if np.unique(lbl_patch).size < 2: continue # 记录 x_y 坐标,之后拼回原图时用它定位 Image.fromarray(img_patch).save(out_dir / f"img_{x}_{y}.png") Image.fromarray(lbl_patch.astype(np.uint8)).save(out_dir / f"lbl_{x}_{y}.png") n += 1 return n

这段代码里有三个值得一提的选择。size=512是速度和上下文视野的折中,太小模型看不到建筑的整体结构,太大占用显存;stride=384意味着每两个切片之间重叠 128 像素,重叠是为了避免建筑被切成两半后完全丢失——这个点在第5章还会展开。np.unique(lbl_patch).size < 2的过滤规则很关键:如果一张切片里只有一个类别,网络学不到任何边界信息,还白白拉低这一个类别的样本质量。另外右下角不足size的部分会被丢弃,如果目标区域正好落在边缘,可以给坐标循环加几个边界抖动,把余量也采进来。

3.3 数据增强与归一化:遥感影像不是普通照片

普通分割任务常用的水平翻转、随机裁剪在遥感里一样有效,但我额外加两项:垂直翻转和随机旋转90度的倍数。遥感影像没有“上下”概念,卫星拍出来的图你翻转180度依然是一张合法图,模型也因此能学到旋转不变性。增强时最重要的是保持影像和标签同步变换。用albumentations比手写省心得多,RandomCrop、HorizontalFlip这些变换同时接受 image 和 mask 两个参数,天然保证对齐。

归一化是另一个经常被忽略的环节。很多人直接把 ImageNet 的 mean/std 拿来用,在自然图像上好使,在遥感上经常翻车。不同传感器、不同季节、不同大气条件,影像的 RGB 数值分布差异非常大。我一般会先统计训练集自身的通道均值方差:

import numpy as np from PIL import Image from glob import glob files = sorted(glob("images/*.png")) means, stds = [], [] for f in files[:200]: # 抽200张足够稳住统计量 arr = np.array(Image.open(f)).astype(np.float32) / 255.0 means.append(arr.mean(axis=(0, 1))) stds.append(arr.std(axis=(0, 1))) mean = np.mean(means, axis=0) std = np.mean(stds, axis=0) print("channel_mean:", mean, "channel_std:", std)

注意两点:一是样本量不用全量,抽200张统计出来的均值和方差已经足够稳定;二是如果你的原始影像是16位 GeoTIFF,先把它缩放到0~1,别把 0~65535 的数值直接喂给网络,否则 BN 层前的梯度会被拉得七零八落。还有一种更省事但是我不推荐的方式,是对每张图做实例归一化,它会让影像失去绝对亮度信息,在阴影严重的遥感场景里反而不利。数据增强的力度是个很玄学的事,我通常先只用翻转和裁剪,把基线跑出来,再逐步加色彩抖动和随机缩放,每一次只改一个变量。

4. 训练一套遥感语义分割模型:从Dataset类到关键参数

4.1 写一个适配遥感分割数据集的Dataset类

数据组织完毕,下一步是把文件夹变成 PyTorch 能吃的 Dataset。这里最容易出问题的是“标签通道数”和“数据类型”。标签必须是单通道的long张量,很多新手在这里把掩膜当成 RGB 图,导致交叉熵在3通道上算得莫名奇妙。给一个可以直接跑的版本:

import torch from torch.utils.data import Dataset from PIL import Image import numpy as np class RemoteSegDataset(Dataset): def __init__(self, img_dir, lbl_dir, file_list, mean=None, std=None, transform=None): self.pairs = [(img_dir + "/" + f, lbl_dir + "/" + f) for f in file_list] self.transform = transform self.mean = mean if mean is not None else [0.485, 0.456, 0.406] self.std = std if std is not None else [0.229, 0.224, 0.225] def __len__(self): return len(self.pairs) def __getitem__(self, idx): img_path, lbl_path = self.pairs[idx] img = np.array(Image.open(img_path)).astype(np.float32) / 255.0 lbl = np.array(Image.open(lbl_path)) if self.transform is not None: aug = self.transform(image=img, mask=lbl) img = aug["image"] lbl = aug["mask"] img = (img - np.array(self.mean)) / np.array(self.std) img = torch.from_numpy(img).permute(2, 0, 1).float() lbl = torch.from_numpy(lbl).long() return img, lbl

逻辑说明:影像读进来先除 255 变成 0~1,再做通道归一化,最后转成(C, H, W);标签直接读成(H, W)的uint8,转long之前绝不混入channel维度。transform如果是albumentations的Compose,它会同时返回image和mask,天然完成同步增强。如果你的标签是开头说的 RGB 伪彩色,记得在进入Dataset之前先转成索引,否则这里lbl.shape直接变三通道,CrossEntropyLoss会立刻报错或者静默算错。

提示:RGB 掩膜转索引尽量在预处理阶段做一次并缓存成单通道 PNG,不要在__getitem__里每次转换,尤其当数据集超过几千张时,重复转换会明显拖慢训练。

4.2 类别不平衡:用像素占比反推加权损失

遥感分割里“植被一大片、道路一条线、房子一小块”是常态。直接拿 CrossEntropy 去训,模型很快学会“全都预测成植被”,因为植被的像素占比可能超过50%,光靠这个预测 mIoU 都能有0.4以上,但人一看就知道什么都没分出来。对抗不平衡最直接的一招是给损失函数加类别权重。权重按像素占比反比计算:

def compute_class_weights(lbl_dir, file_list, num_classes=8): counts = np.zeros(num_classes, dtype=np.int64) for f in file_list: lbl = np.array(Image.open(lbl_dir + "/" + f)) for c in range(num_classes): counts[c] += int((lbl == c).sum()) weights = 1.0 / (counts + 1e-6) weights = weights / weights.sum() * num_classes # 归一化,让均值接近1 return torch.tensor(weights, dtype=torch.float)

这里num_classes建议写成 8,把0背景也算进去,ignore_index=255单独处理。权重归一化这一步很重要:如果直接把未归一化的反比当权重,某些类权重可能比别的类大几十倍,模型会在早期震荡到发疯。比值过大时我还会做一步截断,把最大权重限在20以内。除了权重损失,Focal Loss 也是一条出路,它的 gamma 参数默认2.0通常够用,gamma 太大对简单样本压制太过,反而让前期训练缓慢。

我的实际经验是,先跑一个带权重的 CE 做基线,看 per-class IoU,确认到底是哪些类拉了后腿;如果建筑一直起不来,再考虑 Focal Loss 或者对大图切片做“含建筑优先采样”。优先采样就是在切片循环里额外判断,只保留至少包含若干建筑像素的切片。它比损失函数更立竿见影,但会导致整体样本分布失真,最后评估时一定要用随机采样验证集。

4.3 模型选型与训练参数怎么定:FCN、SegFormer与多大分辨率

模型选型取决于数据规模。如果你手里的切片只有几千张,我的建议是从 U-Net 或 FCN-8s(ResNet50 骨干)起步,网络小、收敛快、能快速验证数据质量。切片数量超过两三万张,上 SegFormer-B2 或 DeepLabV3+ 才有意义,否则大模型会把你所有的调参时间都耗在过拟合上。遥感语义分割场景下,FCN 这类早期语义分割算法依然有它的位置——推理快、易部署,适合先拿来做数据冒烟测试。各方案的关键训练参数我整理成一张表:

参数推荐值说明
输入尺寸512×512平衡上下文与显存,768 需要更大显存
batch_size8~16用 AdamW 时线性缩放学习率
初始学习率AdamW 3e-4 / SGD 0.02SGD 配 poly 策略,指数0.9
训练轮数60~100遥感任务收敛比 ImageNet 慢
损失函数CE + class weights小目标改善后再试试 Focal
评估指标per-class IoU + mIoU只看准确率会被背景主导

关于迁移学习:遥感影像和 ImageNet 图像视觉分布差别不小,但骨干网络前几层学到的边缘、纹理特征仍然能用。常见做法是加载 ImageNet 预训练权重,把最后分割头换成自己的 8 类(或 7 类加背景),先冻结骨干训练 5 个 epoch,再解冻全量微调。学习率用 poly 衰减:lr = base_lr * (1 - iter / total_iters) ** 0.9,比 cosine 更快稳定。训完以后别急着下结论,单独跑一遍验证集的 per-class IoU,看小类数字有没有真正起来。

5. 遥感分割数据集的5个高频翻车点:现象、根因与解决

这条路我基本趟过一遍,挑五个最常见、最隐蔽的写出来,每一条都按“现象 → 原因 → 解决”三步讲清楚。

5.1 影像和标签像素错位:训练一切正常,mIoU就是上不去

现象:loss 正常下降,训练集 mIoU 也能有0.7,验证集突然掉到0.3,可视化预测结果发现建筑轮廓整体偏移半个房子。 原因:影像和标签来自不同数据源,或标注时 GeoJSON 的 CRS 与影像 CRS 不一致,栅格化时没有统一参考。偏移量在切片后被每个切片“原样继承”,模型从头到尾都在学错位的配对。 解决:训练前跑一遍gdalinfo,确认 CRS 一致;不一致就用gdal.Warp重投影到影像的 CRS,再栅格化。如果标签是 PNG 掩膜,也可以拿影像做基准,手动检测偏移量后做整幅平移,但只适合近似刚体偏移,复杂地形下不推荐。

5.2 滑窗把建筑切成两半:边界预测永远糊

现象:模型预测的建筑边缘参差不齐,尤其在切片边界处出现断头路、半栋楼。 原因:滑窗切片的窗口位置没有考虑建筑边缘。一栋楼如果横跨两个切片,每个切片里它都只是“半个建筑”,模型被迫在缺失上下文的情况下学习,边界自然糊。 解决:用带重叠的滑窗,比如size=512, stride=384,让 128 像素的上下文保留周围地物;同时在训练时对切片做小幅随机偏移,让建筑边缘在每次迭代里出现在不同位置。评估阶段用全图推理或重叠拼接投票,消除边界效应。

5.3 RGB伪彩色掩膜没转索引:类别数瞬间爆炸

现象:dataloader 能跑,但 loss 从一开始就异常高,训练过程中类别输出从8变成几百。 原因:标签是三通道伪彩色,像(255, 0, 0)这样的三元组被当成3通道特征输进了交叉熵,Softmax 输出维度还是8,但目标张量变成了(3, H, W),模型相当于在学一个几百类的标签映射。 解决:读取掩膜后先判断ndim,三通道就先按色表映射成单通道索引。不认识的像素填255,交给损失函数的ignore_index=255。这层检查我建议直接写进Dataset.__getitem__,既是风格,也是保命。

5.4 植被占比太大:模型变成“植被预测机”

现象:整体准确率90%以上,但查看 per-class IoU,植被0.85、建筑0.15、道路0.2,mIoU惨不忍睹。 原因:训练集里植被、山地的像素占比远远压过建筑和道路,交叉熵被大类的梯度淹没,小类几乎没有有效学习信号。 解决:第一步,给CrossEntropyLoss加按像素占比反比的类别权重,权重上限截断在20;第二步,做“小类优先采样”,切片时如果某张图建筑像素过少就直接丢弃,保证训练集里每类最低占比;第三步,实在不行上 Focal Loss,并同步调高小类权重。按这个顺序试,九成场景能救回来。

5.5 整图读入内存直接爆掉:训练还没开始就 OOM

现象:dataloader 第一次迭代就卡死,或者一张 1024×1024 的图就把8GB显存吃穿。 原因:输入原始大影像没有切片,整张 1万×1万 的 GeoTIFF 变成 tensor;或者切片尺寸设得太大而 batch_size 没减。 解决:在大图进模型前先做完滑窗切片,缓存成 512×512 小图,训练时按文件名索引读取。另外把pin_memory=True配合适当num_workers,多线程读图能显著缓解 IO 瓶颈;num_workers先从 4 试,卡死就往下调,太猛了反而引起系统调度抖动。这五条是我反复踩过的最痛位置,尤其是第3条,属于看完一眼就能避开但没避开会浪费一整周的血泪经验。

6. 进阶:用逐类IoU和可视化回看验收数据集

6.1 逐类IoU怎么算,以及为什么它能定位你的数据短板

mIoU 是个平均值,会被大类拉高。逐类IoU才是体检表。给一个简单的实现:

def compute_per_class_iou(pred, gt, num_classes=8): ious = [] for c in range(num_classes): p = (pred == c) g = (gt == c) inter = (p & g).sum() union = (p | g).sum() ious.append(float(inter) / float(union + 1e-6)) return ious

脚本跑完,一眼就能看出是哪一类在拖后腿。如果某一类的 IoU 长期低于阈值而其他类正常,问题往往不在模型,在那一类的标注质量或样本量。这个指标也是论文里最常用、也最容易骗过自己的数字,所以你更应该坚持自己算一遍原始结果,而不是只看一个平均分。

6.2 训练期间把预测结果叠回原图,是唯一可靠的质控手段

我常年保持一个习惯:每训练几个 epoch,随机抽20张验证切片,把预测 mask 半透明地叠在影像上存成 PNG,排进 TensorBoard 或直接打开文件夹看。重点不是看整体效果,而是看建筑边界是否贴边、道路有没有断、植被和山地的边界是否尖锐。有一次我盯了半小时发现所有切片里水体总被误判成山地,回去查标签,才发现水体那一条的标签文件后缀名写错了,压根没参与训练。

这个习惯救过我很多次。现在只要数据集里有任何怀疑,我就先把影像和标签两张图透明叠加输出到一张图上:第一眼就能看出错位、类别颠倒、标注污染。这套流程在你从公开数据切到自制数据时尤其有用;别人给你的 7 类数据可以直接用,自己的数据则必须这样验一遍。数据质量永远是一等公民,模型只是把它放大出来给你看。希望这个习惯能帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询