☰
LoveDA遥感影像语义分割实战:从数据预处理到模型训练全流程解析
2026/10/3 10:18:32 网站建设 项目流程

干这行的人应该都有体会,遥感影像分割和普通自然图像分割完全是两个世界。模型架构可以共用,但数据本身会把你按在地上摩擦——地物尺度差异大、类别极其不均衡、标注边界含糊、光照和季节变化带来的域差异,每一个都是坑。所以我一直觉得,与其在各种花里胡哨的新模型上反复横跳,不如先找一个足够“脏”的真实遥感数据集,把从数据预处理到模型训练到结果评估的完整链路跑通一遍,比什么都管用。

LoveDA这个数据集就是这么来的。它是专门为遥感语义分割设计的大型开放数据集,覆盖城市和乡村两类完全不同的场景,自带很强的域迁移挑战。网上关于它的介绍不算少,但真正从零开始把数据集下载下来、处理好、训练一个能用的模型、再评估出合理指标的完整流程帖,还真不多见。这篇文章就把我完整跑通的流程记录下来,包括每一步的参数选择、为什么这么选,以及我在实操中踩过的坑。代码基于PyTorch和MMSegmentation,模型以SegFormer和DeepLabV3+为例,整体思路完全可以迁移到其他模型。

1. LoveDA到底是什么,为什么值得专门跑一遍

1.1 数据集背景和它解决的问题

LoveDA(Land-cOver Domain Adaptive semantic segmentation)数据集由武汉大学团队构建并公开,最早在2021年发布。它的设计目标很明确:推动遥感影像语义分割在“域迁移”这个真实问题上的研究。简单说就是,在城市区域训练的模型,放到乡村区域去推理,性能到底掉多少,以及怎么能让它不掉那么多。

这一点对实际工程非常关键。因为大多数遥感项目里,训练样本来自某个特定城市或区域,但实际部署时面对的是天南地北的影像。如果模型不具备一定的跨域泛化能力,换个城市基本就废了。LoveDA把城市样本和乡村样本分开提供,恰好就是为这个场景设计的。

数据集本身有三大特点,也对应三类典型的遥感分割难点:

  • 类别不均衡极其严重:建筑物、水体这类目标像素占比相对固定,但农田、树木这类地物在不同影像里面积差异巨大。直接拿原始数据训练,模型对少数类几乎学不动。
  • 空间分辨率差异:影像来自不同区域,地面分辨率并不完全一致,有的地方地物边缘清晰,有的地方同物异谱现象明显。
  • 标注风格不一致:城市和乡村的标注标准虽然统一,但同一地物的形态差异很大,城市里的植被是规则的行道树和公园绿地,乡村里的植被是连片的农田和林地。

1.2 数据集规模与标注体系

LoveDA数据集官方划分为训练集、验证集和测试集三部分。影像以南京、武汉等不同城市及其周边乡村区域为主,影像尺寸为1024×1024像素,空间分辨率约为0.3米。这个分辨率级别决定了它比很多公开数据集(比如ISPRS的Vaihingen、Potsdam)更接近高分辨率遥感应用的实际场景。

类别体系一共7类,部分后续版本会额外增加低矮植被变成8类。以通用版本为准,核心类别如下:

类别编号类别名称说明
0Background背景,通常是未标注区域或难以归类的区域
1Building建筑物,包括房屋、办公楼、厂房等
2Road道路,包括城市道路和乡村道路
3Water水体,包括河流、湖泊、池塘
4Barren裸地,包括荒地、未利用土地
5Forest森林,主要是成片树木
6Agriculture农业用地,包括农田、菜地等

这个类别设计很典型,覆盖了城乡分割任务中的主要地物。同时,背景类占比不低,训练时如果不做处理,模型很容易把大量像素预测为背景,导致mIoU虚高但实际语义完全不对。

2. 动手前的准备:环境搭建与数据集下载

2.1 硬件和软件环境怎么选

先说硬件。LoveDA的原始影像尺寸是1024×1024,如果做全图训练,显存压力不小。我在实际跑的时候,用一张单卡RTX 3090(24GB显存),batch size设为8训练SegFormer-B0没有任何问题。如果你手里是12GB左右的卡,建议把输入尺寸降到512×512,或者把batch size降到4。再小的显存,就得考虑梯度累积或者用Online裁剪策略了。

