☰
遥感语义分割实战:PaddleSeg训练到ONNX部署全流程
2026/10/21 12:53:24 网站建设 项目流程

遥感语义分割这个方向,我前后做过好几个项目,从地物分类到建筑物轮廓提取都碰过。训练模型本身不难,难的是把PaddleSeg训练出来的模型转成ONNX再部署到实际环境里——这一步才是真正劝退不少人的地方:训练能出图,换个环境就各种报错,算子不兼容、尺寸对不上、精度掉得离谱。这次我就围绕“PaddleSeg训练遥感数据集并转换为ONNX部署”这条完整链路,把从数据准备到模型落地的过程、踩过的坑、调过的参数、以及最后部署时的一些关键细节,一并整理出来。内容更适合有遥感数据、准备用PaddleSeg做分割训练、并且有部署需求的开发者参考,小白也能按步骤走通。

1. 项目背景与整体思路

1.1 遥感分割任务到底在解决什么问题

遥感语义分割,本质上是对航空影像或卫星影像做逐像素分类。一张高分影像里,每个像素要么是建筑,要么是道路,要么是水体、农田、植被、裸地……模型要学的就是“看图说话”:把影像内容按语义标签分割成不同区域。这个任务跟自然图像的语义分割还不完全一样,遥感影像有几个很突出的特点:

  • 尺寸大:单景影像经常是几万乘几万的像素,直接塞进网络基本不可能,必须裁切成块。
  • 目标尺度差异大:大片农田和水体是超大目标,而建筑物、道路是小目标,模型需要同时兼顾。
  • 标注成本高:逐像素标注非常耗时,很多公开数据集的类别体系、标注风格也各不相同,处理起来很费劲。
  • 背景占比高:在大部分遥感场景里,“其他/背景”类占了绝大部分像素,直接训练容易导致模型被背景主导,目标类别的召回率惨不忍睹。

正因为这些特点,遥感分割项目不是简单地把图像分类套上去就行,而是要从数据预处理、类别权重、模型选择这些层面专门设计。

1.2 为什么选择PaddleSeg而不是自己造轮子

做语义分割,市面上能用的框架其实不少,比如MMSegmentation、各类PyTorch实现的分割仓库,还有PaddleSeg。我之所以在这几个项目里反复用PaddleSeg,核心原因有几个:

第一,它对部署链路友好。PaddleSeg训练完可以直接导出静态图模型,然后用paddle2onnx转成ONNX。这条链路是官方维护的,每一步都有工具支撑,不需要自己写一堆转换脚本。

第二,训练配置是声明式的。修改模型结构、数据增强、损失函数、学习率策略,基本都在一个yaml文件里调整,跑起来就是一条命令。相比自己手写训练循环,它能节省非常多的时间,而且不容易写错。

第三,预训练模型、backbone权重、常见分割网络一应俱全。遥感数据集普遍不大,直接从头训效果很差,用ImageNet上预训练的backbone做迁移学习,是所有可行方案里性价比最高的。

当然,如果你对训练逻辑的掌控要求特别高,或者必须用某个PyTorch生态里的结构,那PyTorch方案也完全可以。但如果你追求的是“快速验证、方便上线的完整闭环”,PaddleSeg这一条链路确实顺滑很多。

1.3 完整技术链路

整个项目的流程大概是这样的:

环节工具/方法产出物关键点
数据准备Python脚本裁剪后的影像与单通道标签标签映射、训练/验证集划分
训练PaddleSeg权重文件(pdparams)模型选型、数据增强、loss调整
导出tools/export.py静态图模型(pdmodel + pdiparams)确认输入尺寸和归一化方式
转ONNXpaddle2onnxonnx文件opset版本、动态维度
部署推理ONNX Runtime等预测结果滑窗拼接、预处理一致性

这条链路看起来不长,但每一步都有暗坑。下面我按顺序把各环节的关键操作和心得展开讲。

2. 环境准备与遥感数据集处理

2.1 环境安装与版本搭配

先说一下环境。我用的是PaddlePaddle GPU版本加PaddleSeg源码运行的方式,Python版本建议控制在3.8到3.10之间。安装命令大致是:

