简介:一份面向固定翼无人机系统辨识与仿真研究的Matlab代码资源,特别适合电子信息工程、计算机、数学等专业学生用于课程设计、期末大作业与毕业设计,也适合需要快速理解UAV建模流程的初学者。压缩包共包含5个文件,其中2个M脚本为核心程序,配合PDF说明文档以及txt/md文本说明,分别用注释解释代码思路、参数含义与运行方式;整体rar包仅911KB,非常轻量。代码采用参数化编程,参数可方便修改,路径和模块注释清晰,并附赠案例数据,下载解压后即可直接运行,省去环境配置与调试时间。工程围绕UAV_sysID_project展开,覆盖固定翼无人机系统辨识项目的主要框架,有助于读者从数据准备、脚本执行到结果分析形成完整认知。已有144人浏览学习,适合希望借助成熟示例快速上手的用户。
1. 做遥感影像语义分割,先想清楚“逐像素”这三个字
语义分割在遥感影像里从来不是“把图分个类”那么简单。同样一张城市影像,目标检测给出几个框,语义分割要回答的是每一个像素到底是道路、建筑、植被还是水体。一个 2 万乘 2 万像素的正射影像,检测模型只需要输出几百个目标框,分割模型却要做四亿次分类判断。这个数量级差异决定了整个技术路线:不是选一个网络就能跑通,而是数据组织、模型结构、损失函数和推理策略都要围绕“逐像素”重新设计。
边界模糊是另一个绕不开的问题。建筑阴影投射到路面上,树冠遮挡了部分屋顶,云影扫过农田——这些地方连人工标注都会犹豫。模型在这种像素上做出的判断,边界必然是锯齿状或粘连的。所以遥感语义分割的工程难点从来不是“准确率再高一个点”,而是“边界更干净一点、小目标不漏掉、跨区域泛化不崩”。本文直接从数据预处理、模型选型、训练调参到评估部署串一遍,重点放在参数和踩坑上,新手能照着跑,熟手能对照检查自己的流程。
2. 数据预处理:遥感影像的分块、波段与标签对齐
2.1 滑动窗口切块:尺寸、重叠率与步长的配合
遥感影像不能整图直接进网络。显存装不下是表面原因,更深层的原因是分割模型对输入尺寸敏感,太大则下采样倍数不够导致感受野不足,太小则上下文信息丢失。常见做法是切成固定尺寸的 patch 送入训练,而 patch 的尺寸和步长决定了样本的有效性和数量。
我习惯用 512×512 作为默认 patch 尺寸。这个大小在 ResNet 系列编码器(下采样 32 倍)下,特征图是 16×16,既不会太小导致语义信息丢失,也不会因为过大降低 batch size。如果显卡显存只有 8GB,可以降到 448×448 或 384×384,但不要低于 320×320,否则道路、河流这类长条状地物的语义连续性会被截断。
步长的选择更讲究。步长等于 patch 尺寸时,相邻 patch 之间没有重叠,样本量最小,训练速度快,但物体正好被切在 patch 边缘时,模型永远看不到完整目标。比如一栋 60 米长的建筑,被 512 像素(假设地面分辨率 0.5 米,即 256 米)的窗口切分,大概率有建筑跨越两个 patch,每个 patch 里只有半栋楼,标注也只剩一半——模型在训练时会把“半栋楼”当成完整特征学进去。推理时会输出残缺的目标。
解决办法是重叠切块。训练时用 stride = patch_size // 2 的步长滑动,配合随机数据增强(随机翻转、随机旋转 90 度、随机色彩抖动),可以从一张标准影像中生成大量有效的训练样本。推理时同样用重叠窗口,但输出的重叠区域需要特殊处理,这个放到第 5 章讲。
提示:切块之前先做一次全局统计分析,记录每张影像的均值与标准差,后续归一化要用。
2.2 多波段输入:RGB 之外的通道到底用不用
大多数预训练模型(ImageNet 系列)的输入是 3 通道 RGB,直接套用到遥感影像上有两个明显问题。一是很多遥感影像是多波段的,至少包含 R、G、B、NIR 四个波段,甚至还有红边、短波红外、热红外。二是影像的数值范围与自然图像完全不同,可能已经是地表反射率产品而非原始 DN 值。所以输入通道设计是一个需要单独决策的工程问题,不是简单地“随便拼凑”。
我的做法是:先用 RGB 三通道跑通基线,再评估是否有必要添加额外波段。添加波段时,优先选 NDVI 和 NDWI 这类归一化指数而非原始波段。原因很简单:归一化指数是比值运算的结果,像素值范围稳定,受光照和大气影响小,模型的输入分布更平稳。NDVI 的计算公式:
import numpy as np def compute_ndvi(nir_band, red_band, epsilon=1e-10): """ 计算归一化植被指数 nir_band: 近红外波段,形状 HxW,float32 red_band: 红光波段,形状 HxW,float32 返回 NDVI,范围约在 -1 到 1 之间 """ ndvi = (nir_band.astype(np.float32) - red_band.astype(np.float32)) / ( nir_band.astype(np.float32) + red_band.astype(np.float32) + epsilon ) # 超过 99.5% 分位数视为异常值(通常由传感器噪声或云阴影导致) clip_max = np.percentile(ndvi, 99.5) clip_min = np.percentile(ndvi, 0.5) ndvi = np.clip(ndvi, clip_min, clip_max) return ndviNDVI 的数值本身被限制在 -1 到 1 之间,这一步就已经做了归一化;异常值裁剪是为了避免个别传感器坏点在训练时放大梯度。如果直接用 NIR、SWIR 等原始波段,需要各自做 min-max 或 z-score 归一化,否则数值范围差异会导致训练早期振荡。
通道拼接时还要考虑预训练权重的问题。ResNet 第一层卷积的权重是 3 通道预训练好的,换成 4 通道或 5 通道后,常规做法是复制 RGB 通道的均值权重到新通道上,把新增通道的权重初始化为极小的随机值(比如均值为 0、标准差 0.01 的正态分布)。这样前向传播初期,新通道对特征的影响是微弱的,不会破坏已有的高层语义特征,但又能让梯度正常回传到新通道上。
2.3 标签对齐:影像偏移与标注滞后的处理
遥感语义分割中,标签质量是决定 mIoU 上限的最大因素。网络结构再新颖,面对一个错位的标签也无能为力。影像可能存在几何校正误差,多时相影像之间存在配准偏差,标注人员手工勾画的建筑物边界与真实屋顶轮廓通常有几像素的偏移。对于精度要求高的任务(比如 0.3 米分辨率的建筑轮廓提取),这个偏移量足以造成显著的边缘误差。
针对标签噪声,我的处理流程是三级审核。第一步是路径归零检查:将标签二值化后与原始影像做半透明叠加,肉眼检查边界错位。第二步是面积分布统计:如果某一类在训练集里占 40%,在验证集里却占 5%,这个标签很可能存在系统性错标。第三步是训练后的预测一致性对比:用训练好的模型对训练集做预测,将预测结果与 ground truth 不一致的区域可视化。这个区域往往是标签噪声最集中的地方,人工抽查修正后重新训练,通常能带来 1~2 个点的 mIoU 提升。
提示:做标签清洗时不要直接修改原始标注文件,保留一份原始标签,另外生成一份 clean 版本。后续训练脚本通过配置文件选择使用哪个版本,避免反复备份和误操作。
3. 模型选型:从 FCN 到 DeepLabV3+,再到分割 Transformer
3.1 编码器-解码器架构:为什么遥感分割普遍不直接用 FCN
FCN(Fully Convolutional Network)是语义分割的开山之作,其核心贡献是把分类网络最后的全连接层替换为卷积层,使得网络可以接受任意大小的输入。但 FCN 有一个致命的弱点:下采样 32 倍后直接上采样回原尺寸,分割结果不仅粗糙,而且边缘位置依赖低层特征,细节丢失严重。在遥感影像这种纹理丰富、目标密集的场景里,FCN 的表现远不如预期。
于是编码器-解码器结构成为事实标准。编码器负责提取语义特征,通过逐步下采样增大感受野;解码器通过上采样和跳跃连接恢复空间细节。两者配合,兼顾了“是什么”和“在哪里”两个维度。遥感影像通常目标密集,同一场景中有大量相似的小物体(比如一排按同样式建的房子),编码器的语义特征能区分“屋顶”与“道路”,解码器的空间特征能区分“这个屋顶”与“那个屋顶”。
选型时首先要确定编码器,也就是 backbone。ResNet-50、ResNet-101、EfficientNet 是常见选择。ResNet-101 比 ResNet-50 更深的优势在遥感影像上能体现出来,因为影像中的物体尺度跨度极大——一棵树可能只有几十个像素,一片森林却覆盖整个画面,深层网络对尺度变化的适应性更强。但也要意识到编码器占了模型总参数量的绝大部分,ResNet-101 的模型体量在部署时可能是个负担。我个人的经验:先跑通 ResNet-50 找明显问题,再用 ResNet-101 验证上限提升,最后根据部署要求决定是否做轻量化。
3.2 DeepLabV3+ 与 UNet++:解码器设计的核心差异
DeepLabV3+ 和 UNet++ 是遥感分割实践中最常用的两个框架,它们在解码器设计上走了两条完全不同的路。
DeepLabV3+ 的核心组件是 ASPP(Atrous Spatial Pyramid Pooling)。ASPP 用不同的空洞率(dilation rate,常见是 6、12、18)对同一特征图做并行卷积,相当于在同一层上捕获多尺度上下文信息。空洞卷积在保持特征图分辨率的同时扩大感受野,这意味着编码器输出的特征图空间细节更好,解码器不需要做复杂的上采样融合。DeepLabV3+ 的解码器结构简洁,先对编码器输出做 4 倍上采样,再与编码器低层特征拼接,最后再做 4 倍上采样,没有复杂的密集连接。
UNet++ 的核心是嵌套的密集跳跃连接,每一层解码器都融合了不同层级的编码器特征。这样的设计对细节恢复更有利,但也有代价:模型参数量显著增大,训练时间更长。遥感影像中的目标边缘质量,在 UNet++ 体系下通常优于 DeepLabV3+,尤其是锯齿状边缘更少。
| 对比维度 | DeepLabV3+ | UNet++ |
|---|---|---|
| 多尺度建模能力 | 强,ASPP 显式建模 | 中,依赖多级特征融合 |
| 边缘细节恢复 | 中等 | 更好 |
| 训练速度 | 更快 | 较慢 |
| 显存占用 | 较低 | 较高 |
| 适用场景 | 道路/水体等大目标 | 建筑/车辆等中小目标 |
遥感影像分割如果以道路、水体、农田等大面积地物为主,DeepLabV3+ 的 ASPP 能更好地建模全局上下文;如果目标是建筑物、小型水体、独立树木等中小目标,UNet++ 的密集跳跃连接对边缘细节更有帮助。
3.3 引入注意力机制与分割 Transformer:什么时候值得换
遥感语义分割近年一个明显变化是分割 Transformer(如 SETR、SegFormer、Mask2Former)从论文走向工程实践。这类模型的核心是用自注意力机制替代卷积,在感受野方面天然优势明显:一个卷积核的局部感受野只有几像素到几十像素,而 Transformer 的注意力模块在高级特征层可以关联整张图。
但在遥感分割任务里,我观察到 Transformer 的优势是有条件的。当我处理的是高分辨率影像中密集小目标时,CNN 仍然能打;当我处理的是跨区域泛化、需要关联全局上下文的任务时,Transformer 的效果值得尝试。SegFormer 这类混合架构(分层 Transformer 编码器加轻量 MLP 解码器)在遥感场景下效果稳定,模型可解释性也不差。需要注意的是 Transformer 对训练数据量更敏感,在标注样本较少的情况下更容易欠拟合或过拟合。我的建议是:数据规模在几千张 patch 以内,先不换 Transformer;有几万张 patch 时再来对比。
4. 训练工程:优化器、学习率与不平衡损失
4.1 优化器与学习率:分割任务为什么和分类任务不一样
图像分类任务中,模型输出的是一个全局概率分布,梯度计算简单直接。语义分割任务中,模型的输出是一个 H×W×C 的张量,每个像素都贡献一份梯度。这意味着梯度的方差更大,更新方向波动更明显,对优化器的稳定性要求更高。
Adam 是分类任务的主流选择,但在分割任务中,SGD with Momentum 有时效果更好。原因在于 SGD 的更新方向在长期看更平稳,适合分割这种需要精细空间特征的任务;而 Adam 在前期收敛快,后期容易出现震荡,导致边界细节反复变化。我的默认配置如下:
import torch import torch.nn as nn import torch.optim as optim model = build_segmentation_model(backbone='resnet50', num_classes=6) criterion = nn.CrossEntropyLoss(ignore_index=255) # 255 为 ignore 类 # 分割任务中更推荐的优化器配置:SGD + Momentum + 多项式衰减 # 初始学习率不宜取太大,0.01 是常见默认值,配合 poly 策略衰减 optimizer = optim.SGD( model.parameters(), lr=0.01, # 8 卡训练时可适当加大到 0.02 momentum=0.9, weight_decay=1e-4, # 遥感影像类别多,权重衰减不宜过大 ) # 多项式衰减:iter 越大学习率越小,比 step 衰减更适合分割 # power=0.9 是常用配置,让模型后期稳定精修 def poly_lr(epoch, max_epoch, initial_lr, power=0.9): return initial_lr * (1 - epoch / max_epoch) ** power scheduler = optim.lr_scheduler.LambdaLR( optimizer, lr_lambda=lambda epoch: poly_lr(epoch, 50, 0.01) )SGD+Momentum 在训练后期比 Adam 更容易收敛到更平滑的局部最优解,在 mIoU 上通常能高出 1~3 个点。Adam 的优势在超参数较少、调试方便,适合做快速 baseline。因此我通常的模式是:先用 Adam 跑通流程,再用 SGD 精调获得最终指标。学习率的初值,单卡 0.01 是保守起点,若使用混合精度训练或分布式训练,可适当提高;但超过 0.05 基本必崩。
4.2 类别不平衡:交叉熵、Dice、Focal 的取舍
遥感分割的类别分布天然不均匀。以土地利用分类为例,植被可能占 50%,水体占 5%,建筑物占 15%,道路占 10%,剩下的类别零零散散。直接用交叉熵训练,模型会把大量参数资源用于拟合占比最高的类别,小类别的像素梯度淹没在大类别中。
实际工程中最有效的方案不是换损失函数,而是先做类别权重。在交叉熵中按类别样本量的倒数设置权重,是最简单也最稳的办法。torch 的 CrossEntropyLoss 自带 weight 参数:
# 先统计训练集中每个类别的像素占比 # 假设 class_ratios = [0.45, 0.05, 0.15, 0.10, 0.15, 0.10] # 权重与占比成反比(训练前根据全量数据统计得到) class_weights = torch.tensor([0.5, 5.0, 1.5, 2.0, 1.5, 2.0], device=device) criterion = nn.CrossEntropyLoss(weight=class_weights, ignore_index=255)权重设置的核心逻辑是“让稀有类别贡献更大的梯度”,但也要防止权重过大导致的类别失衡逆转。比如某个小类别占比不足 1%,如果将其权重设到 20 以上,训练中会出现该类别过拟合,验证集上那类的 IoU 先升后降,其他类别的精度被拉垮。
Dice Loss 对类别不平衡问题天然鲁棒,它基于区域重叠度而非逐像素交叉熵,对小目标的梯度占比更合理。但纯 Dice Loss 训练容易不稳定,因为它的梯度在数值上不饱和,前期剧烈后期无力。常见的方案是交叉熵+Dice 的组合,比如:
def combined_loss(pred, target, weights): ce_loss = nn.functional.cross_entropy(pred, target, weight=weights, ignore_index=255) # 对 pred 做 softmax 后计算 dice probs = torch.softmax(pred, dim=1) target_onehot = torch.nn.functional.one_hot( target, num_classes=pred.shape[1] ).permute(0, 3, 1, 2).float() dice = dice_loss(probs, target_onehot) return ce_loss + 0.5 * dice这个组合方案中,交叉熵负责稳定优化路径,Dice 负责把小目标的梯度托起来。超参数 0.5 是经验值,通常无需频繁调整。Focal Loss 侧重难例挖掘,但对遥感这种目标尺度差异大的场景,容易出现困难样本过于集中导致训练回合延长,收益不如 Dice+CE。
4.3 用 OHEM 提升边界质量:在线难例挖掘的具体写法
边界区域像素数量少,却是模型能力差距最大的地方。交叉熵损失会让大多数背景像素主导梯度,边界像素贡献被淹没。在线难例挖掘(OHEM)的思路是:每次迭代中,只挑选损失值最高的前 k% 像素反向传播。这部分像素正好对应边界、小目标和易混淆区域。
def ohem_cross_entropy(pred, target, keep_ratio=0.3): """ keep_ratio: 保留损失最高的前 30% 像素参与梯度回传 """ loss_per_pixel = nn.functional.cross_entropy( pred, target, ignore_index=255, reduction='none' ) # 将所有非 ignore 的像素展平 valid_mask = (target != 255).view(-1) losses = loss_per_pixel.view(-1) valid_losses = losses[valid_mask] if valid_losses.numel() == 0: return torch.tensor(0.0, device=pred.device) # 按损失值降序排序,取前 keep_ratio k = max(1, int(valid_losses.numel() * keep_ratio)) topk_losses, _ = torch.topk(valid_losses, k) return topk_losses.mean()该函数返回的损失值只来自损失最高的 30% 像素,梯度回传也集中在这部分像素上。实际操作中,keep_ratio 从 0.2 到 0.4 之间是合理区间;调太低训练不稳定,调太高就退化成普通交叉熵。OHEM 在训练后期的收益更明显:前期所有像素都在学,后期只学难点,精度曲线会更平稳。
5. 评估与部署:mIoU 之外,遥感分割还要看什么
5.1 验证集划分与指标解读:OA 和 mIoU 的差距在哪里
遥感分割中 OA(Overall Accuracy)是最常见的指标,但也最容易被“灌水”。如果一张影像中有 60% 的像素是植被,模型只需要把所有像素都预测为植被,OA 就有 60%。所以真正应该看的是 mIoU 和各类别的 IoU 加权平均。
验证集的划分比指标选择更重要。不能把所有 patch 全部随机打乱,因为同一个大影像分出来的相邻 patch 有高度相似的特征,模型在训练中见过类似的 patch,验证结果必然虚高。正确的做法是在影像级别上划分:按地理位置或影像编号分组,保证训练集、验证集、测试集来自不同的影像区域。否则模型的泛化能力会被严重高估,换一张新影像后 mIoU 可能暴跌 5 个点以上。
5.2 重叠推理:切片拼接时重叠区到底保留谁的输出
推理时同样要切块,但重叠区域的拼接策略会直接影响最终结果。直接丢弃重叠区会导致目标被切断,模型输出在 patch 边界处存在明显的“缝”。我常用的方法是:设置重叠率 50%(推理步长为 patch_size // 2),重叠区的每个像素做多次预测,最终结果采用多数投票或预测概率平均。
Softmax 概率平均比多数投票更平滑。具体操作是:为整幅大图维护一个 H×W×C 的概率累加矩阵和一个 H×W 的计数矩阵。每次滑动窗口推理后,将该窗口输出的 softmax 概率累加到对应位置,并将计数加 1。全部窗口推理完后,用累加概率除以计数,得到每个像素的平均概率:
import numpy as np def overlap_inference(model, image, window=512, stride=256, num_classes=6, device='cuda'): h, w = image.shape[:2] probability_map = np.zeros((h, w, num_classes), dtype=np.float32) count_map = np.zeros((h, w, 1), dtype=np.float32) # 从(0,0)开始,按stride滑动,最后一圈不足512像素时向外padding for y in range(0, h - window + 1, stride): for x in range(0, w - window + 1, stride): patch = image[y:y+window, x:x+window] # 归一化与通道维度转换(BCHW) patch_tensor = transform_to_tensor(patch).unsqueeze(0).to(device) with torch.no_grad(): output = torch.softmax(model(patch_tensor), dim=1) probability_map[y:y+window, x:x+window] += output[0].cpu().numpy().transpose(1, 2, 0) count_map[y:y+window, x:x+window] += 1 # 边缘区域如果没被覆盖(比如最右/最下的残余区域),单独推理一次 probability_map /= np.maximum(count_map, 1) final_prediction = np.argmax(probability_map, axis=-1).astype(np.uint8) return final_prediction这段代码的 key point 是 stride 小于 window,重叠区域被多次预测后取均值。它的效果是让分割结果的空间连续性显著优于单次推理,尤其在道路、河流这类长条形基元上,边界更平直。
5.3 提升边缘质量的三个实用技巧
最后分享三个具体技巧,它们不改变模型结构,但能显著提升边界质量。
第一个技巧是条件随机场(CRF)后处理。早期语义分割几乎标配 CRF,现在用得少了,但在遥感分割中,CRF 对消除细碎孤立点依然有效。OpenCV 的cv2.xtra模块里有现成的实现。它对有小块噪点的影像改善最直观,但注意 CRF 作用是局部的,不要指望它修复大面积的错误区域。
第二个技巧是边缘增强训练。在标注阶段额外生成一张边界权重图,让边界像素在损失函数中的权重高于非边界像素。具体操作是对标签做 Canny 边缘检测或形态学梯度,得到边缘像素的 mask,然后将这些像素的交叉熵损失乘上 5~10 倍。这个方式的成本很低,生成边缘图是离线操作,不拖慢训练速度,但边界 IoU 的提升通常在 2 个点以上。
第三个技巧是用测试时增强对预测结果做整合。推理时对同一张大图做多尺度的推理(比如 1.0 倍和 1.25 倍),将两种尺度的概率平均后取 argmax。这个操作能让尺度差异大的目标(既有小房屋又有大厂房)同时获得更好的表现。代价是推理时间成倍增加,更适合离线制图场景而不是在线服务的实时推理。
这三个技巧叠加之后,再去诊断模型的边界问题就会清晰很多:如果加了 CRF 边界还是碎,问题大概率在标签本身;如果大目标边界完整但小目标缺失,问题在损失函数的权重分配。此时回顾 4.3 节的 OHEM 设置和 4.2 节的类别权重,会比继续调 backbone 更有效。
本文还有配套的精品资源,点击获取