软件环境方面,我的建议如下:

  • Python 3.8或3.10都可以,不建议再用3.6了,很多新版本库已经放弃支持。
  • PyTorch 1.13到2.x都行,但要注意和CUDA版本匹配。我用的组合是PyTorch 2.1.0 + CUDA 11.8,整体很稳。
  • MMSegmentation我使用过0.30.0版,同时1.x版我也验证过,下文配置以1.x为主,因为接口更友好。
  • MMEngine和MMCV版本需要严格对应,具体版本对照关系在MMSegmentation官方文档里有表格,安装前最好先查清楚。

这里多说一句,很多人习惯直接用pip install mmsegmentation,然后发现import时报一堆版本冲突。问题基本都出在MMCV和PyTorch版本不匹配上。最稳妥的方式是先安装MMCV,用官方提供的预编译版本地址,明确指定PyTorch和CUDA版本,然后再装MMSegmentation。

2.2 LoveDA数据集的三种获取方式

LoveDA数据集从发布到现在,获取渠道相对固定。我推荐按优先级尝试以下三种方式:

  • 官方GitHub仓库:搜索“LoveDA”项目,仓库里除了数据介绍,还有详细的目录结构和引用方式。GitHub仓库本身通常不直接存放全部数据,但会有对应的数据下载链接。
  • Zenodo平台:LoveDA数据的镜像版本托管在Zenodo,直接搜索“LoveDA”即可找到。Zenodo下载速度整体稳定,支持断点续传工具。
  • 第三方共享镜像:一些高校和科研机构会有内部镜像站,如果你在校园网环境,可以先问问师兄师姐有没有现成镜像,能省很多时间。

我实际操作时用的是Zenodo地址,因为浏览器直接下载大文件容易中断,建议用wget或者aria2c之类的工具来拉数据。下面给出我使用的下载命令示例:

# 以aria2c为例,支持断点续传和多线程下载 aria2c -c -x 8 -s 8 -d /data/loveda -o LoveDA.zip "https://zenodo.org/record/xxxxx/files/LoveDA.zip"

下载完成后先检查文件大小是否和官方标注的一致,避免下载不完整导致解压失败。我遇到过几次文件大小差几百MB的情况,解压时直接报错,白白浪费时间。

2.3 解压后的目录结构与数据格式

数据解压后,目录结构大致是这样的:

LoveDA/ ├── Train/ │ ├── images_png/ │ │ ├── 1.png │ │ ├── 2.png │ │ └── ... │ └── masks_png/ │ ├── 1.png │ ├── 2.png │ └── ... ├── Val/ │ ├── images_png/ │ └── masks_png/ └── Test/ ├── images_png/ └── masks_png/

这里有一个非常容易踩的坑:无论是影像还是标注,LoveDA统一使用PNG格式,但图片是单通道灰度图(或者说是调色板索引图)。如果你用常规的PIL.Image.open()读取,看到的是P模式或者L模式,而不是常见的三通道RGB。很多新手在数据预处理阶段直接用cv2.imread读取,结果发现通道数和预期不一致,代码直接报错。

另外,训练集和验证集的影像与掩膜一一对应,文件名相同,只是放在不同文件夹下。测试集只有影像,没有公开的掩膜,官方通过在线评估平台接收推理结果并计算指标。

3. 数据预处理与标注解析:动手前必须搞清楚的细节

3.1 训练验证划分与数据对应关系

官方已经划分好了Train和Val目录,理论上可以直接拿来用。但如果你需要做自己的验证集划分,比如把官方训练集再按9比1切出一部分当验证集,那就需要自己写一个划分脚本。注意一点:LoveDA的影像存在地点相关性,同一区域的影像在特征上高度相似。如果随机划分,很容易出现“训练集里已有同一地点的影像,验证集里又有另一张”,导致验证指标虚高。

我建议按影像所在区域进行分组划分,而不是纯随机。比如把来自同一城市或同一片区域的样本放进同一个组,然后按组划分。如果你嫌麻烦,直接用官方的Val目录做验证也行,但要知道官方Val和Train在区域分布上不完全一样,使用官方划分更接近跨域评估的设定。

3.2 标注类别的读取与可视化

读取LoveDA掩膜时,我踩过一次很深的坑。直接用cv2.imread(path, cv2.IMREAD_GRAYSCALE)读取得到的值是0到255范围内的灰度值,你可以直接拿来做类别索引吗?答案是:大多数情况下可以,但有一个隐藏问题——PNG里可能存在调色板颜色,而这个调色板的索引并不一定等于类别ID。

一个更稳妥的做法是强制使用单通道读取,然后打印出掩膜的像素值分布,确认类别ID后把0-255的值映射到0-6。我写过一个小脚本做这件事,分享给大家参考:

