ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

遥感语义分割毕设实战:UNet从跑通到答辩全流程

遥感语义分割毕设实战:UNet从跑通到答辩全流程 简介这份资源是面向计算机、人工智能、通信工程等专业学生与教师的高分毕业设计项目包主题为基于Python与UNet网络的遥感图像语义分割答辩评审分达95分适合用作毕设、课程设计、作业或项目初期立项演示也便于基础较好的学习者在此基础上二次开发。压缩包共69个文件约46.93MB包含6个Python源码文件与3个Jupyter Notebook覆盖数据制作、模型定义、训练与预测全流程另有5个tex论文源文件、1个pdf论文、若干png与svg图表、字体及依赖说明文件构成从代码到论文的完整闭环。目前已有189人学习下载。读者可获得可运行的UNet分割实现、遥感数据集处理脚本、论文LaTeX源码与部署文档便于快速复现实验、理解网络结构与训练细节并直接用于毕设撰写与答辩准备。1. 遥感语义分割毕设从 UNet 跑通到能答辩的完整路径遥感图像语义分割这件事真正上手做过的人都知道难点从来不是把 UNet 的论文公式抄一遍而是拿到一批带地理坐标的航拍或卫星影像后怎么把它变成模型能吃的张量、怎么让 8GB 显存的卡不炸、怎么在答辩时解释清楚每一类地物的 IoU 为什么是这个数。这个标题里堆了 Python、UNet、论文源码、部署文档、全部数据资料几个关键词本质上描述的是一套「能直接复现、能写进论文、能当场演示」的毕设交付物。它适合两类人一类是刚学完 Python 基础语法、想找个真实项目练手的本科生另一类是被导师催着交中期成果、需要一套完整 pipeline 撑住论文实验章节的人。我见过太多人卡在数据格式转换那一步就放弃了其实只要把「数据—模型—训练—推理—评估」这条链路拆开每一段都有成熟做法可抄。遥感语义分割和普通自然图像分割最大的区别在于影像幅面大、类别极不均衡、地物边界模糊。一张 512×512 的无人机正射影像里可能 90% 是植被和裸土道路和建筑只占几个百分点。UNet 的编码器-解码器加跳跃连接结构天生适合这种任务因为跳跃连接能把浅层的高分辨率边缘信息直接送到解码端缓解小目标被下采样吃掉的问题。但直接用原版 UNet 跑遥感数据往往会遇到小类别 IoU 长期在 0.1 附近徘徊的情况这就需要在损失函数、数据增强和输入尺寸上做针对性调整。下面几章我会按「先跑通最小闭环再逐项调优最后落到答辩能用的评估和可视化」的顺序展开每一步都给可执行的代码和参数说明。2. 环境搭建与数据准备让 UNet 在本地跑起来的第一步2.1 Python 环境与依赖安装的可靠做法遥感分割项目依赖的库不算多但版本冲突是新手翻车的重灾区。我一般用 conda 建独立环境避免和系统 Python 打架。核心依赖是 PyTorch、torchvision、opencv-python、numpy、matplotlib、tqdm、scikit-learn。如果显卡是 NVIDIA 的装 PyTorch 时一定要去官网查对应 CUDA 版本的安装命令别直接pip install torch否则大概率装成 CPU 版训练时发现 GPU 利用率是 0 才后悔。# 创建独立环境Python 版本建议 3.9 或 3.10兼容性最好 conda create -n rs_seg python3.10 -y conda activate rs_seg # 安装 PyTorch以 CUDA 11.8 为例具体命令按自己驱动版本调整 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 其余依赖 pip install opencv-python numpy matplotlib tqdm scikit-learn pillow装完后用一段短代码验证 GPU 是否可用这一步别省我见过有人训练了两小时才发现跑在 CPU 上。import torch print(CUDA available:, torch.cuda.is_available()) print(Device name:, torch.cuda.get_device_name(0) if torch.cuda.is_available() else CPU) print(PyTorch version:, torch.__version__)如果torch.cuda.is_available()返回 False先检查显卡驱动版本再检查装的 PyTorch 是不是 cu 版本。国内下载慢的话pip 可以临时指定国内源但 PyTorch 的 CUDA 包建议还是走官方 index国内源有时同步不全。2.2 遥感数据集的组织与标签处理遥感语义分割常用的公开数据集有 ISPRS Potsdam、Vaihingen、LoveDA、DeepGlobe 等毕设里用 LoveDA 或自标注的无人机影像都行。不管用哪个最终都要整理成「图像文件夹 标签文件夹 一一对应文件名」的结构。标签必须是单通道的索引图像素值 0、1、2、3 分别代表不同类别而不是 RGB 彩色图。很多新手拿到的标注是彩色 PNG直接喂给模型会导致类别数对不上这是血泪经验。import os import numpy as np import cv2 def convert_color_mask_to_index(mask_path, color_map): 将 RGB 彩色标签图转换为单通道索引图 color_map: dict, {(R,G,B): class_index} mask cv2.imread(mask_path, cv2.IMREAD_COLOR) mask cv2.cvtColor(mask, cv2.COLOR_BGR2RGB) index_mask np.zeros(mask.shape[:2], dtypenp.uint8) for color, idx in color_map.items(): match np.all(mask color, axis-1) index_mask[match] idx return index_mask # 示例三类地物的颜色映射 color_map { (0, 0, 0): 0, # 背景 (255, 0, 0): 1, # 建筑 (0, 255, 0): 2, # 植被 } index_mask convert_color_mask_to_index(label_001.png, color_map) cv2.imwrite(label_001_index.png, index_mask)这段代码的逻辑是逐像素比对颜色命中哪个类别就写对应的索引值。参数color_map必须和你标注工具里定义的颜色完全一致差一个通道值就会漏掉。转换完建议随机抽几张可视化检查确认没有大面积全 0 的情况。另外遥感影像常见的问题是图像和标签尺寸不一致或者标签有偏移训练前统一 resize 到 512×512 或 256×256标签用最近邻插值图像用双线性插值别搞反了。2.3 数据增强与 Dataset 类的写法遥感数据标注成本高样本量往往只有几百张不做增强很容易过拟合。我一般用 albumentations 库它支持图像和标签同步变换比手写 cv2 翻转靠谱得多。增强策略上水平翻转、垂直翻转、随机旋转 90 度对遥感影像都安全因为航拍视角没有固定的上下方向。颜色抖动要谨慎遥感影像的色调和地物类别有一定关联抖太狠会让模型学到错误的颜色先验。import albumentations as A from torch.utils.data import Dataset import cv2 train_transform A.Compose([ A.HorizontalFlip(p0.5), A.VerticalFlip(p0.5), A.RandomRotate90(p0.5), A.RandomResizedCrop(512, 512, scale(0.7, 1.0)), A.Normalize(mean(0.485, 0.456, 0.406), std(0.229, 0.224, 0.225)), ]) class RSDataset(Dataset): def __init__(self, img_dir, mask_dir, transformNone): self.img_dir img_dir self.mask_dir mask_dir self.transform transform self.names sorted(os.listdir(img_dir)) def __len__(self): return len(self.names) def __getitem__(self, idx): name self.names[idx] img cv2.imread(os.path.join(self.img_dir, name)) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) mask cv2.imread(os.path.join(self.mask_dir, name), cv2.IMREAD_GRAYSCALE) if self.transform: augmented self.transform(imageimg, maskmask) img, mask augmented[image], augmented[mask] img img.transpose(2, 0, 1).astype(np.float32) return torch.from_numpy(img), torch.from_numpy(mask).long()Normalize用的均值方差是 ImageNet 的统计值遥感影像严格来说应该用自己的数据集统计但实践中直接用 ImageNet 的值也能收敛差别不大。RandomResizedCrop的 scale 下限别设太低0.7 左右比较稳再低会把小目标裁没。Dataset 返回的 mask 必须是 long 类型因为交叉熵损失要求标签是 int64。3. UNet 模型搭建与训练参数怎么设、损失怎么选3.1 原版 UNet 结构的关键参数UNet 的结构本身不复杂编码器四次下采样解码器四次上采样中间用跳跃连接拼接。但有几个参数直接决定模型能不能跑、效果好不好。第一是输入通道数遥感影像可能是 RGB 三通道也可能是多光谱的 4 通道甚至 8 通道in_channels要跟着改。第二是类别数num_classes必须等于你标签里的最大索引加一少一个类别训练时就会报 index out of range。第三是基础通道数base_channels原版是 64显存不够可以降到 32但别低于 16否则特征表达能力太弱。import torch import torch.nn as nn class DoubleConv(nn.Module): def __init__(self, in_ch, out_ch): super().__init__() self.conv nn.Sequential( nn.Conv2d(in_ch, out_ch, 3, padding1), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue), nn.Conv2d(out_ch, out_ch, 3, padding1), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue), ) def forward(self, x): return self.conv(x) class UNet(nn.Module): def __init__(self, in_channels3, num_classes4, base_channels64): super().__init__() chs [base_channels, base_channels*2, base_channels*4, base_channels*8] self.down1 DoubleConv(in_channels, chs[0]) self.down2 DoubleConv(chs[0], chs[1]) self.down3 DoubleConv(chs[1], chs[2]) self.down4 DoubleConv(chs[2], chs[3]) self.pool nn.MaxPool2d(2) self.bottleneck DoubleConv(chs[3], chs[3]*2) self.up4 nn.ConvTranspose2d(chs[3]*2, chs[3], 2, stride2) self.conv4 DoubleConv(chs[3]*2, chs[3]) self.up3 nn.ConvTranspose2d(chs[3], chs[2], 2, stride2) self.conv3 DoubleConv(chs[2]*2, chs[2]) self.up2 nn.ConvTranspose2d(chs[2], chs[1], 2, stride2) self.conv2 DoubleConv(chs[1]*2, chs[1]) self.up1 nn.ConvTranspose2d(chs[1], chs[0], 2, stride2) self.conv1 DoubleConv(chs[0]*2, chs[0]) self.out nn.Conv2d(chs[0], num_classes, 1) def forward(self, x): d1 self.down1(x) d2 self.down2(self.pool(d1)) d3 self.down3(self.pool(d2)) d4 self.down4(self.pool(d3)) b self.bottleneck(self.pool(d4)) u4 self.conv4(torch.cat([self.up4(b), d4], dim1)) u3 self.conv3(torch.cat([self.up3(u4), d3], dim1)) u2 self.conv2(torch.cat([self.up2(u3), d2], dim1)) u1 self.conv1(torch.cat([self.up1(u2), d1], dim1)) return self.out(u1)ConvTranspose2d的 kernel_size 和 stride 都设 2正好把特征图放大一倍和对应编码层的尺寸对齐。拼接时用torch.cat沿通道维合并所以DoubleConv的输入通道是两倍。如果输入尺寸不是 16 的整数倍下采样四次后可能出现奇数尺寸导致拼接失败训练前统一 resize 到 512 或 256 就能避开。3.2 损失函数与优化器的选择遥感数据类别不均衡是常态纯交叉熵会让模型偏向大类别。我一般用交叉熵加 Dice 损失的组合Dice 对小类别更敏感。权重上交叉熵占 0.5Dice 占 0.5或者根据类别频率给交叉熵加 class weight。优化器用 AdamW学习率 1e-3 起步配合余弦退火。batch size 在 8GB 显存下512×512 输入大概能跑 4 到 8256×256 能跑 16。class DiceLoss(nn.Module): def __init__(self, smooth1e-6): super().__init__() self.smooth smooth def forward(self, logits, targets): probs torch.softmax(logits, dim1) targets_onehot torch.nn.functional.one_hot(targets, num_classeslogits.shape[1]) targets_onehot targets_onehot.permute(0, 3, 1, 2).float() intersection (probs * targets_onehot).sum(dim(2, 3)) union probs.sum(dim(2, 3)) targets_onehot.sum(dim(2, 3)) dice (2 * intersection self.smooth) / (union self.smooth) return 1 - dice.mean() ce_loss nn.CrossEntropyLoss() dice_loss DiceLoss() def combined_loss(logits, targets): return 0.5 * ce_loss(logits, targets) 0.5 * dice_loss(logits, targets)Dice 损失里smooth防止分母为零one_hot把标签转成和 logits 同形状。注意permute的顺序one_hot 输出是 (N, H, W, C)要转成 (N, C, H, W) 才能和 probs 对齐。如果某些类别在 batch 里完全没出现Dice 那一项会接近 1梯度不稳定可以给每个类别加一个最小出现次数过滤或者直接用带 class weight 的交叉熵。3.3 训练循环与显存优化训练循环本身是模板化的但有几个细节决定成败。第一每个 epoch 结束后在验证集上算 mIoU别只看 lossloss 降了 mIoU 不一定涨。第二保存最佳模型时按 mIoU 存不是按 loss 存。第三显存不够时用混合精度训练torch.cuda.amp能省 30% 到 40% 显存速度也快。from torch.cuda.amp import autocast, GradScaler from tqdm import tqdm scaler GradScaler() optimizer torch.optim.AdamW(model.parameters(), lr1e-3, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max50) for epoch in range(50): model.train() loop tqdm(train_loader, descfEpoch {epoch1}) for imgs, masks in loop: imgs, masks imgs.cuda(), masks.cuda() optimizer.zero_grad() with autocast(): logits model(imgs) loss combined_loss(logits, masks) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update() loop.set_postfix(lossloss.item()) scheduler.step() # 验证集评估 mIoU 的代码略按需补充autocast上下文里做前向scaler负责梯度缩放防止 fp16 下梯度下溢。CosineAnnealingLR的T_max设成总 epoch 数学习率从 1e-3 平滑降到接近 0。如果训练 loss 震荡厉害先把学习率降到 5e-4 试试别急着改模型结构。4. 推理、评估与可视化答辩时拿得出手的结果4.1 滑窗推理处理大幅面遥感影像遥感影像动辄几千乘几千像素直接整张喂给 UNet 会爆显存而且下采样后小目标全没了。标准做法是滑窗推理把大图切成有重叠的小块逐块预测后再拼回去。重叠区域用加权平均边缘权重低、中心权重高能消除拼接缝。def sliding_window_inference(model, image, window_size512, stride256, num_classes4): model.eval() h, w image.shape[:2] prob_map np.zeros((num_classes, h, w), dtypenp.float32) count_map np.zeros((h, w), dtypenp.float32) weight np.ones((window_size, window_size), dtypenp.float32) # 高斯权重边缘低中心高 for i in range(window_size): for j in range(window_size): weight[i, j] np.exp(-((i-window_size/2)**2 (j-window_size/2)**2) / (2*(window_size/4)**2)) for y in range(0, h, stride): for x in range(0, w, stride): y2, x2 min(ywindow_size, h), min(xwindow_size, w) y1, x1 max(0, y2-window_size), max(0, x2-window_size) patch image[y1:y2, x1:x2] patch_tensor torch.from_numpy(patch.transpose(2,0,1)).float().unsqueeze(0).cuda() with torch.no_grad(): out torch.softmax(model(patch_tensor), dim1).squeeze(0).cpu().numpy() prob_map[:, y1:y2, x1:x2] out * weight[:y2-y1, :x2-x1] count_map[y1:y2, x1:x2] weight[:y2-y1, :x2-x1] prob_map / np.maximum(count_map, 1e-6) return np.argmax(prob_map, axis0)stride设成 window_size 的一半重叠率 50%既能覆盖边界又不会太慢。高斯权重让中心预测更可信边缘预测权重低拼接后过渡自然。如果显存够window_size 可以开到 1024但 stride 也要跟着调否则重叠不够还是会有缝。4.2 mIoU 与混淆矩阵的计算答辩时老师最爱问的就是每类地物的 IoU 是多少所以评估代码必须写对。mIoU 是各类 IoU 的平均IoU 等于交集除以并集。用混淆矩阵算最稳妥不容易出错。def compute_confusion_matrix(preds, labels, num_classes): mask (labels 0) (labels num_classes) hist np.bincount( num_classes * labels[mask].astype(int) preds[mask], minlengthnum_classes**2 ).reshape(num_classes, num_classes) return hist def compute_iou(conf_matrix): intersection np.diag(conf_matrix) union conf_matrix.sum(axis1) conf_matrix.sum(axis0) - intersection iou intersection / np.maximum(union, 1e-6) miou np.nanmean(iou) return iou, mioubincount那行是标准写法把 (label, pred) 对映射成一个整数索引。np.nanmean忽略掉没有出现的类别避免除零。如果某类 IoU 特别低先看混淆矩阵里它被错分成了什么再针对性加样本或调损失权重。4.3 预测结果可视化与论文配图论文里的分割结果图要清晰、有对比最好放原图、标签、预测三列。配色用固定色表别用随机颜色否则不同图之间没法对比。matplotlib 保存时 dpi 设 300矢量图存 pdf 或 svg位图存 png。import matplotlib.pyplot as plt def visualize_result(image, label, pred, save_path): fig, axes plt.subplots(1, 3, figsize(15, 5)) axes[0].imshow(image) axes[0].set_title(Original) axes[1].imshow(label, cmaptab20, vmin0, vmax19) axes[1].set_title(Ground Truth) axes[2].imshow(pred, cmaptab20, vmin0, vmax19) axes[2].set_title(Prediction) for ax in axes: ax.axis(off) plt.tight_layout() plt.savefig(save_path, dpi300, bbox_inchestight) plt.close()tab20色表最多支持 20 类够用。vmin和vmax固定住保证不同图的颜色映射一致。如果类别超过 20自己定义颜色列表传给cmap。5. 避坑与排查遥感 UNet 训练中最容易翻车的五个点5.1 现象训练 loss 正常下降但 mIoU 一直是 0原因通常是标签索引和模型输出类别数不匹配。比如标签里最大像素值是 3但num_classes设成了 3模型只输出 0、1、2 三类第 3 类永远预测不出来混淆矩阵里那一行全是 0IoU 算出来是 nan。解决方法是打印标签的唯一值确认num_classes max_label 1。5.2 现象显存明明够却报 CUDA out of memory多数情况是验证阶段没加torch.no_grad()或者滑窗推理时窗口开太大。训练时 batch size 能跑 8不代表推理时能整张图塞进去。解决方法是推理全程包在with torch.no_grad():里滑窗的 window_size 从 512 开始试不行降到 256。5.3 现象小类别 IoU 长期低于 0.2遥感数据里道路、水体这类细长地物容易被大类别淹没。除了加 Dice 损失还可以在交叉熵里给每个类别按频率倒数加权。另一个有效做法是过采样包含小类别的 patch在 Dataset 里统计每个样本的小类别像素占比占比高的样本提高采样概率。5.4 现象预测结果有网格状拼接缝滑窗推理时 stride 等于 window_size没有重叠或者重叠区域直接取平均没加权。解决方法是 stride 设为 window_size 的一半并用高斯权重做加权平均。如果还有缝检查图像归一化是不是在切块前做的切块后再归一化会导致块间统计不一致。5.5 现象换了数据集后模型完全不收敛不同数据集的影像亮度、对比度、地物尺度差异很大。直接拿预训练权重微调时学习率要降到 1e-4 甚至更低并且先冻结编码器训练几轮解码头再解冻全部。另外检查新数据集的标签是不是索引图如果是彩色图必须先转换。6. 进阶技巧让毕设从及格到优秀的三个加分项第一个加分项是注意力机制。原版 UNet 的跳跃连接是直接拼接浅层特征里噪声多可以在拼接前加一个通道注意力模块让模型自己学哪些通道重要。实现上就是在DoubleConv后面接一个 SE block代码不到十行但论文里能多写一节消融实验。class SEBlock(nn.Module): def __init__(self, channels, reduction16): super().__init__() self.fc nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Conv2d(channels, channels // reduction, 1), nn.ReLU(inplaceTrue), nn.Conv2d(channels // reduction, channels, 1), nn.Sigmoid(), ) def forward(self, x): return x * self.fc(x)第二个加分项是地物面积估算。答辩时老师常问「分割出来能干嘛」你可以把预测的像素数乘以影像分辨率算出每类地物的实际面积。比如无人机影像分辨率是 0.1 米每像素某类预测了 50000 个像素实际面积就是 50000 × 0.1 × 0.1 500 平方米。这个功能在基于 u net 的遥感图像语义分割与地物面积估算系统这类需求里是核心卖点代码就是在预测结果上做np.bincount再乘系数。第三个加分项是 TensorBoard 可视化训练过程。把 loss、mIoU、学习率曲线都记进去论文里放一张训练曲线图比干巴巴的文字有说服力。torch.utils.tensorboard的SummaryWriter几行就能接入add_scalar记标量add_image记预测图。我自己的习惯是每做完一个版本就在验证集上跑一次完整评估把 mIoU 和每类 IoU 记到表格里改了什么、涨了多少一目了然。别等到最后才补实验那时候数据对不上后悔药没处买。这套流程从环境到评估走一遍大概两三天能跑通剩下的时间都花在调参和写论文上。希望帮到你。本文还有配套的精品资源点击获取
返回列表