
简介面向遥感图像语义分割任务的高质量数据集覆盖建筑、土地、植被、农田、水体等多类地物标注为7类语义标签适合用于深度学习分割模型训练、细粒度地物提取以及与YOLOv5等实例分割框架的实战调试。包内含375个文件主体为186张JPEG原图与187张PNG标签掩膜mask并附1个类别说明文本和1个Python辅助脚本压缩包整体约47.03MB数据组织清晰便于直接划分训练与验证。数据集已分好训练集149对图像与掩膜、验证集37对每张图像分辨率达1024×1024为建筑提取、遥感地物分类等场景提供了可直接落地的样本基础。目前已有166人下载学习适合计算机视觉方向的研究者、遥感算法工程师及高校学生快速开展语义分割实验与模型评测。1. 遥感图像分割数据集到底解决了什么问题给建筑、山地、植被等7类地物做一张像素级地图做高精度遥感落地的人迟早会卡在同一个地方模型选好了训练代码写好了却没有一套能让自己信服的标签数据。遥感影像语义分割数据集核心就是给一景影像里的每一个像素打上类别标签比如建筑、道路、水体、植被、山地、农田外加一个背景类凑成标题里说的7类。它的直接价值不是让你“跑通demo”而是让语义分割算法在真实地理场景下可验证、可对比、可迭代。适合谁做土地覆盖制图、城市规划、生态监测、灾损评估的工程师和研究生——你需要的是一个带像素级标签文件的遥感数据集而不是一堆散装的影像文件。2. 7类语义分割数据集怎么定义类别方案、标签格式与目录结构拿到遥感影像先别急着标注。第一件事不是打开软件画多边形而是把“7类”这回事钉死。类别定义含糊后面所有标签都会跟着含糊模型训练出来也是糊涂账。2.1 先把7类定死类别层级与不可见类别的处理我一般会先建一张类别表把每个类别的包含对象、边界情况、容易混淆的对象写清楚。别嫌这一步啰嗦后面每一块标签的准确性都依赖它。一个比较稳妥的7类方案是这样的Class ID类别名包含对象容易混淆对象0背景/其他未标注区域、不确定地物阴影中的未知地物1建筑房屋、厂房、温室大棚大型桥梁、白色车辆2道路高速公路、城市道路、乡村硬化路裸地、停机坪3水体河流、湖泊、水库、鱼塘阴影、深色湿地4植被乔木、灌木、草地、茂盛农田山地阴坡的林区5山地/裸地裸岩、裸露土壤、采石场干燥农田、建筑工地6农田各类农作物地块草地、休耕裸地注意两个容易翻车的地方。第一背景/其他类必须有而且占的比例最好控制在10%到20%之间不要为了凑数把所有边缘缝隙都归进去不然模型会偷懒把所有不确定像素都丢给背景类。第二如果影像里有大片云、云影我建议在标注规范里明确“云和云影属于背景/其他”而不是强行归到山地或水体——这是遥感分割最容易犯的错误后面避坑章节我会专门展开。2.2 标签文件的三种落地格式RGB索引色、单通道灰度与RLE编码确定了类别接下来要定标签文件的存储格式。遥感标签文件常见的有三种单通道灰度PNG像素值直接等于类别ID例如像素值3就代表水体。训练时最方便不需要任何转换直接当target喂给模型。RGB索引色PNG每个类别对应一个固定RGB颜色人眼看着直观分享给别人时友好但训练前必须做一次颜色到类别ID的映射。RLERun-Length Encoding掩码常见于COCO风格的实例分割数据集语义分割用得相对少适合对象边界复杂的场景。我自己的项目习惯是对外交付用RGB索引色PNG因为对方能直接打开看检查标签更直观自己训练时统一转成单通道灰度PNG因为省去每次forward都做映射的开销。标题里的“标签文件”如果是你从网上下到的大概率是RGB索引色PNG拿到手第一件事就是确认它是不是真的0到6的连续整数。2.3 干净的数据集目录train/valid/images/labels怎么组织目录结构不影响模型精度但影响你三个月后能不能找回自己放哪了。我一般按这个结构组织dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ ├── meta/ │ ├── classes.json │ └── palette.json └── tiles/ ├── scene_01/ ├── scene_02/ └── scene_03/images和labels下每个子目录的文件一一同名比如images/train/scene01_001.png对应labels/train/scene01_001.png。meta目录里放类别名和调色板的映射文件python代码和标注人员都从这里读而不是把类别信息硬编码在脚本里。tiles目录放切好的原始瓦片等划分完成后可以删但训练阶段保留着方便追溯。类别定义好后我建议第一时间跑一个统计脚本看看每个类别的像素占比。这一步直接决定后面要不要做类别权重。from pathlib import Path import numpy as np from PIL import Image label_dir Path(dataset/labels/train) class_names [background, building, road, water, vegetation, mountain, farmland] counts np.zeros(len(class_names), dtypenp.int64) for p in sorted(label_dir.glob(*.png)): arr np.array(Image.open(p).convert(L)) # 标签像素值必须在 0~6 之间越界直接报错 if arr.min() 0 or arr.max() 6: raise ValueError(f{p.name} 标签越界当前最大值为 {arr.max()}) for c in range(len(class_names)): counts[c] int((arr c).sum()) total counts.sum() for c, name in enumerate(class_names): ratio counts[c] / total print(f{name:10s} {counts[c]:12d} {ratio:7.4f})这段代码做的事情很简单遍历所有训练标签逐类统计像素总数然后输出每个类别的占比。越界检查放在循环里不要等到训练时突然报“class index out of range”再回来查那时候定位成本会翻好几倍。参数说明convert(L)是必要的因为PNG标签可能是RGB三通道直接np.array()会得到一个(H, W, 3)的数组和后面的arr c逻辑完全对不上。类别名数组和类别ID必须和标注时的规范严格一致顺序错一位统计结果就是灾难。跑完这个脚本如果某些类别占比低于1%就要警惕了——不是不能训练而是val集和test集里这些类别的样本本来就少算出来的IoU会有很大的方差波动十个点是很正常的事。3. 从原始影像到标签文件标注流程与RGB转单通道预处理类别表和目录都定了接下来是最耗时也最需要细心的一步把原始遥感影像变成“影像标签文件”的成对数据。这一步做得扎实后面的训练和评估才有底气。3.1 标注工具选型与底图准备QGIS和LabelMe各自边界在哪标注遥感分割数据集最常见的方案是QGIS配合栅格化也有一些人在用LabelMe。两种我都用过给一点真实感受。QGIS适合大图幅、多边形为主的标注。你先加载遥感影像新建矢量图层每个类别建一个图层然后用“创建多边形”功能沿着地物边界画。画完以后用“栅格化”工具把矢量转成栅格标签。优势是能在原始分辨率上精确贴合边界劣势是学习成本高而且大量重复画多边形非常费手。LabelMe更适合中小图、轮廓复杂的目标。你把影像切块后逐个标注导出JSON文件再转成掩码。优势是上手快、有Web界面方便多人协作劣势是图幅大了以后效率很低多边形节点一多就卡。我一般会用QGIS。但不管用哪个工具有一个前提必须满足标注底图的合成方式要先定好。自然彩色影像红/绿/蓝波段人眼看着舒服但水体、裸地、建筑之间的光谱差异经常不够大。我通常会把波段重组成“近红外-红-绿”假彩色合成因为近红外对植被响应非常敏感植被在图上显示为亮色调和建筑、裸地一眼就能区分这样标注的边界质量会明显提升。另外如果影像分辨率低于2米建议先重采样到统一分辨率再标注避免不同图幅的标注精度不一致。3.2 RGB标签转单通道重映射脚本与掩膜清洗QGIS栅格化出来的标签通常是RGB格式比如你给建筑定义了(255, 0, 0)栅格化输出里建筑就是这个红色。这种标签不能直接用来训练需要转成单通道灰度像素值等于类别ID。import numpy as np def rgb_to_index(rgb_arr, palette): 把RGB标签图转成单通道索引图 rgb_arr: 形状为 (H, W, 3) 的标签数组 palette: dict格式为 {类别名: (r, g, b)} h, w rgb_arr.shape[:2] idx np.zeros((h, w), dtypenp.uint8) for class_id, (name, color) in enumerate(palette.items()): mask (rgb_arr color).all(axis-1) idx[mask] class_id return idx palette { background: (0, 0, 0), building: (255, 0, 0), road: (255, 255, 0), water: (0, 0, 255), vegetation: (0, 255, 0), mountain: (128, 128, 128), farmland: (0, 128, 0), } rgb np.array(Image.open(label_rgb.png)) label rgb_to_index(rgb, palette) Image.fromarray(label).save(label_idx.png)这段代码的核心逻辑是遍历调色板里的每个类别颜色用(rgb_arr color).all(axis-1)找出所有像素值完全等于该颜色的位置然后写上对应的类别ID。all(axis-1)表示在最后一个维度RGB三通道上全部匹配才算命中不能只比对单通道否则红色和橙色会被混淆。这里有几个容易翻车的细节。第一调色板的类别顺序必须固定不要中途改否则之前生成的标签全部作废。第二栅格化输出里常常会带一条背景黑边或白边看情况归到背景类如果不确定那片区域到底属于什么不要试图硬分类。第三转换前先用np.unique看一眼RGB标签里实际出现了哪些颜色如果出现了调色板之外的颜色说明标注时不小心用了别的颜色先修复标注而不是硬转换。3.3 多波段影像和标签分辨率对齐重投影的坑位遥感影像和标签文件分辨率不对齐是另一个高频翻车点。你拿到的原始影像可能是多波段TIF分辨率是0.8米但你在QGIS里画的矢量边界如果参考的是另一套影像或者栅格化时分辨率设错了生成的标签可能在边缘有半个像素的错位。不要小看这一个像素在建筑轮廓这种高频边界区错位会让模型在训练时不断看到“同一个纹理两套标签”最后学出来的边界就是模糊一片。我的习惯是标注前把所有原始影像切到同一个坐标系、同一个分辨率再开始画。如果实在没有统一的条件那栅格化时把分辨率设成和影像完全一致标签和影像输入网络的尺寸保持一致。另外如果标签是大图幅栅格训练前降采样切瓦片时标签的重采样方法要选“众数”或者“最近邻”绝对不能用双线性插值——双线性插值会在类别边界处产生小数比如3.0到4.0之间出现3.6这既不是水体也不是植被模型看到这种target基本就傻眼了。4. 标签合法性检查与数据集划分训练前必须过的两关标签文件生成之后直接开始训练建议先忍一忍。高质量数据集和“能跑的demo”之间差一次彻底的合法性检查和一次合理的划分。这两件事做不好后续所有指标都是带病作业。4.1 标签合法性检查越界值、未标注区、重叠区三件套第一层检查是像素值范围。import numpy as np from PIL import Image from pathlib import Path label_dir Path(dataset/labels/train) for p in sorted(label_dir.glob(*.png)): arr np.array(Image.open(p).convert(L)) uniq np.unique(arr) if len(uniq) 0: raise ValueError(f{p.name}: 标签为空) if uniq.min() 0 or uniq.max() 6: raise ValueError(f{p.name}: 标签越界像素值 {uniq}) if 255 in uniq: raise ValueError(f{p.name}: 含nodata值 255需要先清理) print(f{p.name}: OK - {uniq})这段循环检查两件事像素值是否在0到6范围内以及有没有出现255。这两个问题分别来自不同的原因像素值越界通常是调色板映射出错255则是QGIS或GDAL栅格化时输出nodata导致。第二层检查是未标注区域。如果标签里某一块大面积的像素值恒为0你要判断它到底是真正的“背景”还是“漏标注”。怎么判断用一个简单办法把影像和标签叠加显示如果某一区域影像纹理很明确——比如明显是房子——但标签里全是0那就是漏标了。相比之下边缘带或图幅交接处全是0可能是原始影像本身就是黑边问题不大。第三层检查是重叠区域。如果你把多张矢量图层一起栅格化某些多边形之间可能有重叠区域栅格化时后画的图层会覆盖先画的结果就是标签和实际地物不一致。检查方法是在QGIS里用“拓扑检查”工具查看重叠多边形或者在转成标签后对比重叠区域的像素是否与任何一方吻合。这类问题往往只影响局部训练时毫不知情但最后可视化输出时会出现“同一个位置一会儿是建筑一会儿是农田”的诡异现象。4.2 按图幅切瓦片滑动窗口采样而不是全图硬塞原始遥感影像往往是几千乘几千的大图幅显存有限不能整图输入网络。常规做法是滑动窗口切瓦片。def sliding_window_crop(image_path, label_path, out_root, window512, stride256): img np.array(Image.open(image_path)) lab np.array(Image.open(label_path).convert(L)) h, w lab.shape k 0 for y in range(0, h - window 1, stride): for x in range(0, w - window 1, stride): img_crop img[y:y window, x:x window] lab_crop lab[y:y window, x:x window] # 过滤纯色瓦片减轻类别不均衡 first lab_crop.reshape(-1)[0] if (lab_crop first).mean() 0.95: continue out_img_dir Path(out_root) / fimages out_lab_dir Path(out_root) / flabels out_img_dir.mkdir(parentsTrue, exist_okTrue) out_lab_dir.mkdir(parentsTrue, exist_okTrue) Image.fromarray(img_crop).save(out_img_dir / f{k:05d}.png) Image.fromarray(lab_crop).save(out_lab_dir / f{k:05d}.png) k 1 sliding_window_crop(scene01.tif, scene01_label.png, tiles, 512, 256)关于窗口尺寸、步长和过滤策略给一组参数参考。窗口512或256都行如果显卡是8G显存256更稳如果显存充足且场景中地物边界很大512的上下文更够用。步长一般取窗口的1/2也就是256对应128512对应256这样相邻瓦片有重叠训练样本量加大相当于一种轻度的数据增强。注意这里的纯色瓦片过滤如果一个瓦片95%以上都是同一个类别这种样本对于分割网络来说几乎不提供信息还会加剧类别不平衡建议扔掉。但不要把所有纯背景瓦片都扔掉否则验证集里没有背景样本模型在这类区域的表现无法被量化。4.3 训练/验证/测试划分时间分离而不是随机洗牌这是遥感分割数据集构建里最容易被忽视、影响也最隐蔽的一步。如果你把切好的瓦片随机打乱然后按比例划分train/val/test很可能出现的情况是属于同一幢建筑、同一条道路的两块相邻瓦片一块在train里另一块在test里。模型在训练时见过这条路测试时再遇到IoU自然很好看但换一景全新影像就全面翻车。正确做法是按原始图幅划分。比如你有10景不同地区或不同时相的影像先把这10景按区域随机分成8个train景、1个val景、1个test景再在每景内部切瓦片。如果按时间采集的影像train取较早的时相test取较晚的时相这样能模拟“用旧数据训练预测新数据”的真实场景。如果你手里只有一景影像没法按图幅划分那么至少按空间位置划分把整景影像分成几个区块不同区块进不同数据集确保train和test在空间上不重叠。这一条是遥感分割的底线。5. 遥感分割数据集避坑指南我踩过的5个坑这个部分写的都是我实际做遥感分割数据集时踩过的坑。每一条都按“现象、原因、解决”展开希望你少走弯路。5.1 坑一loss在2.1左右不降像没在学习现象训练一开始loss降到2.0附近就上不去了感觉模型根本没有学到任何东西。原因大多数自然影像分割数据集类别相对均衡但遥感场景里植被和农田经常占掉80%以上的像素建筑和道路加起来不到5%。普通交叉熵损失在这种极度不平衡的数据集上模型会学出一个“全都预测为植被”的局部最优解因为这样loss也不会太高。解决给损失函数加类别权重权重取每个类别像素占比的倒数再做一次平滑处理防止占比极低的类别权重过大导致训练震荡。另外一个常配合的手段是改用Focal Loss让模型把注意力放在难分类的少数类别上。我的建议是先用类别权重这一招改动最小、效果最稳。5.2 坑二mIoU看着挺高可视化时建筑却全被吃掉了现象训练结束mIoU超过70%保存可视化结果一看建筑区域几乎都是漏分割跟背景混在一起。原因mIoU是所有类别IoU的平均值。如果背景、植被、农田占了大头这几类的IoU很高平均值被拉上去建筑这一类的IoU只有30%也能得到看似不错的整体指标。这是指标选择不当导致的误判不是模型真的“够好了”。解决评估时不仅要看mIoU还要按类别打印IoU。标准做法是输出一个7x7的混淆矩阵逐类看精度和召回率。如果建筑IoU明显低于其他类重点排查建筑标签本身是否准确再看是否需要对训练样本做类别均衡采样。5.3 坑三标签里出现255或254现象训练时突然报错“target中的类别索引超出范围”打开标签图一看某块区域像素值是255。原因QGIS栅格化通常把背景输出为0但GDAL或者某些标注工具会输出255作为nodata标记如果你转换单通道时忘了处理这部分像素255就会混进训练target。解决在标签预处理脚本里加一条强制约定训练前把所有大于等于254的像素值映射到0背景前提是你确定这片区域确实是未标注区域。如果不确定回到原始标注里人工复核。这一步不要嫌麻烦越早做成本越低。5.4 坑四随机切瓦片后同一栋楼被切进train和test现象train IoU很高test IoU掉20个点而且无论怎么调模型都追不回来。原因切瓦片时没有按原始图幅区分同一个大的地面目标被切成了两三块瓦片随机分配后一部分进了train一部分进了test。模型相当于开卷考试。解决按图幅或按空间区块划分数据集确保test里的任何建筑物、道路段都没有在train里出现过。这是遥感分割数据集构建最基础的要求也是最容易被跳过的。5.5 坑五水体识别指标虚高换个地区就翻车现象在本地测试集上水体的IoU超过90%但拿到另一个季节或另一个区域的新影像测试水体分割效果一塌糊涂。原因数据集里的水体样本和云影样本在光谱特征上太像了模型学到的是“深色就是水”。特别是检测水体时厚云阴影下和山体阴影下的深色区域几乎都被错分为水体。解决从数据源头处理。标注时不要指望模型自己学会区分水体和阴影要么把云影、山体阴影单独定义为一个类别训练完再做类别合并要么在标注规范里对含阴影的区域做明确标注并尽量多收集不同光照条件下的影像。另一个技术手段是引入近红外波段作为额外输入通道水体和阴影在近红外波段上有明显差异但前提是你的标签和训练管线都支持多通道输入。6. 用基准训练验证数据集从U-Net到SegFormer的最小评估闭环数据集构建完成别急着上大模型。先用一个标准的U-Net跑通训练闭环确认标签本身没有系统性错误再考虑SegFormer这类更复杂的语义分割模型。很多人跳过这一步直接上大模型结果训练很久才发现标签有问题浪费时间不说还很难定位问题出在数据还是模型。6.1 类别权重与损失函数配置一份可直接修改的PyTorch核心配置import torch import torch.nn as nn # 类别像素占比来自第 2 章的统计脚本 class_ratios torch.tensor([0.08, 0.05, 0.06, 0.12, 0.45, 0.18, 0.06]) class_weight 1.0 / torch.sqrt(class_ratios 1e-6) class_weight class_weight / class_weight.sum() * len(class_ratios) criterion nn.CrossEntropyLoss(weightclass_weight.to(cuda)) optimizer torch.optim.AdamW(model.parameters(), lr1e-3, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max30)这里最关键的是class_weight的构造。直接用类别占比的倒数会给占比极低的类别一个非常大的权重比如0.5%的类别权重会是200训练时一步梯度更新就能把整个特征表示撞歪。所以我用1 / sqrt(ratio)压一下极端权重再用最后一行归一化保证权重之和等于类别数训练初期的loss量级和常规交叉熵一致。损失函数用带权重的交叉熵就够了不需要一上来就上Focal Loss。跑完收敛后如果少数类效果还是不行再换Focal Loss不迟。6.2 用混淆矩阵和每类IoU来定位标签问题写一个简短的评估函数训练结束后在val集上打印混淆矩阵和每类IoU。from sklearn.metrics import confusion_matrix def evaluate(model, val_loader): model.eval() gt_all, pred_all [], [] with torch.no_grad(): for x, y in val_loader: logits model(x.cuda()) pred logits.argmax(dim1).cpu().numpy().reshape(-1) gt_all.extend(y.numpy().reshape(-1)) pred_all.extend(pred) conf confusion_matrix(gt_all, pred_all, labelslist(range(7))) iou [] for c in range(7): tp conf[c, c] fn conf[c, :].sum() - tp fp conf[:, c].sum() - tp iou.append(tp / (tp fp fn 1e-6)) class_names [background, building, road, water, vegetation, mountain, farmland] for c, name in enumerate(class_names): print(f{name:10s} IoU {iou[c]:.4f}) print(fmIoU {np.mean(iou):.4f})这个评估函数的作用是用混淆矩阵找到标签错误的高发区。如果建筑的IoU明显偏低而混淆矩阵显示建筑大量被预测成道路那可能是标注时把深色屋顶画成了道路如果山地被预测成植被那大概率是假彩色合成下的光谱混淆而不是模型能力问题。我自己的习惯是训练前先对一小批瓦片做5轮过拟合测试如果loss能降到很低说明模型能吃住数据接下来再谈泛化loss下不去先怀疑数据和标签不要急着调模型结构。第一次做遥感分割数据集的时候我因为云影没有单独处理导致水体IoU虚高了一个月后来靠混淆矩阵才发现模型用的是“深色即水体”这条懒惰路径。现在每次交付数据我都会先跑一遍这套最小训练闭环再谈其他。这个习惯帮我省掉了大量“模型调不通”的假问题也推荐你试一试。希望帮到你。本文还有配套的精品资源点击获取