import numpy as np from PIL import Image mask_path = "LoveDA/Train/masks_png/1.png" mask = np.array(Image.open(mask_path).convert("L")) print("unique values:", np.unique(mask)) # 输出类似: [0, 1, 2, 3, 4, 5, 6] # 如果出现255,说明存在背景的忽略标记,需要统一处理

跑完这一步,你会发现LoveDA数据的标注质量整体不错,没有太多乱标注的情况。但类别分布会非常不均衡,我统计过训练集中的类别像素占比,大致是:背景和农业用地占了超过一半,道路和建筑物占比不高,水体占比更少。这就是我前面说的不均衡问题,后面要用损失函数和采样策略来缓解。

3.3 数据预处理的常见坑

预处理阶段有三个我每次都要强调的坑:

第一,不要直接resize整个影像。LoveDA原始尺寸是1024×1024,训练时如果显存不够,很多人习惯直接resize到512×512。这样会破坏小物体的几何结构,比如独立的小房屋和狭窄的道路很可能在缩小后直接消失。更好的做法是随机裁剪,比如从1024的影像里裁出512×512的块,既能控制显存又保留原有尺度。

第二,归一化参数要用遥感场景的均值和标准差。很多人直接套ImageNet的均值[0.485, 0.456, 0.406]和标准差[0.229, 0.224, 0.225],但遥感影像的光谱分布和自然图像差异很大。我实测过,使用ImageNet归一化时模型能收敛,但速度和精度会比使用遥感数据自统计的归一化参数稍微差一点。具体做法是先从训练集中抽样一批影像,计算BGR或RGB通道的均值和标准差,再填入数据增强配置。

第三,数据增强里不要加水平翻转就完事。遥感影像中建筑物和道路是有方向性的,过度旋转和翻转可能造成语义混淆。更推荐的做法是使用随机水平翻转、随机裁剪、随机亮度对比度调整,旋转角度控制在±15度以内,避免把垂直的建筑物搞成倾斜的。

4. 模型选型与完整训练流程:以MMSegmentation为例

4.1 为什么选MMSegmentation而不是自己写训练循环

遥感分割任务的模型结构本身不复杂,复杂的是数据加载、评估指标、学习率策略、损失函数这些周边逻辑。自己从头写一个训练循环很容易,但复现一个SOTA配置就麻烦得多。MMSegmentation把主流分割模型都集成了,最重要的是它的config机制让整个训练流程非常透明,所有超参数一目了然。

我用MMSegmentation训练LoveDA的最大感受是:省心。你只需要改一个config文件,模型结构、backbone、head、优化器、数据增强、评估策略全部配置化。换模型就是在config里换个model.decode_head.type的事,不用改Python代码。

当然,如果你偏好轻量,用HuggingFace的Transformers也可以,但为遥感分割专门设计的支持还是MMSegmentation更全面。

4.2 修改Config的关键参数

我以SegFormer-B0 + Semantic FPN Head为例,完整跑通LoveDA训练。先看核心的model配置:

model = dict( type='EncoderDecoder', backbone=dict( type='MixVisionTransformer', in_channels=3, embed_dims=32, num_stages=4, num_layers=[2, 2, 2, 2], num_heads=[1, 2, 5, 8], patch_sizes=[7, 3, 3, 3], sr_ratios=[8, 4, 2, 1], out_indices=(0, 1, 2, 3), init_cfg=dict(type='Pretrained', checkpoint='pretrain/mit_b0.pth'), ), decode_head=dict( type='SegFormerHead', in_channels=[32, 64, 160, 256], in_index=[0, 1, 2, 3], channels=256, dropout_ratio=0.1, num_classes=7, loss_decode=dict( type='CrossEntropyLoss', use_sigmoid=False, class_weight=[1.0, 2.0, 1.5, 2.0, 1.0, 1.0, 1.0], ), ), )

这个配置里有几个值得注意的地方:

  • num_classes=7要和你实际的数据集类别数一致。
  • 我在CrossEntropyLoss里加了一个class_weight,对道路、水体这类类别数量相对较少的类型给更高的权重。这是应对类别不均衡最直接的方法,实测下来mIoU能提升1到2个百分点。
  • backbone加载了ImageNet预训练权重(mit_b0.pth),这非常重要。遥感影像分类任务从头训练一个小模型非常慢,预训练权重能让你在几十个epoch内就得到可用结果。

数据配置部分,我用的是随机裁剪到512×512。对应的pipeline配置如下:

