拓冰建站拓冰建站
首页 / 资讯中心 / 正文

遥感城市图像语义分割:从数据集体检到地物面积估算的完整实践

简介面向遥感城市图像语义分割的数据集资源聚焦海陆区域与典型地物类别划分适用于城市要素提取、地表覆盖分类等视觉任务。数据已完成训练集与验证集划分训练集约800组图像与标注验证集约300组标签覆盖背景、建筑等8个类别并附类别说明文件可直接对接常用分割网络。包内图像与标签目录一一对应免去样本整理的繁琐步骤。另提供一个可视化脚本可随机抽取样本并同时展示原始图像、真实标注及标注在原图上的蒙版叠加效果便于快速核验标注质量与模型输出。资源共2000个文件以JPG原始影像和PNG标签图像为主辅以类别说明文本与可视化脚本压缩包约42.05MB轻量易用。已有129人学习下载适合具备一定深度学习基础、正在开展遥感分割实验的研究者或学习者直接使用既可用于训练分割模型也方便进行算法改进验证。1. 遥感城市图像语义分割数据集这份“开箱即训”的数据到底藏了多少活“遥感城市图像语义分割数据集”这种标题从业者看到的第一反应不是“能跑什么模型”而是“数据本身已经帮我省掉了什么”。约 1000 张带标签的 8 类别图像意味着标注、格式转换、类别映射、训练验证划分这些最脏最累的环节已经做完你不用碰 LabelMe不用纠结 shp 转栅格时坐标偏移也不用花两周去清洗错标。但“已处理完”不等于“拿来就训”标签编码方式、类别不均衡、图像尺寸差异、验证集划分方式每一项都会在训练和部署时反过来咬你一口。这篇文章按最快落地路径走先验证数据再跑通训练最后把模型输出换算成地物面积这类真实指标。2. 八类别标签体系与数据组成先弄清一张标签图里藏的两套编码2.1 从 RGB 掩膜到类别 id两套常见编码方式语义分割数据集在交付时“已处理”通常指两种形态之一。第一种是标签保存为单通道灰度图像素值只取 0 到类别数减 1也就是 0 到 7加载后可以直接作为 CrossEntropy 的目标张量。第二种是标签保存为三通道彩色编码图每一类地物对应一个固定的 RGB 色值训练前必须先查色表、把 RGB 像素映射成类别 id否则模型输出的通道数就会对不上。表常见城市地物 8 分类映射方式示意拿到数据后务必先核对类别 id常见地物含义灰度标签值彩色标签示意RGB0不透水面硬化地面、广场等0(0, 0, 0)1建筑1(128, 0, 0)2低矮植被草地、耕地2(0, 128, 0)3高植被林地、树冠3(0, 0, 128)4道路4(128, 128, 0)5水体5(0, 128, 128)6裸地6(128, 0, 128)7其他7(255, 255, 255)这张表是常见惯例不代表你手里的数据一定长这样。不同来源的城市地物 8 分类往往把“不透水面”和“建筑”拆分合并的次序完全颠倒。训练之前最该做的第一件事是打印标签图的唯一像素值import numpy as np from PIL import Image label_path masks/0001.png label np.array(Image.open(label_path)) print(shape:, label.shape, dtype:, label.dtype) print(unique values:, np.unique(label))如果输出的 shape 是 (H, W)说明这是单通道灰度标签直接可用。如果 shape 是 (H, W, 3)则必须做色表到类别 id 的映射。打印 unique 之后还要核对类别数如果最大值是 7说明 8 类齐全如果最大值只有 4说明这份数据只有 5 个类别在图像里实际出现有可能是部分类别数量极少也有可能是标签漏标。这里就引出一个习惯永远不要用标题上的类别数替代代码输出的事实。2.2 目录组织与图像-标签配对检查用一份脚本把约 1000 张数据梳理成可训练清单这类数据集最常见的目录组织是images/和masks/两个文件夹文件同名、扩展名不同比如images/0001.tif对应masks/0001.png。但真实交付时偶尔会出现少数图像缺少标签、文件名前后缀不一致、尺寸不匹配等情况。先把约 1000 张数据整体体检一遍比训练时让 dataloader 崩掉再回头排查高效得多。import os from glob import glob from collections import Counter from PIL import Image import numpy as np img_dir images mask_dir masks imgs sorted(glob(os.path.join(img_dir, *.tif))) masks sorted(glob(os.path.join(mask_dir, *.png))) img_ids {os.path.splitext(os.path.basename(p))[0] for p in imgs} mask_ids {os.path.splitext(os.path.basename(p))[0] for p in masks} print(missing masks:, img_ids - mask_ids) print(orphan masks:, mask_ids - img_ids) size_counter Counter() for p in imgs: with Image.open(p) as im: size_counter[im.size] 1 print(image size distribution:, size_counter)这段逻辑分两部分先做 id 差集检查找出“有图没标签”和“有标签没图”的文件再用Counter统计所有图像的尺寸分布。如果尺寸分布出现两三种分辨率混在一起后续训练要么统一随机裁剪要么按分辨率分组处理千万别直接全部 resize 到一个固定尺寸小地物会被抹掉这个坑在第 5 章展开。整套检查脚本应在训练之前跑完并把输出保存成一份filelist.csv作为后续划分训练集、验证集的唯一清单而不是每次都在 dataloader 里临时扫描目录。2.3 肉眼快速审查把标签叠加到原图上确认边界脚本能查出文件级问题但查不出语义级问题。有些自动标注或半自动标注得到的遥感掩膜建筑边界会整段错位几个像素水体边缘会有毛刺道路会被植被标签吃掉一段。这类错标在 mIoU 指标上表现为“持续偏低且不管怎么调参都涨不上去”。所以第一轮检查应该做一次可视化叠加随机抽 20 到 30 张图把标签半透明盖在原始影像上看边界。import matplotlib.pyplot as plt import numpy as np from PIL import Image img np.array(Image.open(images/0042.tif).convert(RGB)) label np.array(Image.open(masks/0042.png)) if label.ndim 3: label label[:, :, 0] fig, axes plt.subplots(1, 2, figsize(14, 6)) axes[0].imshow(img) axes[1].imshow(img) overlay axes[1].imshow(label, cmaptab20, alpha0.45) axes[1].set_title(label overlay) plt.show()叠加图里重点看三件事类别边缘是否贴合地物轮廓、细长地物道路、河流是否连续、小目标独立建筑是否被周围类别吞并。遥感影像和自然图像最大的区别在于视角是从上往下建筑和道路这类人工地物边缘通常非常锐利如果看到掩膜边缘出现 2 到 3 个像素的模糊过渡带往往是标注脚本里用了过大半径的形态学膨胀导致的这种标签喂给模型模型学到的边界就会是“糊”的。3. 本地跑通训练的最小方案从类别权重到 U-Net 的完整落地3.1 训练前先算类别权重城市影像默认就是极不均衡的遥感城市图像里建筑、不透水面、道路的像素数量经常是水体、裸地的几十倍。如果直接用普通交叉熵模型会倾向把所有像素都预测成出现频率高的类别因为这样 loss 已经很低了。常见的做法是做一次全局像素统计用中位数频率平衡计算每个类别的权重把权重传给 CrossEntropy 的weight参数。import numpy as np from glob import glob from PIL import Image mask_paths sorted(glob(masks/*.png)) freq np.zeros(8, dtypenp.float64) for p in mask_paths: m np.array(Image.open(p)) if m.ndim 3: m m[:, :, 0] for c in range(8): freq[c] np.sum(m c) freq / freq.sum() median_freq np.median(freq[freq 0]) weights median_freq / np.maximum(freq, 1e-6) weights weights / weights.sum() * len(weights) print(class frequency:, freq) print(class weights:, weights)这段统计会告诉你两个信息哪些类别占比极低哪些类别在数据集中根本没出现。对于占比为 0 的类别权重会被maximum兜底成一个大数但这不代表数据里有这个类别而是说明这份 8 分类数据实际可能只有 6 到 7 个类别有像素。这种情况下有两个选择一是把不存在类别的权重设为 1.0让模型不需要为它分配输出二是把掩膜里这个类别重新映射成 255在 CrossEntropy 里用ignore_index255跳过。经验做法是后者因为类别 id 一旦少于 8意味着顶层分类逻辑和验证脚本都要跟着改不如保留 8 通道输出、只用ignore_index控制梯度回传范围。3.2 最小可跑配置PyTorch Dataset 类与训练循环得到类别权重后进入训练落地。对于约 1000 张的规模U-Net 是最稳的基线DeepLabV3 和 SegFormer 也可以在这份数据上跑但调试成本会高不少。下面给出一个完整可跑的 Dataset 类包含随机裁剪和翻转增强这部分是所有下游工作的公共底座。import torch from torch.utils.data import Dataset from PIL import Image import numpy as np class RemoteSegDataset(Dataset): def __init__(self, img_paths, mask_paths, crop_size512, trainTrue): self.img_paths img_paths self.mask_paths mask_paths self.crop_size crop_size self.train train def __len__(self): return len(self.img_paths) def __getitem__(self, idx): img np.array(Image.open(self.img_paths[idx]).convert(RGB)) mask np.array(Image.open(self.mask_paths[idx])) if mask.ndim 3: mask mask[:, :, 0] h, w img.shape[:2] if self.train: top np.random.randint(0, max(h - self.crop_size, 1)) left np.random.randint(0, max(w - self.crop_size, 1)) img img[top:top self.crop_size, left:left self.crop_size] mask mask[top:top self.crop_size, left:left self.crop_size] if np.random.rand() 0.5: img img[:, ::-1]; mask mask[:, ::-1] else: img np.array(Image.fromarray(img).resize((self.crop_size, self.crop_size))) mask np.array(Image.fromarray(mask).resize((self.crop_size, self.crop_size), Image.NEAREST)) img torch.from_numpy(img.transpose(2, 0, 1)).float() / 255.0 mask torch.from_numpy(mask).long() return img, mask训练模式下对原始图像做随机裁剪避免直接 resize 导致地物形状畸变验证模式下用最近邻插值统一到固定尺寸因为标签不能用双线性插值会把类别 id 插出小数。把数据归一化到 0 到 1 而不是 0 到 255可以在不引入 ImageNet 统计量的情况下让训练更稳定减少一个需要调试的变量。训练循环部分不写完整工程代码只给关键骨架model UNet(in_channels3, num_classes8).cuda() crit torch.nn.CrossEntropyLoss(weighttorch.tensor(weights).cuda(), ignore_index255) opt torch.optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-4) sched torch.optim.lr_scheduler.CosineAnnealingLR(opt, T_max150) for epoch in range(150): model.train() for img, mask in train_loader: pred model(img.cuda()) loss crit(pred, mask.cuda()) opt.zero_grad(); loss.backward(); opt.step() sched.step()batch size 建议 8输入 512×5128 通道输出U-Net 在 8GB 显存上是极限16GB 可以放宽到 16。学习率 1e-4 是遥感场景比较稳的选择1e-3 在 U-Net 上容易前面跌得快、后面震荡。训练 150 轮配合余弦退火基本上 1000 张数据能在两三个小时内看到 mIoU 从 0.3 爬到 0.6 以上。如果追求更高精度可以换成 DeepLabV3 或 SegFormer但在最小验证阶段U-Net 的调试成本最低参数也最好猜。3.3 损失函数组合交叉熵加 Dice 抵御小类别崩坏只靠带权重的交叉熵小类别水体、裸地大概率学到了但 recall 不高因为权重只是放大了梯度并没有直接优化“区域重叠”。这时候拼一个 Dice loss 是常见方案它关注的是预测区域和真实区域的交叠比例对小目标更敏感。def dice_loss(pred, target, eps1.0): pred torch.softmax(pred, dim1) target_onehot torch.nn.functional.one_hot(target, num_classes8).permute(0, 3, 1, 2).float() intersection (pred * target_onehot).sum(dim(2, 3)) union pred.sum(dim(2, 3)) target_onehot.sum(dim(2, 3)) return 1 - (2 * intersection eps) / (union eps)组合公式为loss ce_loss dice_weight * dice_lossdice_weight取 0.3 到 0.5 之间太高会让训练震荡太低起不到保护小类别的作用。Dice loss 在训练早期 loss 下降会显得比纯交叉熵慢这是正常的因为它同时优化“分类正确”和“区域重合”两个目标在梯度方向上偶尔吵架。这里需要强调一个经验加了 Dice 之后每类 IoU 单看往往会更均匀但 mIoU 不一定会比纯交叉熵高因为模型会在主类上让出一些精度来换取小类止损。所以评估时不要只盯 mIoU要看整张每类 IoU 表。4. 数据划分与增强的细节固定种子、瓦片分组与验证集策略4.1 按瓦片划分而不是按像素随机划分避免空间信息泄漏遥感数据最特殊的属性是空间自相关性同一块建筑群、同一条道路的相邻 patch像素分布几乎一样。如果直接把所有图像像素打散后随机分训练集和验证集模型在训练时已经“见过”验证区域附近的空间纹理验证指标会虚高部署到新区域立刻露馅。正确做法是按图像原始瓦片分组同一个瓦片的所有切块只能出现在一个集合里。import os, random import pandas as pd filelist pd.read_csv(filelist.csv) filelist[tile_id] filelist[image_path].str.split(/).str[-1].str.split(_).str[0] tiles sorted(filelist[tile_id].unique()) random.seed(42) random.shuffle(tiles) n_val int(len(tiles) * 0.2) val_tiles set(tiles[:n_val]) train_tiles set(tiles[n_val:]) train_df filelist[filelist[tile_id].isin(train_tiles)] val_df filelist[filelist[tile_id].isin(val_tiles)]这里把文件名中的前缀当作 tile 编号比如city03_000128.tif归属于 tilecity03同一 tile 的所有图像不跨集合。约 1000 张的数据量按 8:2 划分后训练集大约 800 张、验证集 200 张。固定随机种子 42 的目的是让每一次实验的划分完全一致否则你换个 seed 重跑mIoU 波动 3 到 5 个点都说不清是模型问题还是数据切法问题。4.2 增强的度遥感影像别照搬自然图像的增强套路语义分割常用的增强有翻转、旋转、缩放、颜色抖动。但遥感影像和自然图像有几个关键差异地物方向具有物理意义车辆、建筑阴影的投影方向都与拍摄角度相关90 度的旋转增量比任意角度更安全颜色抖动不能开太大因为植被和水体在真彩色影像上有相对稳定的光谱特征抖动过头会让模型学到错误的相关性随机遮挡类增强RandomErasing、Cutout在这类细颗粒地物分割任务里基本是负优化一个小方块盖住可能就是整段道路或整栋建筑。class SegTrainTransform: def __init__(self, crop_size512): self.crop_size crop_size def __call__(self, img, mask): if np.random.rand() 0.5: img img[:, ::-1]; mask mask[:, ::-1] if np.random.rand() 0.5: img np.ascontiguousarray(img[::-1]); mask np.ascontiguousarray(mask[::-1]) k np.random.choice([0, 1, 2, 3]) if k: img np.rot90(img, k); mask np.rot90(mask, k) return img, mask翻转概率各取 0.5旋转只取 90 度的整数倍配合随机裁剪即可。多尺度训练可以加但对 1000 张的数据量来说收益有限还容易让验证集指标波动建议先不加。分割任务的增强原则是“保持语义不变形”像拉伸、透视这类几何增强在自然图像比赛里很常见但遥感地物的尺度是相对固定的一棵树的树冠大小不会被透视变成一条路的大小。4.3 验证指标mIoU 之外每类 IoU 才是排障入口很多第一次跑分割的读者会盯着 mIoU但 mIoU 是在类别维度上先算 IoU 再平均它会被大类完全淹没。比如 8 个类别中有 3 类占比超过 70%模型只要把这 3 类学好了mIoU 就能到 0.65 以上剩下 5 类可能全是零。正确的验证姿势是打印一张每类 IoU 表类别IoU像素占比不透水面0.7422%建筑0.6918%低矮植被0.6125%高植被0.5812%道路0.529%水体0.435%裸地0.214%其他0.335%表格是示意数据但它代表了一类典型现象模型在大类上表现尚可在裸地、其他这类占比低、语义模糊的类别上直接崩掉。如果只看 mIoU你会以为模型已经可用实际上把预测图叠加到原图上裸地全被错分成建筑影子。所以每次训练结束都把 per-class IoU 和混淆矩阵导出成 CSV和模型权重放在同一个实验目录下方便后续对比实验。5. 遥感分割数据集的五个常见坑现象、原因与解决5.1 标签图是三通道被当作单通道读入后类别数爆炸现象训练刚开始 loss 正常跑几个 batch 后 OOM 或者 loss 变成 nan排查发现模型输出通道和标签不一致。 原因标签颜色编码图是 (H, W, 3)直接读进来后标签张量变成 3 个通道CrossEntropy 在通道维上做 one-hot 匹配时崩溃有时程序不报错但会把 H、W、C 三个维度任意一个当成类别维产生几百个“类别”。 解决在 Dataset 的__getitem__里拿到标签后先判断ndim等于 3 时只取[:, :, 0]并按 2.1 节的色表做重映射。重映射时不要用 for 循环遍历每个像素用 numpy 的索引一次性替换否则约 1000 张图跑完预览要十几分钟。5.2 图像尺寸不统一直接 resize 后道路和水体断裂现象训练时为了凑 batch 把所有图 resize 到 512×512loss 下降正常但验证时细长地物的 IoU 特别低尤其是道路的 recall 只有 0.3 左右。 原因原始影像分辨率可能在 1024×1024 到 2048×2048 之间直接缩小到 512 后4 到 6 像素宽的道路变成 1 到 2 像素多数被插值抹掉了。更隐蔽的是不同尺寸的图缩小比例不同模型看到的“道路宽度”在不同样本间不一致它没法学到稳定的尺度特征。 解决训练阶段用随机裁剪把大图裁成 512×512 的小块再进模型验证阶段用滑窗推理而不是 resize。这会在训练代码里多写几行但对遥感任务是必须的。5.3 验证集按像素随机划分指标虚高导致部署翻车现象训练验证 mIoU 0.71自我感觉良好换到另一块城市影像上预测mIoU 直接掉到 0.42。 原因验证集没有按瓦片划分同一地块的相邻 patch 同时出现在训练和验证里。遥感影像的空间自相关比自然图像强得多模型记住了局部纹理而不是泛化出地物语义。 解决按 4.1 节的 tile 分组方式重新划分并保留划分结果的 CSV 文件作为实验记录。这个坑是遥感分割里最隐蔽的因为 mIoU 的虚高不会报错只会让你在部署时被现实狠狠教育一顿。5.4 类别不均衡下模型把少数类全部吞掉现象训练到 80 轮后 mIoU 在 0.6 附近徘徊单独看水体 IoU 是 0预测图里所有水体区域都成了阴影或低矮植被。 原因直接把原始交叉熵当 loss低频类别的梯度贡献被高频类别压过模型学会“全部预测成背景”这个局部最优解。这类现象在遥感城市影像里几乎是必然出现的因为建筑和道路占比太高。 解决用 3.1 节算出的中位数频率权重喂给 CrossEntropy并叠加 Dice loss。如果仍不见效检查混淆矩阵里少数类是不是被连续错分到同一个类别通常是水体被错分成阴影低矮植被需要检查训练集里是否缺少“带阴影的水体”样本。5.5 Loss 正常但验证指标震荡翻出 10 张图才发现标签错位现象训练 loss 平滑下降验证 mIoU 每轮波动超过 5 个点而且波动没有周期性。 原因数据交付时有个别图像的标签和图像不是同一场景比如images/0173.tif配的是masks/0172.png肉眼很难发现但验证集里每次抽到这张图都会让 IoU 暴跌。 解决用 2.3 节的叠加可视化抽看训练集和验证集各 20 张图重点检查文件名后缀相近的样本。更系统的做法是计算图像和标签的相互信息或边缘一致性但对约 1000 张的数据量人工抽看 40 张图成本更低、更可靠。这条经验说明一个道理数据集交付再“干净”都不值得盲目信任可视化永远是第一道防线。6. 滑窗推理、地物面积估算与三类可视化验证的三个进阶技巧第一招是滑窗推理。验证和部署时整张遥感影像通常比训练时的裁剪尺寸大得多直接 resize 会丢失小地物。常见做法是让窗口在图像上按步长滑动窗口之间保留 25% 到 50% 的重叠推理结果在重叠区加权平均权重向窗口中心倾斜避免拼图边缘出现明显的拼接缝。步长取窗口尺寸的一半比如 512 的窗口步长 256既保证重叠又不会让推理时间膨胀到不可接受。第二招是把预测像素数换算成地物面积这是“地物面积估算”类应用的核心一步import numpy as np pred np.array(sliding_predict(model, big_image)) # (H, W) counts np.bincount(pred.ravel(), minlength8) gsd 0.55 # 地面采样距离单位米/像素从影像元数据读取 pixel_area gsd * gsd areas_m2 {i: counts[i] * pixel_area for i in range(8)} print(areas_m2)如果影像源是带地理信息的 GeoTIFFGSD 从元数据里读如果是普通图片就按相对面积处理。这里容易踩的坑是 GSD 的平方0.55 米和 0.75 米的差距在面积估算里接近一倍所以面积结果必须标注清楚假设的 GSD不要裸报一个数字。第三招是单类可视化验证。mIoU 是数字但真正决定交付质量的往往是某几个实用场景水体边界是否连续、建筑边缘是否平滑、道路是否有断头。我自己的习惯是每个 epoch 结束后固定保存 3 张验证图的预测结果一张看整体、一张放大看道路、一张放大看水体边界跑完训练后快速翻一遍图片序列比盯着 loss 曲线更容易发现问题。这三点做完这套约 1000 张数据的 8 类别分割流程才算真正闭环从标签体检到训练调参从瓦片划分到滑窗推理最后落在地物面积这种业务可用的输出上。养成固定随机种子、保存每次实验的每类 IoU、定期可视化抽检这三个习惯之后再换更大规模的数据集也只是量变不会再来一次从零踩坑。希望帮到你。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门