# 创建虚拟环境 conda create -n paddle python=3.9 conda activate paddle # 安装PaddlePaddle GPU版本(根据你的CUDA版本选择对应安装包) pip install paddlepaddle-gpu==2.5.2 -i https://mirror.baidu.com/pypi/simple # 下载PaddleSeg源码并安装依赖 git clone https://github.com/PaddlePaddle/PaddleSeg.git cd PaddleSeg pip install -r requirements.txt

这里有一个经验:版本不要盲目追新。PaddleSeg的代码更新比较频繁,如果你的PaddlePaddle版本太新或太旧,容易出现编译报错或某几个算子不匹配。我自己更习惯用一个相对稳定的组合,先把框架环境跑通做完一个项目,再考虑升级。

另外,遥感影像处理常用到GDAL和OpenCV。GDAL用于读遥感影像,OpenCV用于图像处理,在装PaddleSeg依赖之外记得一起装:

pip install opencv-python gdal

千万别忽略GDAL,对GeoTIFF这类带地理信息的影像,普通PIL库经常读不干净,数据预处理阶段就出问题。

2.2 遥感影像标签的格式处理

很多公开遥感数据集的标注不是单通道索引图,而是RGB彩色图。比如某开源数据集里,一个像素如果是(0, 0, 0)就代表背景,(255, 255, 255)代表建筑物,(0, 255, 0)代表植被。而PaddleSeg训练时要求标签是单通道的整数索引图,也就是说每个像素的取值直接就是类别编号,从0开始依次递增。

由于彩色标签映射到类别索引是多对一的映射关系,写代码时最忌讳用for循环逐像素判断,那样处理一张大图可能跑几分钟。正确做法是用numpy做批量映射:

import numpy as np import cv2 # 定义类别颜色映射,例如 # 0: 背景 1: 建筑 2: 道路 3: 水体 4: 植被 color_map = np.array([ [0, 0, 0], # 背景 [255, 255, 255], # 建筑 [255, 0, 0], # 道路 [0, 0, 255], # 水体 [0, 255, 0] # 植被 ], dtype=np.uint8) # 读取彩色标签 rgb_label = cv2.imread('label.png', cv2.IMREAD_COLOR) rgb_label = cv2.cvtColor(rgb_label, cv2.COLOR_BGR2RGB) # 初始化单通道类别图 h, w = rgb_label.shape[:2] class_label = np.zeros((h, w), dtype=np.uint8) # 逐个类别批量处理 for idx, color in enumerate(color_map): mask = np.all(rgb_label == color, axis=-1) class_label[mask] = idx

处理完之后,用PaddleSeg读取时才不会因为标签通道多而报错,训练loss也能正常收敛。

注意:如果数据集里存在没有覆盖到的颜色值,这些像素会被归为0类,也就是背景。处理前最好统计一遍标签的颜色直方图,确认所有颜色都映射正确,否则背景类别里混着未知对象,模型精度会莫名其妙下降。

2.3 影像裁剪与数据集目录组织

原始遥感影像动辄一两万像素宽,不可能整图输入。常规做法是滑动窗口裁剪,分成512×512或1024×1024的小块。裁剪时有几个细节直接影响训练效果:

  • 裁剪窗口要和标签完全对齐。最简单的方式是先把影像和标签一起读进来,再用同一组裁窗坐标切割,避免对影像和标签分别计算导致错位。
  • 边角区域如果不足一个窗口,可以选择丢弃,也可以做padding。我建议直接丢弃,因为遥感影像边角通常是黑边或无意义区域,保留会让模型学到无用的噪声。
  • 重叠裁剪能增加样本数量,但注意别把完全重复的块同时放进训练集和验证集,否则验证指标会虚高。

裁剪完成后,按这个目录结构组织数据:

data/rs ├── images │ ├── train_001.jpg │ ├── train_002.jpg │ └── ... ├── labels │ ├── train_001.png │ ├── train_002.png │ └── ... ├── train.txt ├── val.txt └── test.txt

train.txt和val.txt的每一行是“影像路径 标签路径”,路径通常相对于项目根目录写相对路径。这个txt就是PaddleSeg数据集的入口,后面训练时直接指定。

我没把遮挡、变形的复杂增强放这里做,因为在训练配置里PaddleSeg会提供更丰富的在线增强方式,离线阶段只需要把数据准备干净即可。