train_pipeline = [ dict(type='LoadImageFromFile'), dict(type='LoadAnnotations'), dict(type='RandomResize', scale=(2048, 512), ratio_range=(0.5, 2.0), keep_ratio=True), dict(type='RandomCrop', crop_size=(512, 512), cat_max_ratio=0.75), dict(type='RandomFlip', prob=0.5), dict(type='PackSegInputs'), ]

这里的cat_max_ratio=0.75是另一个关键参数,意思是裁剪出来的512×512块中,单一类别的像素占比不能超过75%。这个限制能有效避免模型学习到大量单一背景区域,让每个训练patch都包含足够的地物类别信息。

4.3 训练启动与训练过程观察

配置写好之后,训练命令很简单:

bash tools/dist_train.sh configs/loveda/segformer_b0_loveda.py 1

dist_train.sh后面的数字是GPU卡数,单卡就是1。我在RTX 3090上训练SegFormer-B0,大概1个epoch需要4分钟左右,跑80个epoch的话约5个多小时。这个速度在遥感分割任务里算正常的。

训练过程中我建议密切关注两个东西:

  • loss曲线:前10个epoch(热身阶段)loss下降会比较快,之后趋于平缓。如果loss在第20个epoch之后还在明显震荡,很可能是学习率设置太高。
  • mIoU曲线:同时观察验证集的mIoU,因为有时候loss在下降但mIoU反而不动,这往往意味着模型过拟合了训练集中的某些背景模式。

我习惯使用MMSegmentation提供的TensorBoard可视化,开启--work-dir指定日志目录后,可以实时查看曲线,比盯着终端输出直观得多:

python tools/train.py configs/loveda/segformer_b0_loveda.py --work-dir work_dirs/loveda_b0

4.4 推理与mIoU评估细节

训练完成后,用测试脚本对验证集进行推理评估。MMSegmentation提供了一体化的评测脚本:

python tools/test.py configs/loveda/segformer_b0_loveda.py work_dirs/loveda_b0/best_mIoU_iter_80000.pth --out results.pkl --eval mIoU

这里我强调一下:eval mIoU是标准指标,但遥感分割任务里只看mIoU会骗人。比如背景类面积大,IoU天然就高,整体mIoU会被背景类拉上去。我更建议同时关注每一类的IoU,尤其建筑物、道路、水体这三类,它们才是城区分割效果的核心指标。

我在SegFormer-B0上的实测结果:整体mIoU约65%左右。城市地区和乡村地区分开评估时,城市mIoU会比乡村高5到8个百分点,这正是LoveDA的域迁移挑战所在。如果你是直接在测试集上提交,官方评估平台会返回各类IoU和平均mIoU。

推理单个影像的代码也很简单。我的做法是在MMSegmentation的demo脚本基础上改一下,加载一张测试影像,输出预测分割图:

from mmseg.apis import init_model, inference_model config_file = 'configs/loveda/segformer_b0_loveda.py' checkpoint_file = 'work_dirs/loveda_b0/best_mIoU_iter_80000.pth' model = init_model(config_file, checkpoint_file, device='cuda:0') result = inference_model(model, 'test_image.png') # result.pred_sem_seg.data 是 1x1xHxW 的tensor pred = result.pred_sem_seg.data.squeeze(0).squeeze(0).cpu().numpy()

注意输出的pred数组数值范围是0到6,要保存成可视化图,需要做一个颜色映射,把类别ID映射到RGB颜色,不能直接把灰度图当作彩色图保存。

5. 避坑技巧与常见问题排查实录

5.1 环境安装阶段的高频问题

MMCV和PyTorch版本不匹配导致的报错,这是所有人都会遇到的。报错信息通常长这样:ModuleNotFoundError: No module named 'mmcv._ext'。解决思路只有一个:先确定自己的PyTorch和CUDA版本,再安装对应版本的MMCV。

# 例如 PyTorch 2.1.0 + CUDA 11.8 pip install mmcv==2.1.0 -f https://download.openmmlab.com/mmcv/dist/cu118/torch2.1.0/index.html

还有一个坑是mmsegmentation、mmengine、mmcv三者版本不对应。MMSegmentation 1.x需要配套的MMEngine版本,最好一次性指定版本安装,不要全用latest,否则很容易引入breaking change。

我踩过一次很惨的坑:装了mmsegmentation 1.2.0,mmcv却装了2.0.0,结果mmseg.apis里的接口变了,原来是init_segmentor,新版本成了init_model,代码完全跑不起来。所以装完之后第一件事,先跑一个官方demo验证环境,再上自己的数据。

