简介遥感城市图像语义分割数据集面向计算机视觉初学者与遥感影像分割研究者提供约1000张已标注的遥感图像覆盖海陆区域等8类地物图像与标签均已预处理可直接用于训练语义分割模型。数据已按约800张训练集、300张验证集划分每张原图对应同名mask模板附带的classes.txt清晰标注0背景、1建筑等类别含义便于快速理解标签规范。压缩包内还有Python可视化脚本可随机抽取样本将原始图片、GT图像及GT在原图上的蒙板叠加效果展示并保存方便直观检查数据质量与预测效果。资源共2000个文件以png标签/蒙板、jpg原始影像为主另有说明txt与可视化py脚本整体仅42.05MB轻量易获取。目前已有129人学习可配合U-Net、SwinUNet等分割网络改进专栏进行实践是开展遥感图像分割实验的高性价比基础数据。1. 这个遥感城市语义分割数据集拿到的不是图是训练后悔药找训练数据这件事做过语义分割的都懂手动标注成本高类别定义乱拿到手的标签不是格式不对就是跟图像对不齐。遥感城市图像更是重灾区建筑、道路、水体、植被挤在一起高分辨率下逐像素标一版人先崩溃。一个约1000张、8类别、标签已处理完、可以直接进训练流程的遥感城市图像语义分割数据集解决的不是“缺数据”而是把你从“标注-清洗-对格式”这条长链条里解脱出来。这个方向适合两类人一类是做城市遥感项目、需要快速验证分割模型效果的工程师另一类是学生和刚入门语义分割的开发者想拿现成数据跑通全流程。下面我会把拿到数据之后要做的事拆开讲先体检再转换接着跑通训练最后告诉你最容易翻车的几个细节。2. 数据体检拿到数据集先做这三件事别急着开训2.1 先摸清目录结构和标签格式文件数量、尺寸、位深一次性对齐语义分割数据集最常见的打包方式是images/和labels/两个平铺目录images放原始影像labels放逐像素的标签图。我拿到任何一个现成数据集不会直接开训先跑三条命令摸底确认它是不是“已处理完”。# 1) 统计图像和标签数量确认两边是成对的 find datasets/images -type f | wc -l find datasets/labels -type f | wc -l # 2) 抽查前5张图的尺寸和位深 python -c from PIL import Image import glob img_paths sorted(glob.glob(datasets/images/*.png))[:5] for p in img_paths: im Image.open(p) lb Image.open(p.replace(images, labels)) print(im.size, im.mode, lb.size, lb.mode) 第一条命令确认原图和标签数量一致。数量对不上说明数据本身有缺失后续 DataLoader 一跑就报错。第二条命令重点看三样尺寸、mode、标签是否也是同样的宽高。尺寸不一致的问题在遥感数据里经常出现——有些瓦片边缘被裁掉后没做对齐进到模型里就要么报错要么得做 resize而 resize 标签图会让边界类别对不准。标签图如果是P或L模式说明是单通道索引图可以直接用如果是RGB模式说明是调色板存储或彩色标注图第3章会专门处理。提示不要用看图软件直接双击标签图看颜色。索引图的像素值只有 0~7但软件会按调色板渲染成彩色肉眼看起来“像那么回事”实际像素值跟你以为的完全不是一回事。2.2 8个类别到底是谁像素值统计与类别定义核对标题说8类别但具体是哪8类不同数据集定义不一样。城市遥感图像里常见做法是把地表覆盖分成建筑、道路、植被、水体、车辆、裸地等。拿到手后第一步是读一遍类别定义我一般会把类别表整理成这样类别ID名称说明0背景未分类区域训练时通常当作 ignore_index 或普通背景1建筑含屋顶、楼房轮廓是城市分割的主类别2道路含车行道、人行道关注细连通性3植被树木、草地、绿化带4水体河流、湖泊、池塘5车辆轿车、公交车小目标6裸地施工地、裸土、沙地7其他不归入以上类的对象这一张表先立住后面所有转换和训练都以它为准。不要相信文件名里的色彩暗示直接对标签做像素值分布统计看看这8类在整份数据里的占比情况。import numpy as np import glob from PIL import Image hist np.zeros(8, dtypenp.int64) for p in glob.glob(datasets/labels/*.png): arr np.array(Image.open(p)) hist np.bincount(arr.ravel(), minlength8)[:8] print(hist) print((hist / hist.sum()).round(4))这段代码会给你一个很直观的比例分布。以我的经验城市遥感分割里道路和建筑通常占大头车辆、水体这类小类别可能只占百分之零点几。如果某个类别像素占比连0.1%都不到那基本可以肯定后面训练会把它学到消失需要在第5章讲到的类别权重里做补救。这一步走完你对这份数据的认知就不再是“约1000张、8类”而是“哪些类别好分、哪些类别是弱势群体”。这个认知直接决定后面的转换和训练策略。2.3 训练/验证集划分按瓦片来源分组别让验证集泄漏训练集语义分割的 train/val 划分不能像普通分类那样随机 split。遥感影像通常是一张大图裁成若干瓦片同一栋建筑、同一条道路会被切到相邻的几张图里。如果随机分配到两边模型在训练时已经见过验证区域里的纹理和边缘验证指标会虚高mIoU 看起来很好部署到新区域马上露馅。我建议按文件名前缀分组后再划分。大多数瓦片数据集的命名里带着行列号比如xx_12_08.png前缀相同的瓦片来自于同一块大场景应该放进同一个集合。划分脚本如下import glob import random paths sorted(glob.glob(datasets/images/*.png)) groups {} for p in paths: name p.split(/)[-1] # 如 shenzhen_12_08.png prefix name.rsplit(_, 2)[0] # 去掉行列号保留大图来源前缀 groups.setdefault(prefix, []).append(p) keys list(groups.keys()) random.Random(42).shuffle(keys) split int(len(keys) * 0.8) train_keys, val_keys keys[:split], keys[split:] train_files [p for k in train_keys for p in groups[k]] val_files [p for k in val_keys for p in groups[k]] print(ftrain: {len(train_files)}, val: {len(val_files)})这里的关键是rsplit(_, 2)[0]把瓦片文件名最后两段“行号_列号”切掉剩下的部分当作场景来源标识。如果你的数据有地理坐标甚至可以按经纬度分块那会更严格。对绝大多数情况按目录名或文件名前缀分组就够用了。划分完之后生成train.txt、val.txt后续训练框架直接读这两个文件。3. 把标签转成训练能吃的索引图从调色板PNG到单通道label3.1 标签的真相调色板PNG和单通道索引图的差别训练语义分割模型时标签要被当作CrossEntropyLoss的 target 输入。这个损失函数要求 target 是单通道整数张量每个像素的值是类别ID范围在[0, num_classes)之间。问题来了很多现成遥感数据集的标签是调色板PNG读取出来是RGB三通道的彩色图每个类别用一组RGB值表示。如果直接把三通道彩色图当 target 传给损失函数PyTorch 会报维度错误或者在维度对齐时悄悄出错。即使不出错模型的 logits 是三通道的——height、width、num_classes标签也是三通道的——height、width、RGB两个“3”含义完全不同模型学的是“这张图里哪些颜色组合更像建筑”而不是“哪些像素是建筑”。正确的做法是把彩色标签通过颜色映射表转成单通道索引图每个像素只保留一个整数ID。这就是标题里“已处理完”的真实含义——数据制作者已经帮你做了大部分工作但你仍需验证标签到底是不是符合模型输入要求。遥感数据集的标签通常以两种形式存在存储形式读取方式能否直接训练单通道索引PNGL/P模式np.array(Image.open(p))出来就是(H, W)能但要核对像素值范围彩色调色板PNGRGB模式np.array(Image.open(p).convert(RGB))出来是(H, W, 3)不能必须先映射成索引图3.2 颜色映射表与转换脚本把8类标签变成模型认识的整数先把数据里出现过的颜色全部打出来看看颜色和类别的对应关系。这一步不能省不同数据集对同一种颜色含义的定义经常不一样比如红色可能是建筑也可能是裸地。打印颜色的脚本images glob.glob(datasets/labels/*.png)[:200] color_set set() for p in images: arr np.array(Image.open(p).convert(RGB)) # 把 H*W 个像素的 RGB 三元组变成集合 color_set.update(map(tuple, arr.reshape(-1, 3))) print(sorted(color_set))跑完之后你会拿到一份类似{(0, 0, 0), (128, 0, 0), (0, 128, 0) ...}的颜色集合。接下来要做的是把这组颜色逐一对到8个类别ID上。这个过程要参照你在2.2里整理的类别定义表如果发现颜色集合里出现第9种颜色要么是数据标注时留下的噪声要么是第8类“其他”的定义里有多个颜色。我的建议是把这个颜色当作背景或 ignore 处理不要让噪声污染训练。根据颜色表写映射脚本把整份标签转成单通道索引图存到labels_index/目录下import numpy as np from PIL import Image import glob import os # 以实际打印出来的颜色表为准下面只是示例 COLOR2ID { (0, 0, 0): 0, # 背景 (128, 0, 0): 1, # 建筑 (128, 128, 0): 2, # 道路 (0, 128, 0): 3, # 植被 (0, 0, 128): 4, # 水体 (128, 128, 128): 5, # 车辆 (128, 0, 128): 6, # 裸地 (0, 128, 128): 7, # 其他 } os.makedirs(datasets/labels_index, exist_okTrue) for p in glob.glob(datasets/labels/*.png): rgb np.array(Image.open(p).convert(RGB)) h, w rgb.shape[:2] idx np.zeros((h, w), dtypenp.uint8) for color, class_id in COLOR2ID.items(): mask (rgb color).all(axis-1) idx[mask] class_id out_path os.path.join(datasets/labels_index, os.path.basename(p)) Image.fromarray(idx, modeL).save(out_path)这段脚本的核心逻辑是mask (rgb color).all(axis-1)。rgb color把每个像素的三通道和当前颜色逐位比较得到一个(H, W, 3)的布尔数组.all(axis-1)要求三个通道同时相等最终得到这张图里“哪些像素是当前类别”的掩膜。用uint8存索引图足够8个类别完全用不完0~255的空间。不要用int64存标签内存会白白浪费好几倍后面进 DataLoader 时还影响吞吐。3.3 转换后的三个校验等式数量、维度、类别完整性不能少转换完不能直接开训先验证一遍“没翻车”。我习惯做三个校验原图和标签数量一致、尺寸一致、索引图的类别集合等于0~7。glob.glob(datasets/labels/*.png) glob.glob(datasets/labels_index/*.png) # 数量一致 import numpy as np from PIL import Image import glob bad [] for p in glob.glob(datasets/labels_index/*.png)[:200]: img Image.open(p) arr np.array(img) # 校验1单通道 assert arr.ndim 2, f{p} has {arr.ndim} dims # 校验2类别ID完整 if set(np.unique(arr)) - set(range(8)): bad.append(p) print(f{p} - {np.unique(arr)}) print(校验完成)这里的assert arr.ndim 2确保存出来的是单通道索引图set(np.unique(arr)) - set(range(8))检测有没有出现 0~7 之外的像素值。出现意外值只有两种可能颜色表漏填了某种颜色或者原始标签有标注边界噪声。如果是漏填去补映射表再跑一遍如果是噪声这个现象我要提醒你直接去掉或把它设成背景不要在训练时才发现问题那会儿定位成本高得多。把转换好的目录结构固定下来后续所有训练脚本都从这里读数据datasets/ ├── images/ # 原始遥感影像 ├── labels_index/ # 转好的单通道索引图 └── splits/ ├── train.txt # 训练集文件名列表 └── val.txt # 验证集文件名列表train.txt里每行一个文件名不带扩展名大多数训练框架的CustomDataset都认这种格式。这一步做完数据才是真正“可以训练”的状态。4. 用 SegFormer 把这个数据集跑通最小训练流程4.1 模型选型SegFormer、DeepLabV3 还是 Mask2Former1000张遥感城市图像8个类别这个体量下选模型要考虑两个因素一是遥感图像的尺度跨度大建筑边缘细节和小目标并存二是数据量有限模型不能太贪。我建议第一版直接上 SegFormer-B0 或 B1。对比一下常见选型模型特点对这个数据集的适配UNet占用资源少收敛快作为 baseline 可以但对大尺度遥感图像感受野不够DeepLabV3空洞卷积多尺度能力好稳定适合当对比模型SegFormerTransformer 编码器加轻量MLP解码头迁移动态好1000张数据量下不容易过拟合推荐先用Mask2Former掩码级训练效果强但参数多数据量到这个规模容易过拟合不建议一上来就用如果你之前用过 YOLO 系列做目标检测或实例分割那要确认一点YOLO 家族解决的是“框住目标”和“分开每个实例”语义分割要的是逐像素的类别归属不区分个体。所以这里不能用检测框架的思路得走像素级分类模型。Mask2Former 虽然能做语义分割但它本质是掩码训练范式调参成本高数据规模不够时收益不明显。4.2 用 mmsegmentation 把数据塞进训练管线这里我用 mmsegmentation 来跑因为它是语义分割里配置化程度最高的框架改数据集、改模型、改损失都不用动主代码。首先准备一个数据配置告诉框架从哪里读图、从哪里读标签# configs/_base_/datasets/remote8.py dataset_type CustomDataset data_root datasets/ train_pipeline [ dict(typeLoadImageFromFile), dict(typeLoadAnnotations), dict(typeRandomFlip, prob0.5), dict(typeRandomRotate, prob0.5, degree90), # 遥感图旋转不变性 dict(typeResize, img_scale(1024, 1024), ratio_range(0.8, 1.2)), dict(typeNormalize, mean[123.675, 116.28, 103.53], std[58.395, 57.12, 57.375]), dict(typePackSegInputs) ] data dict( traindict( typedataset_type, data_rootdata_root, ann_filesplits/train.txt, img_dirimages, seg_map_dirlabels_index, pipelinetrain_pipeline), valdict( typedataset_type, data_rootdata_root, ann_filesplits/val.txt, img_dirimages, seg_map_dirlabels_index, pipelineval_pipeline) )seg_map_dir指向的是第3章转换出来的labels_index不是原始labels。这个位置最容易写错写错的话框架不会报错但训练出来的模型会认为“标签是彩色图”推理结果一团糟。模型配置里最关键的只有两处num_classes8以及损失函数设置。SegFormer 默认的解码头来自 ADE20K 的150类不改num_classes的话模型输出通道维度对不上训练直接崩。辅助头也要跟着改成8类否则主头学好了辅助头还在往150类上输出。# segformer_b0_1024x1024_remote8.py model dict( typeEncoderDecoder, backbonedict( typeMixVisionTransformer, init_cfgdict( typePretrained, checkpointcheckpoints/segformer_mit-b0_512x512_160k_ade20k.pth)), decode_headdict( typeSegformerHead, num_classes8, loss_decodedict(typeCrossEntropyLoss, use_sigmoidFalse, loss_weight1.0)), auxiliary_headdict( typeFCNHead, num_classes8, loss_decodedict(typeCrossEntropyLoss, loss_weight0.4)) )训练时用分布式命令或单卡命令都行单卡先跑通bash tools/dist_train.sh configs/segformer_b0_1024x1024_remote8.py 1第一次跑通训练后先看 train 集上的 loss 是否稳定下降再看 val 集上的 mIoU。如果 loss 降到0.2以下还继续跑观察验证指标是否同步上涨出现“loss降但mIoU不动”的现象直接跳到第5章5.1。4.3 这个数据集上最值得调的4个超参数1000张遥感数据不算多超参数调对能省一倍时间。我一般先盯这4个crop size。训练时的裁剪尺寸选1024或768。遥感图像分辨率高如果按 ImageNet 习惯用512建筑边缘细节和车辆等小目标在下采样两层后直接消失。显存有限就选768不要低于512。batch size。SegFormer-B0 1024裁剪单张12G显存大概能放4张。batch小导致BN统计不稳定用梯度累积来模拟更大的batch每4步累积一次等效batch16mIoU能比直接batch4提高1~2个点。学习率。用2e-4作为初始学习率backbone 部分的 lr 乘0.1。遥感图不像自然图像那样有大量预训练特征可直接迁移主干学习率太高会把预训练权重冲掉太低又学不到遥感特有的纹理。ignore_index。如果你的标签里有255或未标注区域在损失里设ignore_index255。不设置的话未标注区域的像素直接参与梯度计算把模型往错误方向拉。设置完记得验证一下标签里确实没有255以外的非法值否则第3章的校验白做了。这个流程跑完你应该有一个能收敛的模型。接下来真正区别经验高低的是踩坑排查能力。5. 遥感语义分割避坑清单1000张数据上最容易翻车的5个地方5.1 现象训练 loss 一直降val mIoU 就是不动训练到第5个epochloss 从1.2掉到0.3打印 val 集的 mIoU 却只有50%而且连续几个epoch不涨。原因是遥感城市数据的类别分布极度不均衡道路、建筑、背景可能占了全部像素的85%以上交叉熵损失被大头类别主导模型只需要把每张图的建筑和道路分对loss 就能很低车辆、水体这些小类别学不学无所谓。解决方法是给损失函数加类别权重。权重用中位数频率平衡法对占比小的类别赋予更高的权重import torch import torch.nn as nn # class_freq 来自第2.2节的像素直方图统计 freq torch.tensor(class_freq, dtypetorch.float32) weights 1.0 / torch.log(1.02 freq) weights weights / weights.sum() * len(weights) # 归一化 criterion nn.CrossEntropyLoss(weightweights, ignore_index255)log的底数是21.02是平滑项防止某类占比太低导致权重爆炸。加了权重之后 loss 数值会上升这是正常现象不要因为 loss 变大就回退盯着 val mIoU 看。5.2 现象建筑边缘像锯齿细道路被识成断线预测图上建筑的轮廓边缘有明显锯齿4~8像素宽的人行道被切成一段一段。原因是模型下采样 stride 过大小目标和高频边界被池化层抹掉了。遥感图像的细节纹理比自然图像密集道路边缘在原始图上可能只有3~5像素经过2次下采样就剩1个像素再进到注意力模块里基本没信息了。三个手段组合解决第一推理时用第6章的滑窗降低单次缩放带来的信息损失第二训练时开辅助损失auxiliary_head让浅层特征也参与梯度回传深层语义和浅层边缘各学各的第三crop size 不要小于768Resize 的缩放范围控制在ratio_range(0.8, 1.2)别把原图压得太狠。5.3 现象车辆和水体这类小类别直接消失mIoU 为0验证集里车辆类别的 IoU 算出来是0水体也是0。原因是这两个类别在整份数据里像素占比可能都不到0.5%连类别权重都救不回来。另一个常见原因是大图里的车辆只有几十个像素模型下采样后直接消失了。解决思路是过采样含小类别的图。先统计每张标签里含哪些类别训练时把含车辆的图重复采样保证每个批次里都出现车辆。采样逻辑可以参考import random # car_images 是统计后得到的“含车辆类别”的图像路径列表 def sample_batch(batch_size): batch [] # 每个batch里强制放1~2张含小类别的图 batch random.sample(car_images, k2) batch random.sample(all_images, kbatch_size - 2) return batch除了过采样还有一个容易忽略的点推理时把原图缩放到1024再进去车可能只剩3个像素。如果验证指标差在这类小目标上先停一停检查 inference 时的图像尺度别一口咬定模型不行。5.4 现象val mIoU 比 train 高十来个点一看指标val mIoU 65%train mIoU 52%直觉不对劲。最可能的原因是数据泄漏——第2.3节里提到的瓦片同源问题。遥感大图切成瓦片后同一区域的相邻瓦片有大量重叠语义随机划分时训练集和验证集各自都包含了同一片区域的不同瓦片模型在训练时已经见过了验证区域。另一种可能性是验证集正巧挑到了大片连通的建筑区或大路区这类类别好分指标虚高。解决方法是回到第2.3节按瓦片来源重新划分并且验证集按类别均衡抽样至少保证每个类别在验证集里都能看到20张以上的图。5.5 现象可视化输出颜色混乱和标签对不上模型推理完把预测数组用plt.imshow(pred)直接展示出来的颜色乱糟糟建筑是紫色道路是绿色跟训练标签的风格完全不同。原因是预测结果是类别索引值是0~7的整数matplotlib 把它当灰度或默认colormap映射成了伪彩色。这不代表模型训练失败只是可视化方式不对。正确的做法是定义一份固定的8类colormap把索引映射成固定的RGB颜色再显示colormap np.array([ [0, 0, 0], # 背景 [128, 0, 0], # 建筑 [128, 128, 0], # 道路 [0, 128, 0], # 植被 [0, 0, 128], # 水体 [128, 128, 128], # 车辆 [128, 0, 128], # 裸地 [0, 128, 128], # 其他 ], dtypenp.uint8) vis colormap[pred_idx] # pred_idx 是预测的索引图 (H, W)可视化对比是语义分割项目里最容易被低估的环节。没有统一的colormap你永远不知道模型到底错在哪里。这个习惯建立起来之后踩坑速度会明显变快。6. 重叠滑窗推理把瓦片接缝和边界抖动一次压掉6.1 重叠滑窗推理消除接缝训练完的模型要在大尺寸遥感图上做推理时不能整图直接塞进网络。显存放不下而且大图直接缩放会把建筑、车辆这些细节压没了。常见做法是滑窗推理每块单独预测再拼回去。问题是窗口边缘的预测质量比中心差拼出来的图有明显接缝。我给模型推理加一个 overlap 参数让相邻窗口重叠64像素重叠区域的预测结果做均值融合接缝会明显缓解def slide_infer(model, img, window512, overlap64): h, w img.shape[:2] step window - overlap out np.zeros((h, w, 8), dtypenp.float32) cnt np.zeros((h, w, 1), dtypenp.float32) for y in range(0, h - window 1, step): for x in range(0, w - window 1, step): patch img[y:y window, x:x window] input_tensor preprocess(patch).unsqueeze(0).cuda() with torch.no_grad(): logits model(input_tensor) logits logits.squeeze(0).permute(1, 2, 0).cpu().numpy() out[y:y window, x:x window] logits cnt[y:y window, x:x window] 1 prob out / cnt return prob.argmax(axis-1).astype(np.uint8)overlap64时窗口中心区域会被多次预测求平均边缘处的低质量预测被稀释。参数上1024的窗口配64的overlap基本够用窗口越大单次推理越慢但上下文更全。如果你在500张以上的验证集上做评估滑窗加 overlap 能比整图缩放推理涨1~3个 mIoU 点尤其是建筑边缘和道路细节。6.2 用逐类IoU给数据集和模型做体检全局mIoU只能告诉你模型整体行不行看不出哪个类别在拖后腿。我的习惯是推理完成后立刻逐类算IoU把8个类别的分数打出来from collections import defaultdict ious defaultdict(float) for pred, gt in zip(pred_list, gt_list): for c in range(8): inter ((pred c) (gt c)).sum() union ((pred c) | (gt c)).sum() ious[c] inter / (union 1e-6) for c in range(8): print(fclass {c}: IoU {ious[c] / len(pred_list):.4f})这份表比任何单一指标都诚实。如果车辆IoU是0先回去看第2.2节的类别占比确认训练集里到底有几张图含车辆如果建筑IoU只有40%去看可视化的预测图是边缘锯齿还是大块漏检。把每个类别的失败模式记录在案再决定调损失、调权重还是补数据。我现在的习惯是换任何数据集第一步就把逐类IoU和瓦片归属表拉出来否则训练多少次都可能白跑。这个数据集的坑不算多但绕过的每一个都对应着少走的一段弯路希望帮到你。本文还有配套的精品资源点击获取