2.4 类别不平衡的处理思路

遥感数据集的类别分布极不均衡,背景可能占了90%以上,而水体、道路、建筑物这些关键类别只占很小比例。如果直接拿原始分割来做,mIoU可能看起来有80%,但仔细观察会发现模型把所有像素都预测成背景,召回率惨不忍睹。

我处理类别不平衡有三个手段:

  • 裁剪时过滤纯背景块。如果一个块里目标类别像素占比低于某个阈值(比如5%),就不把它写入训练列表。这能保证模型每轮迭代都见到有效信息。
  • 损失函数里加Dice Loss或Lovasz Loss。这类损失天然关心类别区域的交集,对小类和边界区域友好。用交叉熵加Dice的混合损失,是遥感分割里很稳的组合。
  • 在yaml配置里给每个类别设置loss权重。如果道路特别重要,就把道路类的权重调高一点,让模型更关注它。

这几个手段不是互斥的,实际项目里我经常三个一起上。

3. 模型选择与训练配置实操

3.1 不同分割模型的适配场景

PaddleSeg里能选的模型不少,但遥感场景下没必要什么都试,重点选几类就好:

模型特点遥感场景推荐度
U-Net结构简单、对小样本友好数据量少时的首选
DeepLabV3+空洞卷积捕获多尺度上下文,精度均衡一般推荐的默认选择
HRNet保持高分辨率特征,边界细节好对边缘轮廓要求高时用
OCRNet目标上下文关系建模强目标结构复杂、粘连多时用
SegFormerTransformer结构,效率与精度平衡数据量大、需要更强能力时用

我在一个农田地块分割项目里,前期用DeepLabV3+效果总体稳定,后期换成OCRNet后边界精度提升了不少。如果你想最快跑通,从DeepLabV3+开始不会错。它训练相对稳定,对显存要求也不算最苛刻,是很多分割任务的默认好选择。

3.2 训练配置的改写要点

PaddleSeg训练的核心是yaml配置文件。我基于DeepLabV3+建了一个配置,关键部分长这样:

batch_size: 8 iters: 80000 train_dataset: type: Dataset dataset_root: data/rs train_list: data/rs/train.txt num_classes: 5 transforms: - type: ResizeStepScaling min_scale_factor: 0.5 max_scale_factor: 2.0 scale_step_size: 0.25 - type: RandomHorizontalFlip - type: RandomVerticalFlip - type: Normalize mean: [0.485, 0.456, 0.406] std: [0.229, 0.224, 0.225] mode: train val_dataset: type: Dataset dataset_root: data/rs val_list: data/rs/val.txt num_classes: 5 transforms: - type: Normalize mean: [0.485, 0.456, 0.406] std: [0.229, 0.224, 0.225] mode: val optimizer: type: sgd lr: 0.01 momentum: 0.9 weight_decay: 3.5e-4 lr_scheduler: type: PolynomialDecay learning_rate: 0.01 end_lr: 0 power: 0.9 loss: types: - type: CrossEntropyLoss - type: DiceLoss coef: [1, 1]

几个关键点解释一下:

  • 学习率与batch_size:训练分割模型,初始学习率一般按batch大小线性缩放。PaddleSeg默认0.01对应batch_size等于16左右,如果你改成8,可以把学习率先设为0.007或0.005,训练更稳定。
  • PolynomialDecay:这是语义分割里最常用的学习率策略,训练后期学习率衰减到接近0,模型能在后半程精调参数。用固定学习率或者step降低,后期容易震荡。
  • 损失函数:交叉熵加Dice的组合是遥感分类不平衡下的稳妥方案。如果计算资源紧张,也可以先用单独交叉熵跑通,再加Dice。
  • 归一化参数:我的例子用的是ImageNet的mean和std。如果你的遥感影像是多光谱数据,通道数和通道含义完全不一样,就一定要重新统计数据集自己的均值和方差,不能照抄。

3.3 训练过程监控与调优

训练过程中我会盯几个核心指标:loss、mIoU、Kappa、F1。PaddleSeg训练的时候会在日志里输出这些值,比如:

INFO: Step 1000/80000, loss=0.3256, lr=0.0082, time=1.82s INFO: Step 1000, mIoU=0.6542