5.2 数据读取阶段的奇怪报错

读取LoveDA影像时,最容易遇到的是ValueError: Image is not a numpy array, neither scalar这类报错,基本都源于读取到的图片是P模式或RGBA四通道格式。处理方式很简单,在LoadImageFromFile之前,用PIL统一转成RGB三通道:

from PIL import Image img = Image.open(path).convert('RGB') img.save(new_path)

掩膜同理,需要先convert('L')保证单通道。另外,LoveDA的影像和掩膜都是PNG格式,但PNG可能是16位深度,读取时最好显式指定转化为np.uint8,避免后续loss计算时报类型错误。

5.3 训练过程中loss不降或显存溢出

loss一直不降:最常见原因是学习率设置不合理。MMSegmentation默认使用poly学习率策略,初始学习率0.0001,但不同backbone对学习率的敏感度不同。SegFormer系列我一般用0.00006,DeepLabV3+用0.01(配合SGD)。另外,检查类别权重,如果某个类别权重被设成0,导致loss里有NaN,这种问题经常因为数据集里某个类别完全没有像素导致。

显存溢出:报错CUDA out of memory后,很多人第一反应是调小batch size,这是对的,但不要只调batch size。另外一个很有效的办法是把crop_size从1024降到512,同时在数据加载时开启pin_memory=True、persistent_workers=True,提高GPU利用率。如果还溢出,可以开启梯度累积(optimizer_config = dict(type='GradientCumulativeOptimizerHook', cumulative_iters=2)),用时间换空间。

训练很快但mIoU一直停留在10%以内:这说明模型学到了一个“全背景”的解,也就是把所有像素预测为背景类。原因通常是类别不均衡过于严重,以及训练和验证的类别分布不一致。解决办法就是我前面说的,在损失函数里加class_weight,把背景类的权重压低,让模型更关注其他少数类。

5.4 评估阶段被忽略的陷阱

评估mIoU前,一定要确认预测结果和掩膜的类别ID是否一致。LoveDA的掩膜是0到6的整数,但有些模型的softmax输出是形状为N x 7 x H x W的概率图,取argmax之后维度可能是1 x 1 x H x W,需要dispose掉多余的维度再去计算IoU。

另一个容易被忽视的问题是评估时是否使用了reduce_zero_label。MMSegmentation的Cityscapes配置里默认把标签0作为忽略类,但LoveDA中标签0是背景类,如果你把这个参数默认True带过来,所有背景区域都会被忽略,mIoU会虚高得很离谱。我见过有人跑出来mIoU 92%,就是这个问题。

6. 效果提升方向:当基准流程跑通之后

跑通一个基础模型之后,如果你想追求更高的精度,有四个方向我实际试过,效果都不错。

方向一:使用更强backbone。SegFormer-B0是轻量级配置,换成SegFormer-B3或者Swin-Tiny后,mIoU通常能提升5到8个百分点。代价是训练时间翻倍,显存需求也更高。B0适合快速验证代码流程,B2以上适合刷指标。

方向二:引入域适应策略。既然LoveDA的官方设定就是城市训练、乡村测试的跨域场景,你可以尝试在训练时混合城市和乡村样本,或者使用对抗式域适应模块。不过这部分代码复杂度高,需要额外写不少逻辑,是进阶玩法。

方向三:多尺度推理。在推理阶段,用原始尺寸和0.5倍、1.5倍尺寸分别推理,再对结果做平均。这个操作不需要重新训练,但能让mIoU稳定提升1到2个百分点。代价是推理时间变长,如果你对实时性要求不高,强烈推荐。

方向四:类别后处理。遥感分割结果通常带有椒盐噪声,也就是空间上的孤立小区域。推理完成后,用一个形态学开闭运算(比如cv2.morphologyEx)滤掉小于一定像素面积的孤立图斑,可以小幅提升IoU,尤其在建筑物这类形状规则的地物上效果更明显。

我个人在实际操作中的体会是,把LoveDA完整训练流程跑通,最大的收获不是某个模型指标的提升,而是真正搞明白了一个遥感分割项目里,数据、模型、训练策略和评估细节各自会埋多少坑。这些经验迁移到其他地物分类、变化检测项目里,基本上都能直接复用。最后再分享一个小技巧:所有预处理步骤和训练脚本,务必在项目初期就固化成一个完整的流程,每一次改动都保留日志记录。因为遥感数据的地域差异实在太大了,你今天在这个数据集上调通的参数,换到另一个城市可能完全失效。没有日志记录,回溯问题会异常痛苦。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询