遥感屋顶语义分割:从数据集构建到模型训练实战

📅 2026/8/26 6:06:57
遥感屋顶语义分割:从数据集构建到模型训练实战
简介语义分割是像素级分类技术在遥感影像分析中扮演关键角色。它通过为每个像素赋予类别标签精确描绘地物边界相比目标检测框具备更高的几何精度尤其适用于建筑物屋顶提取、面积统计与三维重建等场景。实际项目中屋顶分割常面临类别不平衡、标注噪声、模型选型与部署性能等挑战。借助深度学习框架合理设计损失函数如Dice Loss与交叉熵的组合配合数据增强、多尺度推理及二阶段微调可显著提升mIoU和边界质量。本文以一套约6000张图像的屋顶分割数据集为起点从数据规格、UNet/FPN/DeepLabV3模型对比到训练调优、ONNX导出及GIS矢量化的完整链路系统梳理一套可复用的工程实践方案为遥感业务中的精细化屋顶分析提供落地参考。1. 项目起源与数据集要解决的真实问题1.1 为什么需要屋顶级别的语义分割做遥感这一行的人应该都有体会很多时候拿到一张高分辨率卫星图或航拍图第一件事往往不是看地物分布得有多漂亮而是先把建筑物、道路、水体、植被这些基本面给提出来。过去大家习惯用目标检测框来做建筑物提取一个矩形框框过去精度看起来还行但一落到实际业务里就露馅屋顶是不规则多边形矩形框天然会带上一堆背景像素算面积也好、做三维重建也好误差都大得让人没法接受。语义分割就不一样了。它是像素级的分类每个像素都会被打上这是屋顶这是道路这是树的标签输出的是一张和原图同尺寸的mask图。用mask去做面积统计、矢量化和后续GIS分析误差直接降一个量级。我当年第一次把分割结果叠加到CAD底图上时那种终于能用了的感觉至今还记得。这个项目的核心目标就是做一个专门针对遥感建筑屋顶的语义分割数据集。你可能觉得分割数据集不是一抓一大把吗确实不少但仔细去看就会发现通用分割数据集里屋顶往往只是建筑物这个大类的子集类别定义模糊屋顶和建筑混淆在一起而专门做屋顶的数据集又往往只针对某一个城市的影像风格换个地区、换个传感器模型性能就崩了。这个数据集的价值就在于它是冲着拿来就能训、训完就能用去的。1.2 现有公开数据集的痛点我最早尝试过直接拿公开数据集来训踩了不少坑。比如某著名的街景分割数据集质量和标注都很棒但它是街景视角屋顶在画面里的形态是透视变形的跟遥感顶视图完全是两回事。还有一些大规模的遥感数据集里面包含建筑物类别但分辨率、光照、季节差异太大而且类别体系里屋顶通常只是一个附带类像素占比极低模型学到的大多是这玩意儿是个建筑的模糊概念而不是屋顶边界的精确位置。还有一个更现实的问题是很多数据集的原始标注是GeoJSON或矢量格式需要自己切片、自己转栅格、自己对齐坐标系。我见过不少同行卡在这一步——数据下载下来一周了还没跑到模型训练那一行。这个项目在发布前就已经把这些脏活累活处理完了标签直接给的是训练可用的PNG mask图RGB图和mask图一一对应目录结构也是规规矩矩的train/val/test划分。1.3 数据集的定位直接服务语义分割训练我在设计这个数据集的时候给自己的要求非常明确下载下来解压之后5分钟内就能开始跑训练。所以我不光整理了图片和标签还顺手把训练集、验证集、测试集的文本清单都生成了甚至配套给了训练脚本的推荐设置。整个数据集的构建逻辑都是围绕减少从数据到模型的摩擦力来做的。当然光有数据还不够模型怎么选、训练参数怎么调、遇到类别不平衡怎么处理这些也都是实际复现时绕不开的问题。下面我会把数据集本身的细节、我在这个项目上跑模型的经验以及踩过的几个大坑全部摊开来讲。2. 数据集的完整规格与关键处理细节2.1 整体规模与组成先说大家最关心的规模整个数据集约6000张图像每张图像都有与之对应的像素级标签图。6000张听起来不算海量但考虑到遥感影像本身信息密度极高一张1024x1024的图可能包含几十栋甚至上百栋建筑这个数量的有效学习样本是相当充足的。我做实验的时候用这个数据及训练UNet在验证集上mIoU能到0.78左右训练时间单卡大约四五个小时性价比很高。数据集中图像的分辨率不是统一的我保留了原始影像的多尺度特性。这是因为在实际使用中传感器分辨率是变化的如果全部裁剪成固定大小模型很容易过拟合到特定尺度。我的处理方式是在打包数据之前按比例统一缩放到边长不超过1024像素的尺寸同时保持原始宽高比。这样既保证了训练时的显存可控又保留了不同地物在不同尺度下的形态多样性。2.2 类别体系设计这个数据集的类别体系是经过反复斟酌的。我见过很多语义分割数据集只标注建筑/非建筑二类训练起来倒是省事但一旦你希望把屋顶和周边环境的关系也建模进来就无能为力了。所以在这个数据集里我用了多类别分割的方案具体类别如下类别ID类别名称说明典型占比0背景无法归入以下类别的像素约55%1屋顶各类建筑屋顶包括平顶、坡顶、彩钢顶等约18%2道路沥青路、水泥路、泥土路等可通行路面约12%3植被树木、草地、农田等绿色覆盖约9%4水体河流、湖泊、池塘、游泳池等约3%5车辆可辨识的汽车、卡车等约1%6裸地未硬化的土地、施工场地等约2%把背景单独分出来同时把道路、植被这些高频地物也纳入标注是我在实践中总结出来的经验。因为语义分割模型在训练时非常依赖上下文信息——如果只标屋顶和背景模型很难区分像屋顶但不是屋顶的区域比如浅色地面、广场、车棚顶而把相关地物一起标注后模型能够学到更丰富的特征边界最终提升屋顶分割本身的精度。2.3 标注格式与目录结构在标注格式的选择上我用的是最通用的PNG格式单通道掩码图。每个像素的值对应类别ID比如屋顶区域的像素值是1、植被区域的像素值是3。这种格式有三大好处可以直接被绝大多数深度学习框架读取不需要额外写解析代码。文件体积小一张1024x1024的mask图只有几百KB。颜色映射完全可控训练时想可视化可以随便上色不依赖原始标注工具的固定配色。整个数据集的目录结构如下roof_seg_dataset/ ├── train/ │ ├── images/ │ │ ├── img_0001.jpg │ │ ├── img_0002.jpg │ │ └── ... │ └── masks/ │ ├── img_0001.png │ ├── img_0002.png │ └── ... ├── val/ │ ├── images/ │ │ └── ... │ └── masks/ │ └── ... ├── test/ │ ├── images/ │ │ └── ... │ └── masks/ │ └── ... ├── train_list.txt ├── val_list.txt └── test_list.txtimages里是JPEG格式的RGB原始影像masks里是对应的PNG格式标签图。文件名一一对应完全不用手动对齐。三个txt文件里每行是一对相对路径格式如train/images/img_0001.jpg train/masks/img_0001.png配合PyTorch等框架写Dataset类时极其方便。2.4 我踩过的预处理坑坐标系与对齐问题如果你接触过遥感数据一定知道一个让人头疼的问题原始影像经过正射校正后坐标信息是有的但一旦做切片处理很容易出现图像和标签错位。我在早期做过一批实验某些图像的标签错位了好几个像素训练出来的模型在边界上莫名多了一圈鬼影排查了很久才发现是切片时用了不同的插值方式图像用了双三次插值标签却用了最近邻导致边缘位移。这个数据集在处理时统一了规则RGB影像和mask图在切片时全部使用最近邻插值。虽然RGB图用最近邻会有一定的锯齿但好处是保证图像和标签的像素级对齐这种对齐对分割任务来说远重要于视觉上的平滑。如果你之后要自己做数据集这个细节一定要记住宁可影像看起来略微锐化也不能让标签错位。3. 模型选型与训练策略的关键决策3.1 Backbone与分割头的选型对比数据集准备好了模型选什么是下一个问题。我拿这个数据集跑了一遍主流的分割结构分别是UNet、FPN和DeepLabV3Backbone分别用了ResNet34和ResNet50。先说结论UNet ResNet34训练速度最快显存占用最小验证集mIoU约0.74。适合快速验证新想法。FPN ResNet50精度和速度比较均衡mIoU约0.77而且FPN本身是多层级特征融合对小屋顶的捕捉比UNet好一些。DeepLabV3 ResNet50精度最高mIoU约0.79但对显存要求也最高训练时间明显拉长。如果显卡是16G甚至更低建议别上这个组合。很多教程开头就推荐DeepLabV3但我个人觉得选模型要看你的最终落地方向。如果只是做面积统计这类粗粒度分析UNet就非常够用了如果要做精细的屋顶边界提取比如服务光伏板排布方案设计那还是FPN或者DeepLabV3更稳。实现上我推荐直接用segmentation-models-pytorch这个库一行代码就能创建模型。以FPN为例import segmentation_models_pytorch as smp model smp.FPN( encoder_nameresnet50, encoder_weightsimagenet, in_channels3, classes7, )用预训练Encoder的收益非常明显我在没有预训练权重的情况下从零训FPNmIoU只有0.63但换用ImageNet预训练后直接跳到0.77。遥感影像和自然图像虽然差异大但底层特征边缘、纹理、颜色渐变是通用的预训练权重能帮模型省掉大量底层特征学习的成本。3.2 损失函数交叉熵不是万能解药语义分割最常用的损失函数就是交叉熵Cross Entropy Loss简单稳定梯度回传也好算。但在这个数据集上如果直接用标准交叉熵训练到后半段会出现一个典型问题背景像素占大头模型会倾向于把不确定的区域都预测成背景导致屋顶召回率偏低。我在验证集上做过统计只使用交叉熵时屋顶类别的IoU约0.65而背景类别IoU约0.92。问题很明确——类别不平衡。解法也不止一种我选的是Dice Loss Cross Entropy联合损失import torch.nn as nn class CombinedLoss(nn.Module): def __init__(self, weight_dice0.5, weight_ce0.5): super().__init__() self.ce nn.CrossEntropyLoss(ignore_index255) self.weight_dice weight_dice self.weight_ce weight_ce def forward(self, logits, targets): ce_loss self.ce(logits, targets) dice_loss self.dice_loss(logits, targets) return self.weight_ce * ce_loss self.weight_dice * dice_loss def dice_loss(self, logits, targets): probs torch.softmax(logits, dim1) # 对每个类别计算dice然后取平均 ...Dice Loss的特性是它直接优化Dice系数本身对前景类别被漏掉的情况惩罚很大和交叉熵组合后模型能同时兼顾整体像素分类准确和屋顶区域被完整找出。实测下来组合损失比单独交叉熵在屋顶类别IoU上提升了约0.09这个提升非常可观。3.3 训练超参数配置我的基准设置这里直接分享一套我在这个数据集上调出的、比较稳的训练配置。显卡是单张RTX 309024G如果你显存更小把batch size和输入尺寸按比例下调即可。超参数推荐值备注输入尺寸512x512随机裁剪兼顾显存和空间分辨率Batch Size16显存不足时降到8优化器AdamW权重衰减设1e-4初始学习率1e-4配合Cosine Annealing衰减训练轮数60个epoch在FPN上约4.5小时Warmup5个epoch避免初始阶段震荡数据增强RandomFlip RandomRotate90 ColorJitter详见下方关于数据增强一个容易踩的坑是别对遥感影像做随机的仿射变换。道路、屋顶都有明确的方向性你旋转30度、45度的胡乱变换会破坏地物的真实几何形态模型学到的东西会偏离真实场景。我实际用的是水平/垂直翻转、90度整数倍旋转、轻微的颜色抖动和随机裁剪这些操作不会破坏类别的几何合理性又能有效增加数据多样性。3.4 类别不平衡除了损失函数还能做什么除了换损失函数我在数据侧也做了两手准备。一是类别加权采样在写PyTorch的Dataset类时计算每张图中屋顶像素的占比按占比的倒数作为采样权重让那些屋顶密集的样本更频繁地被抽到。这个做法的效果很好——让模型多看屋顶多的样本它就能更精准地学习屋顶内部的可变性。二是过采样补充模块把屋顶占比超过30%的图单独提炼到一个子目录在训练循环里隔几个epoch强制插入这些样本。说白了就是给模型开小灶。我的观察是这两招配合Dice Loss屋顶类别IoU还能再涨2到3个百分点。4. 训练过程中的坑与调试记录4.1 显存溢出与输入尺寸的取舍第一次在RTX 3090上跑DeepLabV3输入尺寸1024x1024batch size直接设4结果一个step都还没跑完就OOM了。我当时以为显存不够就是显存不够后来才发现是特征图的尺寸太大上采样和ASPP模块占用极高。排查之后我把输入尺寸改成512x512batch size设16可以顺利训练。但这里就有一个矛盾输入尺寸越小小目标越容易消失。512x512的图中一栋小屋顶可能只有十几个像素模型很难学会。为了解决这个问题我采用了二阶段训练策略第一阶段用512x512输入训练30个epoch让模型学会全局特征和大致位置。第二阶段冻结Encoder用768x768输入、小学习率再微调15个epoch让Decoder精细抠边界。这样既控制了显存又让模型见过高分辨率的细节。最终在屋顶类别IoU上二阶段比直接单阶段512训练高约3个百分点。4.2 小屋顶目标漏检最典型的问题我在第一版模型跑完后专门挑了几张充满密集小型自建房的影像做可视化结果发现中等以上面积的屋顶分割效果很不错但那些10像素以内的屋顶几乎全部漏检。这个问题的本质是语义分割模型的感受野和特征金字塔的匹配问题。小目标在深层特征图里已经消失全靠浅层高分辨率特征来识别。解决办法我用的是给FPN增加一个更浅的P2层让模型在更高分辨率的特征图上做预测。在segmentation-models-pytorch里直接改decoder的in_channels不太方便我干脆换了一个思路在模型输出后加一个反卷积引导的上采样分支把最终logits的分辨率从原图的1/4恢复到1/2再和浅层特征做一次相加融合。改造之后小屋顶的召回率有明显提升代价是推理速度慢了不到10%完全能接受。4.3 标签噪声与模型学习上限即使数据集再精心处理标注中也难免有噪声——有的屋顶边缘少标了一两圈像素有的阴影被错误标成背景。如果你发现训练到后期loss不降、验证集指标波动较大很多时候不是模型问题而是标签噪声到了模型能拟合的极限。我在这个项目中尝试过一种缓解方法标签平滑Label Smoothing。把one-hot标签从[0, 1]改成[0.05, 0.95]附近相当于告诉模型不要对标签过分自信对抵御标注噪声有明显的正则作用。具体实现上用PyTorch的CrossEntropyLoss(label_smoothing0.1)就能完成一行代码的事效果实实在在。4.4 过拟合从验证集mIoU上发现的信号6000张图不算少但对模型而言如果训练到50个epoch的时候训练集mIoU已经逼近0.91验证集却停在0.78说明开始过拟合了。这时候常规操作是Early Stopping和更强的数据增强。我实际用的是多尺度测试Multi-scale Inference推理时把输入图缩放到[0.75, 1.0, 1.25]三个尺度分别分割后再加权平均。这个操作直接把验证集mIoU推高了约0.02代价是推理时间翻了三倍——不过对于离线数据分析场景这个代价很值得。5. 从训练到落地推理优化与应用场景扩展5.1 导成ONNX并部署到CPU服务器很多做遥感分析的团队最终跑模型的机子未必有GPU。我在这个项目上尝试过把PyTorch模型导出为ONNX再用onnxruntime做CPU推理。关键点有两个动态输入尺寸遥感影像大小不一导出时要让输入维度支持动态变化否则每张图都要先resize到固定尺寸之前分割出来的边界细节会损失。导出时用dynamic_axes{input: {0: batch, 2: height, 3: width}}就可以。用半精度还是单精度CPU上用单精度float32更稳用float16虽然理论上更快但很多CPU的推理内核支持并不好实测反而更慢。做完整合后一张1024x1024的遥感图在单核CPU上推理大约需要2秒左右作为批处理任务完全够用。5.2 输出后处理让mask变成可用的GIS矢量分割模型输出是像素级的mask图但实际业务里客户往往要的是矢量。我的做法是用OpenCV的轮廓提取和Douglas-Peucker简化import cv2 import numpy as np roof_mask (output[1] 0.5).astype(np.uint8) # 屋顶类别的二值mask contours, _ cv2.findContours( roof_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE ) for cnt in contours: approx cv2.approxPolyDP(cnt, epsilon2.0, closedTrue) # approx 就是多边形的顶点坐标这个步骤有三个容易忽略的细节先做一次形态学开运算去掉小噪点、轮廓简化时epsilon别太大否则屋顶直角会变成钝角、输出坐标系需要和原影像的投影一致。做完这些mask就直接变成能导入CAD或GIS的polygon了。5.3 从屋顶分割扩展到建筑屋顶细分类语义分割只是起点我现在的方向是在这个数据集的基础上加上更细分的屋顶材质类别——彩钢瓦、混凝土平顶、瓦片坡顶、玻璃顶等。方法是在现有分割模型输出的屋顶区域上再训练一个分类头做材质识别。这么做的好处是不用重新标注大量数据只需要给已有的屋顶mask加一个属性标签就能把单一分割任务变成分割分类的复合任务业务价值直接翻倍。5.4 数据集的迭代方向我还在持续扩充数据集方向包括增加更多传感器类型高分多光谱、无人机可见光的影像提升跨传感器泛化能力。补充不同季节的数据屋顶在积雪、落叶、强光照等条件下的表现差异很大。引入少量带不规则屋顶形状的样本这类在传统民居和现代异形建筑中很常见对模型边界能力考验更大。如果你手头有自己采集的遥感数据也建议按我上面说的格式整理然后用这个数据集的mask做预训练再在你的小数据上微调效果通常比裸用ImageNet预训练强得多。我实测过迁移到另一个城市的数据mIoU提升了大概5个百分点。最后说一句语义分割数据集的价值不在于有多少张图而在于标注质量、类别体系、处理方式是否能让模型全力发挥。我这次整理的数据集和整套训练流程已经在我自己的好几个项目里跑通了你拿过去用的时候建议先从模型选型部分开始看用FPN ResNet50 组合损失这个组合跑一个基线再根据你的实际场景逐步调优。本文还有配套的精品资源点击获取