如果你的loss长期不下降,优先检查标签图和影像图有没有对齐、类别映射有没有写错,这比调模型结构更常见。如果你的模型验证mIoU很高但预测图很粗糙,通常说明大类别主导了学习,要检查小类别的单独IoU,必要时增加损失权重。

显存溢出的问题也很常见。遥感裁剪块如果设成1024×1024,batch_size又设成8,普通16G显存很容易爆掉。我的处理方式优先级是:先减小batch_size,再减小crop_size。优先保crop_size的原因很简单,如果裁块太小,大目标被切得太碎,模型学不到完整的上下文,精度会掉得厉害。

另外一定要单独分出一部分数据做验证集。我习惯把训练和验证数据严格按空间区域划分,避免同一块地物同时出现在两边。

4. ONNX转换与部署落地

4.1 为什么要导出ONNX

训练完的PaddleSeg模型还是paddle格式,而实际部署环境往往不装PaddlePaddle,要找ONNX作为中间的交互格式。ONNX是一种跨框架的模型交换格式,几乎所有的推理框架都支持它,包括ONNX Runtime、TensorRT、OpenVINO,以及各种边缘设备工具链。

我之前的项目里,内网服务器上只允许装ONNX Runtime,根本不能装PaddlePaddle全家桶。把模型导出为ONNX之后,推理端的全部依赖就是一个onnxruntime库,彻底和训练框架解耦,部署成本大幅下降。如果你后续要上TensorRT加速,ONNX也是跳板,直接在TensorRT里解析ONNX然后优化就行。

4.2 用工具导出静态图模型

PaddleSeg直接训练出来的pdparams是动态图权重,不能直接转ONNX,必须先导出静态图推理模型。做法是用PaddleSeg自带的导出脚本:

python tools/export.py \ --config configs/deeplabv3p_resnet50_rs.yaml \ --model_path output/best_model/model.pdparams \ --save_dir output/infer_model

导出成功后会得到两个文件:model.pdmodel是静态图结构,model.pdiparams是参数文件。这两个文件就是Paddle分割模型的标准产物。导出时有一个信息很关键:如果你之前训练用了自定义归一化参数,导出模型结构里会带上预处理相关的算子,但保持输入是[N, C, H, W]。导出工具的默认输入尺寸通常是[1, 3, 1024, 1024],如果你的部署环境和这个尺寸不同,后面再处理。

4.3 paddle2onnx转换命令与参数避坑

转ONNX的官方工具是paddle2onnx,安装一步到位:

pip install paddle2onnx

然后用命令行转换:

paddle2onnx \ --model_dir output/infer_model \ --model_filename model.pdmodel \ --params_filename model.pdiparams \ --save_file output/model.onnx \ --opset_version 11 \ --enable_onnx_checker True

这个命令默认输出的ONNX输入尺寸是固定形状。如果你需要动态height和width维度,可以在paddle2onnx里配置输入shape的字典参数,或者转换后用工具把固定维度改成-1。

注意:opset版本不要盲目选最新的。ONNX Runtime和TensorRT对opset的支持有滞后,如果选了一个太新的opset,导出时可能没问题,但部署时某些算子不支持,反而增加排查成本。我个人习惯先用opset 11,等部署端确认支持再考虑升级。如果paddle2onnx报算子不支持,通常是当前paddle2onnx版本太老,优先升级paddle2onnx而不是改模型。

4.4 导出后的精度验证

转完ONNX之后,必须在同一批图像上验证精度有没有掉。有些模型转换后因为算子在推理框架和训练框架的实现不同,输出会有一点点偏差,但如果偏差过大,部署肯定不能直接用。

我常用的验证方法是:用原始Paddle模型和ONNX模型分别预测同一批验证集,比较预测结果的mIoU和逐像素diff。

