简介:以PyTorch为基础实现的高分遥感语义分割(地物分类)项目,围绕高分二号真实影像数据,提供从数据准备、模型训练到结果可视化的完整源码与说明文档。主要面向计算机、通信、人工智能、自动化等相关专业的高校学生、教师或从业者,适合作为毕业设计、课程设计及项目复现的参考。资源共858个文件,压缩包约550.47MB;819张PNG样例图包含遥感影像切片、标注与预测结果可视化,35个Python脚本涵盖数据读取、模型构建、训练与评估流程,另有CSV索引与DOCX手册,结构清晰、便于按模块查阅。已有157人学习下载,项目为个人毕设成果,答辩评审分达98分,代码经调试测试可正常运行。借助影像数据、脚本和文档,可快速掌握高分遥感地物分类的完整实现思路,也能在现有框架上扩展迁移到其他语义分割场景。
1. 基于PyTorch的高分遥感语义分割:这张图不是“图片”,是一片带着坐标的地面
高分遥感影像和手机拍的照片有本质区别:一张高分二号或者WorldView级别的影像,动辄一万二乘一万二像素,单个文件几百MB,更重要的是每个像素都对应着真实的地面坐标,直接把它当普通图片塞进PyTorch的DataLoader里,第一轮训练就会把显存和耐心一起耗尽。所以基于PyTorch做高分遥感语义分割(地物分类)这条路的第一个任务不是调模型,而是想清楚“怎么把一景大影像变成模型吃得下的patch,再在推理阶段把预测结果拼回真实坐标”。
这篇文章会顺着毕业设计里最常走的完整链路往下讲:先说明为什么DeepLabV3+这类带空洞卷积的语义分割模型适合遥感地物分类,再讲数据怎样切、标签怎样对齐、类别不平衡怎样处理,然后给出一套可以直接改参数跑起来的PyTorch训练与评估闭环,最后落在推理阶段的重叠滑窗、多尺度TTA和CRF边界修正这些真正决定最终图精度的技巧上。适合已经会用PyTorch做分类任务、但还没完整跑过语义分割的人,也适合想在答辩前把每个参数为什么这么设讲清楚的人。
2. 为什么DeepLabV3+是高分遥感地物分类的稳妥选型
高分遥感语义分割任务里,一个建筑物可能占几百个像素,一辆车只有十几个像素,道路连续延伸数千像素,而阴影和植被的边界又细又碎。模型要在同一个特征空间里同时照顾这些尺寸差异巨大的地物,核心矛盾是感受野:卷积核太小,抓不住大尺度上下文;卷积核太大,参数和计算量又扛不住。DeepLabV3+用一组并列的空洞卷积来解决这个问题,这也是它在“deeplabv3语义分割”这个方向里长期是默认选项的原因。
2.1 ASPP模块:用不同扩张率并行抓多尺度上下文
DeepLabV3+的Decoder前端是一个叫ASPP(Atrous Spatial Pyramid Pooling)的结构,它对同一个特征图并行做四路处理:一路1×1卷积,三路扩张率分别为6、12、18的3×3空洞卷积,再加一路全局平均池化后上采样回来,最后把所有输出拼接起来。空洞卷积的作用是在不增加参数量、不下采样特征图的前提下扩大感受野,这正是遥感地物分类需要的特性——影像本来就大,再频繁下采样会让小目标直接消失。
以单层3×3卷积为例,扩张率d对应的等效卷积核尺寸和感受野可以这样算:
| 扩张率 d | 等效卷积核 | 单层感受野 |
|---|---|---|
| 1 | 3×3 | 5×5 |
| 6 | 13×13 | 15×15 |
| 12 | 25×25 | 27×27 |
| 18 | 37×37 | 39×39 |
注意表格里只是单层空洞卷积的理论感受野,网络经过多层串联后实际感受野会更大。这里有一个容易被忽略的点:ASPP里扩张率取6、12、18并不是拍脑袋,而是这几个数值对应的等效卷积核覆盖了从“单个小目标”到“一大片建筑区”的中间尺度。毕业设计里如果只看默认参数不动,也能跑出不错的结果,但明白这个数值关系,才能在答辩被问到“为什么用这三个扩张率”时接得住。
2.2 输入patch尺寸与显存的取舍关系
高分遥感影像不能整图进网络,必须先切成固定尺寸的patch。patch选多大,直接决定了单个patch对应的地面范围:2米分辨率影像上,512×512的patch覆盖约1公里×1公里的地面;如果换到0.5米分辨率,同样512×512只覆盖256米×256米。这就是高分遥感和普通图像分类最不一样的地方——同样的模型输入尺寸,在不同分辨率影像上看到的“物理世界大小”完全不同,这直接影响模型能不能学到道路、建筑的完整轮廓。
显存预算方面,以DeepLabV3+配ResNet50 Backbone为例,输入512×512、batch size为4,COCO预训练权重加载后训练显存占用通常接近8GB;同样的patch尺寸把batch调到8,就要摸到14GB左右的上限。我一般会这样取舍:优先保证patch不小于512,因为遥感地物分类里上下文信息太重要,patch太小连一个完整建筑群都装不下;显存不够就降batch size,降到4以下时BN层统计量会很不稳定,可以考虑把Backbone的BN换成GroupNorm,或者直接用batch size 4配梯度累积。
提示:batch size降到2以下时,BN层的均值和方差估计几乎失效,训练时即使Loss在降,验证集mIoU也可能一直上不去。这种情况优先开梯度累积,而不是硬着头皮继续训。
2.3 与U-Net、SegFormer的对比:毕业设计场景下谁更稳
U-Net的跳跃连接在医学图像分割里表现很好,但放到高分遥感上有一个明显短板:它的Encoder下采样倍数固定,感受野扩展依赖反复池化,对大面积连片地物的上下文捕捉不如空洞卷积直接;SegFormer这类Transformer模型精度确实能做得更高,但训练时间和显存开销都不小,在只有一张消费级显卡的毕业设计环境里,调参周期会被拉得很长。DeepLabV3+的优势在于:PyTorch官方torchvision里直接提供了ResNet Backbone的预训练权重,修改分类头就能迁移到地物分类;ASPP的多尺度结构天然适配遥感地物尺寸差异极大的特点;训练收敛速度比Transformer快,适合迭代实验。
3. 语义分割数据集如何制作:高分影像切片与标签对齐
做遥感语义分割,数据准备阶段的时间通常比训练还长。高分影像动辄上万像素宽,标签图往往是由标注软件导出的同尺寸单波段文件,两个文件必须严格逐像素对齐。这里最容易出的问题不是模型,而是切片时把影像和标签错位了一个偏移量,导致训练Loss降不下去,但人眼又很难发现。所以高分影像切片的第一原则是:影像和标签必须用同一套滑窗参数、同一个起始坐标来切,并且把原始投影和仿射变换参数原样保留下来。
3.1 用GDAL读取高分影像并保留地理坐标信息
GDAL是遥感领域读取栅格数据的标准库,它能拿到影像的波段数、宽高、投影字符串和仿射变换六参数,其中仿射变换记录的是影像左上角坐标和每个像素的地面尺寸。切片时,把这些信息随patch一起保存,后期推理完才能把预测结果精确拼回真实地理坐标系。
from osgeo import gdal import numpy as np def read_image_geo(path): ds = gdal.Open(path) # 读取仿射变换参数和投影信息 geotransform = ds.GetGeoTransform() projection = ds.GetProjection() # 按波段读入为 (C, H, W) 的numpy数组 image = ds.ReadAsArray() ds = None # GDAL读入影像波段顺序是 B1, B2, B3... # 高分影像通常是RGB加近红外,模型输入需要转为 HWC 的RGB顺序 if image.ndim == 3: image = image[:3].transpose(1, 2, 0) return image, geotransform, projection这段代码做了两件事:把整景高分影像读进内存,同时把地理信息取出来备用。需要注意波段顺序,高分影像常是RGBNir四个波段,PyTorch模型一般按RGB三通道预训练,所以这里只取前三个波段并转成HWC格式。如果确实需要用到近红外波段做植被分析,可以保留四通道并修改模型第一层卷积的输入通道数,但毕业设计里大多数地物分类任务RGB已经足够。
3.2 滑窗切片的三个关键参数:patch_size、stride与边界模式
切片逻辑不复杂,但参数设置直接决定训练样本数量和信息完整性。patch_size控制模型输入尺寸,一般取512或256;stride控制滑窗步长,当stride小于patch_size时相邻patch有重叠,相当于给训练集做了无损扩充。边界部分有两种处理:丢弃不足patch_size的边条,或者用镜像填充补齐。遥感影像四周往往是大片无关背景,我一般选择丢弃,省得模型在这些区域浪费时间。
def sliding_crop(image, label, patch_size=512, stride=256): """ 对影像和标签做同步滑窗裁剪。 影像尺寸不足patch_size的行列会被直接丢弃。 """ h, w = image.shape[:2] patches, labels = [], [] # 注意range的步长是stride,patch与patch之间会产生重叠 for y in range(0, h - patch_size + 1, stride): for x in range(0, w - patch_size + 1, stride): patch = image[y:y + patch_size, x:x + patch_size] lab = label[y:y + patch_size, x:x + patch_size] patches.append(patch) labels.append(lab) return np.stack(patches), np.stack(labels)训练时stride=256意味着每个patch和相邻patch有50%的重叠,样本数量大约是stride=512时的四倍。这个操作本质上是把一张大图扩充成大量有重叠的子图,强迫模型在不同偏移下都能正确分类,对提升边界稳定性作用很大。推理阶段则反过来,为了避免同一地物被预测多次导致拼接缝,推理时一般让stride等于patch_size,或者用稍小的重叠配合后面要讲的权重融合策略。还有个细节:PyTorch的DataLoader要求输入是(N, C, H, W)四维张量,这里的输出是(N, H, W, C),训练前需要transpose一下。
3.3 标注类别整理与类别不平衡的权重计算
高分遥感地物分类的标签图里,类别往往有几十种甚至上百种,但很多细类在特定场景下并不需要区分。毕业设计里通常的做法是把原始标注合并成一个6到8类的小体系,比如:不透水面、建筑、道路、植被、裸地、水体。合并标签时要特别注意一件事:类别ID必须从0开始连续递增,不能跳号。语义分割的交叉熵损失函数在做log_softmax时会按类别数生成输出通道,如果标签里出现了一个不在预设范围内的ID,Loss会直接变成NaN。
合并完类别之后,统计每个类别的像素占比。高分遥感影像中水体、植被的像素量往往远大于道路和独立建筑,直接用原始交叉熵训练,小类别会被大类别“淹没”。常用的做法是计算类别权重并传给损失函数:
import numpy as np def compute_class_weights(label, num_classes=6): # 统计每个类别像素个数 counts = np.bincount(label.ravel(), minlength=num_classes).astype(np.float32) # 像素占比,加eps防止除零 freq = counts / (counts.sum() + 1e-8) # 常见做法:权重 = 1 / 频率,再用对数或开根号压缩动态范围 weights = 1.0 / (freq ** 0.5 + 1e-8) weights = weights / weights.mean() return weights权重计算里那个0.5次方是个实用的折中:如果直接用1/freq,小类别权重会被放得极大,训练初期模型会疯狂把像素预测成小类别;开了根号之后权重差异更温和,模型既不会忽略小类别,也不会过度偏向。算出来的weights是一个长度为类别数的数组,后续传给nn.CrossEntropyLoss(weight=...)时会自动逐类别加权。
4. 基于PyTorch的训练闭环:模型构建、损失函数与评估指标
数据准备妥当后,进入基于PyTorch的训练阶段。这一章不是把torchvision的示例代码搬过来就完事,而是要交代清楚三个直接影响结果的部分:预训练模型怎么接上自己的类别数、损失函数怎么配合类别权重、验证指标到底该看哪个数。
4.1 加载torchvision预训练模型并修改分类头
torchvision从某个版本起直接内置了DeepLabV3和DeepLabV3+的实现,并且提供了在COCO语义分割数据集上预训练的权重。迁移学习的策略是:把Backbone和ASPP部分的参数原样拿过来,只把最后的分类卷积层输出通道数改成自己的类别数。
import torch import torch.nn as nn from torchvision.models.segmentation import deeplabv3_resnet50, DeepLabV3_ResNet50_Weights def build_model(num_classes=6, pretrained=True): if pretrained: # COCO预训练权重包含21类(20类+VOC背景) model = deeplabv3_resnet50(weights=DeepLabV3_ResNet50_Weights.COCO_WITH_VOC_LABELS_V1) else: model = deeplabv3_resnet50(weights=None) # classifier是Sequential,最后一层把输出从21类改成num_classes in_channels = model.classifier[4].in_channels model.classifier[4] = nn.Conv2d(in_channels, num_classes, kernel_size=1) return model模型里classifier[4]是ASPP之后最后的1×1卷积层,它的in_channels是256,改动它不会影响前面预训练好的特征提取能力。这里有一个容易踩的坑:如果只改model.classifier[4]而忘掉在DeepLabV3+里还有一个auxiliary classifier,训练时用了aux_loss=True就会报维度不匹配。deeplabv3_resnet50默认的auxiliary classifier在model.aux_classifier里也有一个输出层,同样需要把它的Conv2d输出通道改成num_classes,或者训练时直接把aux_loss关掉。
4.2 损失函数:带权重的交叉熵与Dice Loss组合
地物分类的标签分布天然不均衡,单用交叉熵在道路、建筑这类中频类别上表现还行,但遇到极稀有的类别(比如独立树木、小水体)时召回率会很差。常见做法是把类别权重交叉熵和一个简单的Dice Loss按比例相加:
class_weights = torch.tensor([0.8, 1.2, 0.5, 1.5, 0.7, 2.0], device=device) criterion_ce = nn.CrossEntropyLoss(weight=class_weights, ignore_index=255) def dice_loss(pred, target, smooth=1.0): # pred是logits,先做softmax再取预测类别对应的概率 pred_prob = torch.softmax(pred, dim=1) # 转成one-hot编码与target比较 n, c, h, w = pred.shape target_onehot = torch.zeros_like(pred_prob).scatter_( 1, target.unsqueeze(1).long(), 1.0) intersection = (pred_prob * target_onehot).sum(dim=(2, 3)) union = pred_prob.sum(dim=(2, 3)) + target_onehot.sum(dim=(2, 3)) dice = (2.0 * intersection + smooth) / (union + smooth) return 1.0 - dice.mean() total_loss = criterion_ce(pred, target) + 0.3 * dice_loss(pred, target)交叉熵负责逐像素的类别判别,Dice Loss直接优化区域重叠度,两个损失相加能明显改善小类别召回率。这里0.3是一个经验系数,Dice Loss的数值范围本身就比交叉熵小,权重太高会破坏交叉熵的梯度方向。ignore_index=255是遥感标签里另一个常用约定:把标注质量差或难以判断的像素设为255,训练时完全不参与损失计算。
4.3 训练流程与PyTorch环境的常见坑
训练主循环里有一个必做的设计:学习率衰减采用poly策略,而不是每N个epoch直接乘以0.1。poly策略让学习率随训练进度平滑下降,对语义分割这种需要后期慢慢磨边界的任务更友好。配置环境时,PyTorch安装直接用Anaconda建虚拟环境最省事,GPU版执行conda create -n seg python=3.9后,再按PyTorch官网给的命令安装对应CUDA版本的torch和torchvision。如果用的是CPU环境,训练一个512×512的DeepLabV3+会慢到无法接受,毕业设计至少需要一块显存不低于6GB的GPU。
from torch.optim.lr_scheduler import LambdaLR optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4, weight_decay=1e-4) num_epochs = 100 def poly_decay(epoch): return (1 - epoch / num_epochs) ** 0.9 scheduler = LambdaLR(optimizer, lr_lambda=poly_decay)训练中Loss怎么调都降不下去时,第一反应不要是调学习率,先打印模型输入和标签的数值范围:图像像素有没有归一化到0到1,标签是不是从0开始连续。高分影像经常是16位存储,像素值范围远超普通图像的0到255,如果忘了做归一化,BN层会被打乱,Loss直接不收敛。另一个常见问题是损失图里突然出现NaN,多半是学习率过大导致梯度爆炸,把lr从1e-4降到3e-5再试。
4.4 评估指标:mIoU、Kappa与逐类F1的计算方法
语义分割的评估不能只看准确率,因为大类别占比高会掩盖小类别的失败。遥感地物分类最常看的指标是mIoU和Kappa系数,代码实现时只需要维护一个混淆矩阵:
def compute_iou_kappa(confusion_matrix): # 混淆矩阵是 (num_classes, num_classes),行是真实类别,列是预测类别 iou = np.diag(confusion_matrix) / ( confusion_matrix.sum(axis=1) + confusion_matrix.sum(axis=0) - np.diag(confusion_matrix) + 1e-8) miou = np.nanmean(iou) # Kappa系数:衡量预测与真实之间的一致性,排除了随机一致的部分 total = confusion_matrix.sum() p0 = np.diag(confusion_matrix).sum() / (total + 1e-8) pe = (confusion_matrix.sum(axis=1) * confusion_matrix.sum(axis=0)).sum() / (total ** 2 + 1e-8) kappa = (p0 - pe) / (1 - pe + 1e-8) return miou, kappa, iou训练时每两个epoch用验证集算一次mIoU,保存验证集上mIoU最高的模型权重,而不是保存最后一个epoch的权重,这一步能让最终结果高出1到2个百分点。只看mIoU还不够,高分遥感地物分类里道路这类细长目标往往IoU不高但场景中很重要,建议在评估表里把每类IoU也打印出来,重点看道路和小建筑的IoU变化,才能判断后处理该往哪个方向优化。
5. 推理阶段的三层后处理:避免拼接缝、提升边界精度
训练结束后的推理阶段,是一个“细节决定成败”的地方——模型预测出来的概率图raw输出直接填色拼接,会在patch交界处看到明显的马赛克状接缝,边界也毛糙得像锯齿。这一章按“先消除接缝,再整体提升精度,最后细化边界”三层来做后处理,每一层都有明确收益。
5.1 重叠滑窗推断:不直接拼图,加权平均重叠区
推理时放弃stride=patch_size的无重叠方案,改成让相邻窗口有1/4到1/2的重叠。每个patch预测出概率图后,不是取argmax直接写回原图,而是把每个位置的预测概率当作“该滑窗对这块区域的一次投票”,最终概率取所有覆盖过该位置的滑窗的平均值。这个操作能显著缓解模型在patch边缘信息不足导致的错误预测。
def sliding_inference(model, image, patch_size=512, stride=256, num_classes=6): h, w = image.shape[:2] # score累加每个像素各类别的概率,count记录每个像素被多少个patch覆盖 score = np.zeros((num_classes, h, w), dtype=np.float32) count = np.zeros((h, w), dtype=np.float32) for y in range(0, h - patch_size + 1, stride): for x in range(0, w - patch_size + 1, stride): patch = image[y:y+patch_size, x:x+patch_size] # 转成模型输入格式(N,C,H,W),归一化后前向传播 input_tensor = torch.from_numpy(patch.transpose(2, 0, 1)).unsqueeze(0).float().cuda() with torch.no_grad(): output = model(input_tensor)['out'] # (1, C, H, W) prob = torch.softmax(output, dim=1).cpu().numpy()[0] # (C, H, W) # 累加概率,同时更新覆盖计数 score[:, y:y+patch_size, x:x+patch_size] += prob count[y:y+patch_size, x:x+patch_size] += 1 # 每个像素最终概率 = 所有覆盖它的滑窗概率平均值 final_prob = score / (count[None] + 1e-8) return final_prob.argmax(axis=0)推理时stride=256,输出结果比无重叠推段在边界处的错分要少很多。代价是推理时间随着重叠率上升而增加——patch_size=512、stride=256意味着每个像素被4个patch覆盖,推理耗时大约是stride=512时的4倍(每张patch一模一样,但因为重叠几乎每个区域都被重复算了4次)。实际项目里如果时间有限,可以把stride设为patch_size的一半,即512,兼顾速度与质量。
5.2 多尺度TTA:三个尺度投票,mIoU稳定提升1到2个点
重叠滑窗解决的是空间上的不连续,多尺度TTA解决的是尺度上的不连续。高分影像里同一个地物在不同尺度下看形状完全不同:1倍尺度下道路是一整条连续结构,0.75倍尺度下道路在视野里更“开阔”,模型可能更关注它与两侧地物的边界关系。做法是把同一个小patch分别按0.75、1.0、1.25倍缩放后送入模型,将三个尺度得到的概率图插值回原始patch大小后取平均,再进入argmax。多尺度TTA基本不需要改模型代码,只需要对输入图像做一遍resize,换来的是边界更平滑、建筑轮廓更完整,mIoU通常能提升1到2个百分点,是性价比极高的推理增强手段。
5.3 DenseCRF边界修正:让分割边界贴合真实地物边缘
最后一步是可选但效果明显的DenseCRF(全连接条件随机场),它利用高分影像原始像素的颜色和位置信息,对模型输出的概率图做边界修正。具体原理是把每个像素看作图上的一个节点,像素颜色相近、位置相邻的节点更可能属于同一类,通过迭代扩散来抹平概率图中的小噪点并拉直边界。对高分遥感这种分辨率高、地物边缘锐利的场景,CRF能把建筑边界和道路边缘那些锯齿状错分像素修正掉。
在PyTorch生态里没有原生的DenseCRF实现,常见做法是把模型输出的概率图和原图一起传给pydensecrf库处理。一个在遥感语义分割中比较常用的参数配置是pos_w=3, pos_xy_std=1, bi_w=4, bi_xy_std=67, bi_rgb_std=3,其中pos_w控制位置平滑的强度,bi_w控制双边平滑的强度,bi_xy_std和bi_rgb_std分别控制颜色相似度和空间距离对边界力的影响。CRF的代价是处理时间随像素数上升,一张512×512的patch一般要数十毫秒到上百毫秒,所以最划算的使用方式不是修正整张拼接大图,而是先完成重叠滑窗和多尺度TTA得到拼接结果,再单独把置信度较低的区域裁剪出来做CRF精修,既控制时间又精确打击道路边缘与阴影错分这类具体问题。
本文还有配套的精品资源,点击获取