简介:面向遥感影像语义分割任务的开源数据集,适用于建筑提取、山地植被分类、农田与水体识别等场景。图像统一为1024×1024高分辨率,支持细粒度分割模型的训练与验证,既适合计算机视觉初学者练习分割流程,也可用于科研和工程评估。数据集按训练集和验证集组织:训练集含149张原始jpg图像及149张对应png掩膜,验证集含37张图像及37张掩膜,样本经过整理可直接开展监督学习。标签覆盖背景、土地、建筑、农田、植被、水体等7个类别,并附类别txt说明文件,便于确认类别索引和后续自定义改动。包内共375个文件,除jpg原图与png掩膜外,还提供Python脚本用于数据处理或格式转换,整体压缩包约47MB,轻量易下载。已有166人学习,配套博客与YOLOv5分割实战参考可帮助快速上手。适合遥感、计算机视觉领域的学生、研究人员及开发者作为标准数据资源使用。
1. 遥感语义分割的7类数据集:它到底解决了什么、凭什么值得做
做遥感图像分割的人,十有八九都卡在同一个地方:算法模型不缺,缺的是能直接喂进模型的带标签数据。目标检测还能用框把建筑圈出来,可一旦要算建筑占地面积、评估山地植被覆盖率、做生态遥感指数分析,框就完全不够用了,必须像素级地判断每一块地是什么。这个图像分割数据集把遥感影像里的地物归成建筑、山地、植被等7个类,标签文件跟着影像一起,把原始影像和像素级mask的对应关系全部打通,拿过来就能训练语义分割模型。它适合三类人:做遥感地物识别的算法工程师、做土地利用分类的GIS从业者,以及想从目标检测切到语义分割但找不到干净数据集的入门者。不需要自己从零标注几千张影像,这是做这个方向最值钱的部分。
2. 标签文件为什么长这样:从原始影像到像素级mask的完整链路
2.1 单通道PNG里藏了什么:调色板映射与类索引的关系
遥感分割数据集的标签文件绝大多数是PNG格式,但这里的PNG和普通照片的PNG完全不是一回事。普通PNG是RGB三通道,而标签PNG是单通道索引图像,每个像素存的是一个整数,这个整数就是类别索引。显示的时候通过调色板(palette)把索引映射成颜色,所以你在看图软件里看到的是彩色mask,但实际数据里每个像素只有一个值。
这带来一个最常见的误解:很多人以为标签图是RGB三通道,读进来变成一个[H, W, 3]的数组再去做one-hot编码,结果训练时loss怎么都不收敛。正确做法是读成[H, W]的单通道,值域在0到6之间(7类,背景可能是0),然后在训练代码里做one-hot转成[7, H, W]的概率图。用Python验证一张标签图是不是单通道,代码长这样:
import numpy as np from PIL import Image mask_path = "labels/001.png" mask = np.array(Image.open(mask_path)) print("shape:", mask.shape) print("dtype:", mask.dtype) print("unique values:", np.unique(mask))如果shape是(H, W),说明索引模式正确;如果shape是(H, W, 3)且三个通道的值都一样,说明标签被读成了RGB模式但内容仍然有效,需要转回索引模式。如果shape是(H, W, 3)且三个通道值不一样,基本可以判定标注工具在导出时直接把RGB写进了每个像素,这类标签需要做颜色到索引的映射才能用,而不是直接拿来训练。
2.2 从矢量边界到栅格mask:把shp转成和影像对齐的标签文件
遥感领域的原始标注通常是矢量格式,常见的有shapefile里的面要素,每一块地物是一个多边形。模型训练要的是栅格mask,所以关键的中间步骤不是画图,而是矢栅转换。常见做法是先用GDAL把遥感影像读进来拿到地理变换参数,然后创建一个和影像像素网格完全对齐的空白栅格,再把矢量多边形通过GDAL的RasterizeLayer烧录进去。
from osgeo import gdal, ogr raster_path = "images/tile_001.tif" shp_path = "labels/tile_001.shp" out_mask = "labels/tile_001.png" # 读取影像尺寸与地理变换,保证mask和影像像素严格对齐 ds = gdal.Open(raster_path) geo_transform = ds.GetGeoTransform() width = ds.RasterXSize height = ds.RasterYSize # 创建单通道内存栅格,值为0代表背景 target_ds = gdal.GetDriverByName("MEM").Create("", width, height, 1, gdal.GDT_Byte) target_ds.SetGeoTransform(geo_transform) target_ds.SetProjection(ds.GetProjection()) # 设置类别字段与烧录值 shp_ds = ogr.Open(shp_path) layer = shp_ds.GetLayer() field = "class_id" # 矢量属性表中的类别字段 options = ["ATTRIBUTE=" + field] gdal.RasterizeLayer(target_ds, [1], layer, options=options)这段代码的核心是最后两步:先保证栅格和影像的尺寸、坐标一致,再用矢量属性表里的class_id字段作为烧录值。很多人忽略SetProjection这一步,结果mask和影像在GIS软件里叠不上,训练时虽然能跑,但可视化时对不上位置。烧录完成后如果发现mask和影像有偏移,优先检查这一步的坐标系是否一致。
2.3 类别平衡:遥感分割最容易忽视的第一道坎
7类地物在遥感影像里的面积天然不平衡。建筑和植被通常占比很大,山地可能面积也大,但道路、水体这种窄条和细碎地物占的比例很小。如果直接拿原始分布训练,模型会倾向于把所有不确定像素都预测成大类别,小类别的IoU可能只有个位数。拿到数据集后第一件事不是急着跑模型,而是统计类别分布。
import numpy as np from PIL import Image from collections import Counter total_counts = Counter() for i in range(1, len(valid_images) + 1): mask = np.array(Image.open(f"labels/{i:03d}.png")) total_counts.update(mask.flatten()) total_pixels = sum(total_counts.values()) for cls_id in range(7): count = total_counts.get(cls_id, 0) print(f"class {cls_id}: {count / total_pixels:.4f}")统计结果出来后,如果最大类别占比超过60%,就得在loss里加类别权重,或者用加权采样让每张图的类别分布更均衡。我一般直接用CrossEntropyLoss的weight参数,权重取总像素数除以该类别像素数的对数,这样既放大小类别梯度,又不会让权重差异过大导致训练震荡。别跳过这一步,很多人在山地和植被上翻车,根子就在类别分布没看。
3. 基于SegFormer训练这个7类数据集:从目录组织到命令跑通
3.1 数据目录怎么组织:参考VOC风格的三个子目录
语义分割数据集的目录结构看起来是小事,但直接影响后续所有脚本。我最常用的是VOC风格:JPEGImages放原始影像,SegmentationClass放标签PNG,ImageSets/Segmentation放txt索引文件。txt里每行一个文件名不带扩展名,训练脚本按这个列表读图,而不是直接扫描整个文件夹,好处是划分训练集和验证集只需要改txt,不用移动任何文件。
dataset/ ├── JPEGImages/ │ ├── tile_001.jpg │ ├── tile_002.jpg │ └── ... ├── SegmentationClass/ │ ├── tile_001.png │ ├── tile_002.png │ └── ... ├── ImageSets/ │ └── Segmentation/ │ ├── train.txt │ ├── val.txt │ └── trainval.txt └── class_names.txtclass_names.txt按顺序写7个类别名,每个类一行,顺序必须和标签像素值严格一致。第0行对应背景,第1行对应建筑,依此类推。这是最容易埋坑的地方:如果class_names.txt里的顺序和mask像素值对不上,训练脚本不会报错,但mIoU曲线可能莫名其妙地停在某个值上不去,可视化时所有预测结果都偏移一个类。
3.2 最小训练配置:SegFormer-B0在这个数据集上的启动参数
训练遥感分割数据集,模型选择上SegFormer是既省心又稳定的选项,不需要像传统FCN那样精心设计编码器,MiT-B0足够处理这种7类任务,而且感受野够大,对建筑这种不规则轮廓抓得比纯卷积结构好。用MMSegmentation跑,配置文件里的核心参数有四个:crop size、batch size、学习率和类别权重。
# segformer_b0_remote_7class.py 配置文件核心段 dataset_type = 'CustomDataset' data_root = 'data/remote_7class/' img_norm_cfg = dict( mean=[123.675, 116.28, 103.53], std=[58.395, 57.12, 57.375], to_rgb=True) train_pipeline = [ dict(type='LoadImageFromFile'), dict(type='LoadAnnotations'), dict(type='RandomCrop', crop_size=(512, 512)), dict(type='RandomFlip', prob=0.5), dict(type='Normalize', **img_norm_cfg), dict(type='DefaultFormatBundle'), dict(type='Collect', keys=['img', 'gt_semantic_seg']), ] model = dict( type='EncoderDecoder', backbone=dict( type='MixVisionTransformer', in_channels=3, embed_dims=[32, 64, 160, 256], num_layers=[2, 2, 2, 2]), decode_head=dict( type='SegformerHead', in_channels=[32, 64, 160, 256], num_classes=7, loss_decode=dict( type='CrossEntropyLoss', use_sigmoid=False, class_weight=[1.0, 1.2, 1.1, 2.0, 1.8, 1.5, 1.3])))crop size取512是经过考量的:遥感影像原始尺寸通常几千乘几千,直接整图输入显存放不下,随机裁剪成512乘512既能覆盖大部分地物尺度,又不会让建筑这种中等目标被切碎。batch size在单卡上取4到8,显存不够时优先减crop size而不是减batch size,因为小图多batch比大图少batch更容易稳定训练。class_weight按上一步类别统计的倒数再归一化,四个关键类的权重区分度要大,否则等于没加。
3.3 断点续训和评估:两条不能省的命令
训练遥感数据集不像跑分类那样短平快,一次性跑到收敛的可能性很低,中断是常态。训练脚本必须支持断点续训,否则每次从头来就是灾难。用MMSegmentation训练时,启动命令和续训命令如下:
# 首次启动训练,8卡分布式 bash tools/dist_train.sh configs/segformer/segformer_b0_remote_7class.py 8 # 中断后续训,加--resume参数 bash tools/dist_train.sh configs/segformer/segformer_b0_remote_7class.py 8 --resume work_dirs/segformer_b0_remote_7class/latest.pth评估时注意:评测脚本默认输出的是mIoU、mAcc、aAcc这三个汇总指标,但7类遥感任务里,汇总指标掩盖了很多细节,必须把每个类各自的IoU单独打出来,否则你根本不知道到底哪一类在拖后腿。用单卡评估的命令:
python tools/test.py configs/segformer/segformer_b0_remote_7class.py \ work_dirs/segformer_b0_remote_7class/best_mIoU_iter_80000.pth \ --eval mIoUeval参数会针对每个类分别输出IoU,对照class_names.txt里的顺序逐行看。建筑、山地、植被这三类的IoU应该在75以上,道路和水体这种窄条类目能过55就算合格。如果大类都上去了,小类卡在30以下,优先怀疑类别权重不够,其次是crop size把这些细长目标切碎了。
4. 评估指标怎么解读:mIoU的涨跌背后是哪些真实遥感问题
4.1 混淆矩阵比mIoU数字更诚实:建筑和裸地为什么总是互相污染
训练收敛后,先别急着看mIoU总分,拉一张混淆矩阵出来,看哪两类在互相污染。遥感分割里最常见的错误配对是建筑和裸地:建筑屋顶在卫星影像上和裸土颜色接近,模型经常把建筑边缘预测成裸地,或者反过来。第二个高频污染对是植被和山地,山地阴影里的植被和低矮灌木在光谱上确实接近,人眼都容易分错。
验证集上mIoU高并不代表生产环境可靠。遥感影像的分布和自然图像不一样,不同季节、不同光照、不同传感器的影像光谱差异巨大。这个数据集里的影像如果来自固定区域固定季节,模型换到另一块区域的影像上mIoU可能直接掉20个点。所以评估时除了数据集的验证集,我建议额外留出完全不同的区域影像做盲测,不看训练区域的任何像素。
4.2 每类IoU的计算与多类别mIoU的关系:看懂评估输出
每类IoU的计算公式是交集除以并集,分子是预测为该类且真实也为该类的像素数,分母是预测为该类或真实为该类的总像素数。对遥感分割来说,这个公式对小类目标极其苛刻:一条道路如果只有4个像素宽,预测偏移1个像素,IoU就掉到60以下。mIoU则是所有类别IoU的算术平均,它的隐患在于大类和小类权重相等,某个小类从30提到70,对mIoU的拉动很大,但视觉上可能看不出明显改善,因为小类在画面里的面积占比本来就很低。
看评估结果有个实用技巧:先看7类的IoU分布,再算一下中位数和均值的差。如果均值比中位数高5个点以上,说明有大类表现很好拉高了整体,小类其实还烂着。这种时候优化方向应该是小类的损失权重,而不是继续堆模型容量。反向情况,均值比中位数低,说明有某类严重拖后腿,要单独分析那一类的错误模式。
4.3 可视化预测mask的检查流程:不只看颜色,要看边界
评估指标全部合格,不代表结果能用。遥感图像分割和手机拍照分割最大的区别是几何精度要求:建筑边界偏几个像素,算占地面积可能就差了几十平方米。所以可视化检查预测结果时,不要只看整体颜色分布对不对,要看边界处的贴合度。
from PIL import Image import numpy as np img = np.array(Image.open("images/tile_001.jpg")) pred = np.array(Image.open("preds/tile_001.png")) label = np.array(Image.open("labels/tile_001.png")) # 把预测和标签不一致的像素标记为白色 diff = (pred != label).astype(np.uint8) * 255 diff_img = Image.fromarray(diff).convert("RGB") overlay = Image.blend(Image.fromarray(img).convert("RGB"), diff_img, alpha=0.5) overlay.save("check_diff.png")输出图里白色区域就是预测和标签不一致的像素,重点看这些不一致是聚集在类别边界附近,还是大面积出现在类别内部。如果主要集中在边界,属于正常误差;如果类别内部出现整片不一致,说明模型在该类上存在系统性偏差,大概率是训练数据里该类样本的影像特征不够多样。白色像素占比超过15%,这个模型基本还不能投入实际使用。
5. 避坑指南:遥感分割数据集上最常见的五个翻车现场
5.1 标签图看起来是灰的:调色板信息丢失还是索引错乱
现象:用看图软件打开label PNG,显示出来是纯黑或者一片灰,完全看不出类别颜色。原因:某些标注工具导出的PNG只写了索引值,没写调色板,看图软件不认识索引就直接显示成灰度。解决:这不代表数据坏了,训练代码里直接按单通道读取即可,不需要恢复调色板;如果需要可视化,用代码把索引映射成自定义颜色再保存。
5.2 山地的IoU怎么调都上不去:类别不平衡的锅还是标注噪声
现象:训练到8万迭代,建筑、植被IoU已经过80,山地IoU始终卡在50左右。原因:山地在数据集里面积占比过大或过小都会出问题,占比过大时模型倾向于把阴影、低矮植被全判成山地,占比过小时模型根本没机会学到山地的纹理特征。另一个常被忽略的原因是山地的标注边界本身就很模糊,标注员之间的一致性差,这部分噪声是模型上限的天花板。解决:先统计山地类别的像素占比,如果在10%-30%区间,检查训练集的影像里山地的光照角度是否多样性不足,建议把山地单独抽出来看预测错误的可视化结果,判断是漏检还是误检。
5.3 大影像切块之后主体类别偏移:crop size和overlap怎么权衡
现象:遥感影像原始尺寸是4000乘4000,随机裁剪512乘512训练,模型在验证集上mIoU不低,但整图预测时建筑边缘出现大量锯齿且类别偏移。原因:切块后模型缺少全局上下文,不知道当前切块在整张图里的位置,遇到大面积植被时,局部特征和山地很像。解决:推理阶段用滑动窗口预测,窗口之间留overlap,重叠区域取多次预测的投票结果。我一般用窗口512、步长256,overlap是50%,整图预测质量立刻上一个台阶。训练阶段不用刻意加overlap,推理时加就够。
5.4 验证集mIoU高、换块区域就崩:训练和测试影像的分辨率不一致
现象:数据集里影像分辨率是1米每像素,模型验证集mIoU到85,换到另一块区域分辨率变成2米的影像,mIoU掉到60。原因:语义分割模型对分辨率极其敏感,不同GSD(地面采样距离)下同一类地物的纹理特征完全不同,模型在1米数据上学到的纹理模式在2米数据上根本不存在。解决:训练时做多尺度增强,把影像随机缩放到0.5倍、0.75倍、1.25倍再裁剪;如果生产数据的分辨率已知,应该把它们作为额外训练数据或至少做domain adaptation。不要指望模型自己在分辨率变化时保持鲁棒,这是血泪经验。
5.5 训练时数据读取慢到GPU空转:高分辨率PNG解码太慢
现象:数据加载器成为瓶颈,GPU利用率只有40%,训练一步要等很久。原因:遥感PNG动辄几千乘几千像素,每张解码耗时几十毫秒,普通DataLoader的prefetch不够用。解决:先把所有影像离线裁成512乘512的小块并保存成JPEG或PNG,训练时直接读小块;或者用NVJPEG这类硬件解码库。C++后端跑遥感数据已经不是可选而是必选,纯Python读大图再在线裁剪,速度根本跟不上。
6. 快速验证数据集与标签文件的两个技巧:加载检查和首轮训练试金石
拿到一套新的图像分割数据集,别急着配SegFormer、调学习率,先做两个快速检查,能帮你省掉至少一个星期的无效调试。
第一个是加载检查。写一个几十行的脚本,按train.txt的索引顺序,读一张影像、读一张标签,检查三件事:影像和标签的文件名一一对应且一一对得上;标签的unique值集合恰好是0到6;随机抽5张图目视检查标签轮廓和影像里的地物边界是否大致贴合。这一步几乎能发现所有由于矢栅转换、坐标偏移、类别映射错乱导致的问题,而且成本极低。
# 快速验证脚本:核心检查逻辑 for name in train_list: img = np.array(Image.open(f"JPEGImages/{name}.jpg")) mask = np.array(Image.open(f"SegmentationClass/{name}.png")) assert set(np.unique(mask)) <= set(range(7)), "类别超出范围" assert img.shape[:2] == mask.shape, "影像与标签尺寸不一致"第二个是首轮训练试金石。用极小配置跑20个迭代:batch_size取2、crop_size取256、学习率取正常值的1/10,只看loss能不能持续下降。这一步不追求精度,只验证数据管线是通的。loss完全不动,检查标签值是不是全为0;loss下降但速度极慢,检查数据增强里有没有把标签也做了归一化;loss到0.5左右开始震荡,大概率是类别权重设置得过于极端。试金石跑通后再回到完整配置正常训练,基本不会再出现半夜醒来发现训练中断的惨剧。
数据集的7类划分本身也值得花半天确认:这7类是否符合你的业务需求。只有建筑、山地、植被的话,水体在哪、道路在哪,模型预测出来的类别输出少了几类,后处理阶段照样得补标注。如果业务场景正好是这7类覆盖的范围,这个数据集能让你绕过最痛苦的标注环节直接到模型迭代;如果覆盖不全,把它当作预训练来源再用这个模型做半自动标注,给新类别打底稿,也是一条很顺的路线。这是我自己踩过的教训:当初草率地把一套数据集的类别强行映射到业务需求上,结果在道路这一类上白白调了一个多月,不如一开始就摸清类别边界再决定。希望帮到你。
本文还有配套的精品资源,点击获取