import onnxruntime as ort import numpy as np import cv2 # 创建ONNX Runtime会话 sess = ort.InferenceSession('output/model.onnx', providers=['CPUExecutionProvider']) input_name = sess.get_inputs()[0].name # 预处理,这里必须和训练时保持一致 img = cv2.imread('test_image.jpg') img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img = cv2.resize(img, (1024, 1024), interpolation=cv2.INTER_LINEAR) img = img.astype(np.float32) / 255.0 img = (img - np.array([0.485, 0.456, 0.406])) / np.array([0.229, 0.224, 0.225]) img = img.transpose(2, 0, 1) # HWC -> CHW img = np.expand_dims(img, axis=0).astype(np.float32) # 推理 outputs = sess.run(None, {input_name: img})[0] # [1, num_classes, H, W] result = np.argmax(outputs[0], axis=0) # [H, W]

如果两个模型在验证集上的mIoU差值在0.5%以内,就可以认为导出没问题。我遇到过一次转换后精度掉了3个点的情况,原因是用ImageNet的归一化参数推理时忘了除以255,直接导致预处理错位。这种问题不是模型结构坏了,而是输入侧不一致,排查时先看预处理。

4.5 推理工程要点:滑窗与拼接

遥感大图在部署时要切成块推理,再拼回原图。如果不做重叠,拼接缝的位置会非常突兀,尤其是道路、建筑这类细长目标,直接断成两截。我的方案是滑窗加重叠推理:

  • 设crop_size为1024,stride取512,也就是窗口重叠50%。
  • 重叠区域的类别结果不能简单用最后一次推理覆盖,最好在每个像素位置上记录每个类别的累计得分,推理完成后取最大得分对应的类别。
  • 如果内存紧张,也可以重叠区域只算边缘权重,给靠近窗口中心的像素更高权重,边缘像素权重降低。

这个滑窗逻辑用ONNX Runtime推理一遍,速度和显存占用都可控。如果想让部署更快,可以上TensorRT获得更好的加速效果。导出成ONNX后再转TensorRT,在服务器上效果常常提升一倍左右。

5. 踩坑记录与排查手册

5.1 环境相关最常出问题的地方

PaddlePaddle版本和CUDA版本不匹配是最常见的问题。装完paddlepaddle-gpu后,一定要先跑一段小例子确认能用GPU而不是张冠李戴用了CPU。如果paddle2onnx转ONNX时报算子不支持,优先升级paddle2onnx和PaddleSeg到这个组合的兼容版本。

5.2 数据相关最容易忽略的细节

标签格式转换是重灾区,RGB标签不转单通道直接训练,loss基本上发疯。另外一个隐蔽问题是训练目录里图片和标签数量不一致,PaddleSeg读取时会报错或随机错位。我写了一个脚本扫了一遍所有训练列表里的路径是否真实存在、图片和标签大小是否一致,提前拦下了不少问题。

5.3 训练调优的关键提醒

只盯着mIoU容易误判。遥感数据集类别极度不平衡时,mIoU高不代表小类结果好。训练时不仅看mIoU,还应该看每类的IoU。如果某个关键类的IoU比其他类差很多,果断调loss权重、加数据增强或收集更多该类样本。

5.4 ONNX部署的相关问题速查

现象可能原因解决办法
推理结果整体错乱归一化方式和训练不一致核对mean/std和resize方式
输出尺寸与输入不对应模型导出时固定了尺寸导出时设置动态H/W或推理前resize
TensorRT加载失败opset版本过高重新导出较低opset的ONNX
拼接处出现明显断裂滑窗没有重叠改为重叠滑窗并做概率投票
精度比训练低1%以内正常转换误差可接受,如果太大则检查算子替换影响

5.5 一些个人体会

这条链路跑完,最大的感触是:PaddleSeg训练到导出这段其实很顺,真正花时间的地方全在数据处理和部署端的细节上。

遥感影像的标签映射要仔细核对,训练配置的mean和std一定要跟推理时保持一致,滑窗推理的重叠策略能够直接影响最终出图的观感。如果你准备做一个遥感分割项目,我建议一开始就把“训练、导出、部署”当成一条链路来设计,而不是先训练完再考虑部署。比如导出时动态输入尺寸、推理时统一预处理方式,这些在前面数据准备阶段就可以预留好接口。另一个小技巧是:验证转换结果时,别只看mIoU,直接抽出几张预测图叠加到原图上人工看一眼,拼接缝、边缘断裂这种问题,视觉检查比指标更直接。这种检查花不了多少时间,但能省掉后面调试部署的一大堆麻烦。

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

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

立即咨询