简介这份资源面向计算机相关专业的本科生与课程设计学习者提供一套基于UNet的遥感图像语义分割完整毕业设计项目涵盖从数据制作到模型训练、预测与论文撰写的全流程适合作为毕业设计、期末大作业或课程设计参考难度适中。压缩包共68个文件约46.94MB包含6个Python源码文件与3个Jupyter Notebook覆盖数据生成、模型定义、训练与预测脚本另有32张png与6个svg用于结果图和网络结构展示5个tex及bib、pdf构成毕业论文正文与参考文献并附字体、说明文档等辅助材料。目前已有369人学习下载说明该方案在同类选题中具有一定参考价值。读者可据此获得可本地运行的UNet分割代码、遥感数据集制作流程、训练与推理示例以及配套论文的章节结构与图表组织方式便于快速搭建自己的实验框架并完成写作。1. 从一张遥感图到毕业设计UNet 语义分割到底在做什么遥感图像语义分割这件事说白了就是给卫星或无人机拍下来的地表图像做逐像素分类这张图里哪些像素是建筑、哪些是道路、哪些是水体、哪些是植被。和普通图像分类只输出一个标签不同语义分割要求每个像素都有归属输出是一张和原图同尺寸的掩膜图。毕业设计里选这个方向好处是任务定义清晰、数据集公开、评价指标成熟坏处是它对显存、标注质量和训练技巧都有实打实的要求不是跑通一个 demo 就能交差的。UNet 之所以成为这个任务的常客是因为它的结构天然适合处理遥感图像的两个痛点一是目标尺度差异大既有大片农田也有细窄乡道二是边界要求高建筑轮廓糊了指标就掉。编码器逐层下采样提取语义解码器逐层上采样恢复分辨率中间的跳跃连接把浅层的高频细节直接送到对应层级这套设计在遥感场景里非常吃香。这篇笔记面向的是正在做或准备做「Python 毕业设计基于 UNet 的遥感图像语义分割」的人我会把数据准备、模型搭建、训练调参、指标验证和论文里该写什么一条线讲清楚让你能照着复现也能看懂自己每一步在干什么。2. 遥感数据集的准备与预处理从原始影像到可训练张量2.1 为什么遥感数据不能直接喂给 UNet遥感图像和自然图像最大的区别在于通道和动态范围。常见的高分影像可能是 RGB 三通道也可能是包含近红外的四通道甚至多光谱的八通道以上像素值往往是 16 位无符号整数直接归一化到 0 到 1 会丢失大量对比度信息。另一个坑是图像尺寸遥感影像动辄几千乘几千像素而 UNet 的下采样层数决定了输入尺寸最好是 2 的整数次幂的倍数常见做法是裁成 256×256 或 512×512 的图块。我一般会先做两件事统计全数据集的像素值分布用百分位裁剪把极端值压掉再做归一化然后按固定步长滑窗裁块裁的时候保证相邻块之间有重叠避免把目标切断。标注掩膜要和影像严格对齐遥感数据里最常见的错误就是影像和标签错位一个像素训练时 loss 会一直震荡下不去。2.2 用 Python 做滑窗裁块与数据集划分下面这段代码是我常用的裁块脚本输入是原始影像和对应掩膜输出是规整的图块和一份划分清单。import os import numpy as np import cv2 from sklearn.model_selection import train_test_split def sliding_crop(img, mask, patch256, stride128): 滑窗裁块stride 小于 patch 保证重叠 h, w img.shape[:2] patches [] for y in range(0, h - patch 1, stride): for x in range(0, w - patch 1, stride): img_p img[y:ypatch, x:xpatch] mask_p mask[y:ypatch, x:xpatch] # 过滤掉全背景块减少类别不平衡 if mask_p.sum() 0: continue patches.append((img_p, mask_p)) return patches def normalize_img(img): 按 2% 和 98% 分位裁剪后归一化保留对比度 lo, hi np.percentile(img, (2, 98)) img np.clip(img, lo, hi) img (img - lo) / (hi - lo 1e-6) return img.astype(np.float32) img_dir, mask_dir raw/images, raw/masks all_patches [] for name in os.listdir(img_dir): img cv2.imread(os.path.join(img_dir, name), cv2.IMREAD_UNCHANGED) mask cv2.imread(os.path.join(mask_dir, name), cv2.IMREAD_GRAYSCALE) img normalize_img(img) all_patches.extend(sliding_crop(img, mask)) train_p, val_p train_test_split(all_patches, test_size0.2, random_state42) print(f训练块 {len(train_p)}验证块 {len(val_p)})这段代码里patch控制图块大小stride控制重叠程度步长越小样本越多但冗余也越大我一般取 patch 的一半。normalize_img里的百分位裁剪是关键遥感影像常有云层或异常高亮不做裁剪归一化后大部分像素会挤在很小的区间里。mask_p.sum() 0这行过滤全背景块遥感数据里背景占比往往超过七成不过滤的话模型会倾向于全预测背景。2.3 类别不平衡与数据增强的取舍遥感分割的类别极不平衡道路、建筑这类目标可能只占几个百分点。常见做法是给损失函数加权或者用带权重的交叉熵。数据增强方面水平翻转、垂直翻转、90 度旋转对遥感图像是安全的因为地物没有固定的上下方向但颜色抖动要慎用遥感影像的波段反射率是有物理意义的乱调颜色会让模型学到错误的纹理关联。我一般只做几何增强最多加一点轻微的高斯噪声。提示划分数据集时按整幅大图划分不要按裁好的图块随机划分否则同一幅图的相邻块会同时出现在训练集和验证集里验证指标虚高答辩时被问到就尴尬了。3. UNet 模型搭建编码器、解码器与跳跃连接的工程实现3.1 为什么选 UNet 而不是 DeepLab 或 SegFormer毕业设计选模型要平衡效果、显存和可解释性。DeepLab 系列依赖空洞卷积和 ASPP 模块结构复杂调参空间大SegFormer 基于 Transformer效果好但训练需要更多数据和算力本科阶段的显卡不一定扛得住。UNet 结构对称、代码量小、每一层的作用都能讲清楚论文里画结构图和写原理都方便。更重要的是UNet 的跳跃连接在遥感场景里对边界恢复帮助明显这是有实验支撑的。当然 UNet 也有短板下采样会丢失小目标信息对细窄道路和独立小建筑不友好。改进方向通常是加注意力模块、换更强的骨干网络、或者在跳跃连接上加门控。这些改进点正好是论文里可以展开写的地方。3.2 用 PyTorch 实现一个可训练的 UNet下面是我常用的 UNet 实现双卷积块加最大池化下采样转置卷积上采样跳跃连接用通道拼接。import torch import torch.nn as nn class DoubleConv(nn.Module): def __init__(self, in_ch, out_ch): super().__init__() self.net 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.net(x) class UNet(nn.Module): def __init__(self, in_ch3, num_classes5, base64): super().__init__() # 编码器 self.enc1 DoubleConv(in_ch, base) self.enc2 DoubleConv(base, base*2) self.enc3 DoubleConv(base*2, base*4) self.enc4 DoubleConv(base*4, base*8) self.pool nn.MaxPool2d(2) # 瓶颈 self.bottleneck DoubleConv(base*8, base*16) # 解码器 self.up4 nn.ConvTranspose2d(base*16, base*8, 2, stride2) self.dec4 DoubleConv(base*16, base*8) self.up3 nn.ConvTranspose2d(base*8, base*4, 2, stride2) self.dec3 DoubleConv(base*8, base*4) self.up2 nn.ConvTranspose2d(base*4, base*2, 2, stride2) self.dec2 DoubleConv(base*4, base*2) self.up1 nn.ConvTranspose2d(base*2, base, 2, stride2) self.dec1 DoubleConv(base*2, base) self.out nn.Conv2d(base, num_classes, 1) def forward(self, x): e1 self.enc1(x) e2 self.enc2(self.pool(e1)) e3 self.enc3(self.pool(e2)) e4 self.enc4(self.pool(e3)) b self.bottleneck(self.pool(e4)) d4 self.dec4(torch.cat([self.up4(b), e4], dim1)) d3 self.dec3(torch.cat([self.up3(d4), e3], dim1)) d2 self.dec2(torch.cat([self.up2(d3), e2], dim1)) d1 self.dec1(torch.cat([self.up1(d2), e1], dim1)) return self.out(d1)base控制第一层通道数显存不够就降到 32效果会掉一点但能跑起来。num_classes要和你标注的类别数一致背景也算一类。torch.cat那几行就是跳跃连接把解码器上采样的结果和编码器同层级的特征在通道维拼接。转置卷积会有棋盘格伪影介意的话可以换成双线性插值加卷积的组合。3.3 损失函数与评价指标的选型遥感分割常用交叉熵加 Dice 损失的组合。交叉熵管像素级分类Dice 管区域重叠度两者互补。如果类别极不平衡可以给交叉熵加权重权重取类别频率的倒数。评价指标方面mIoU 是标配但遥感场景里我还建议看 F1 和召回率因为漏检建筑和误检建筑的代价不一样论文里把这两个指标都列出来会显得更专业。def dice_loss(pred, target, eps1e-6): pred 是 softmax 后的概率target 是 one-hot inter (pred * target).sum(dim(2, 3)) union pred.sum(dim(2, 3)) target.sum(dim(2, 3)) return 1 - ((2 * inter eps) / (union eps)).mean() # 组合损失 ce nn.CrossEntropyLoss(weightclass_weights) loss ce(logits, mask) dice_loss(torch.softmax(logits, 1), onehot_mask)class_weights按训练集里每个类别的像素占比算占比越低权重越高。Dice 损失对空掩膜要加eps防止除零这个细节不注意训练中途会出 NaN。4. 训练、调参与验证让 UNet 在遥感数据上真正收敛4.1 训练循环里必须盯住的几个量训练脚本本身不复杂难的是判断模型有没有在正常学习。我一般会同时记录训练 loss、验证 loss、验证 mIoU 和学习率。如果训练 loss 下降但验证 loss 上升说明过拟合要加增强或加 dropout如果两个都不降先检查学习率是不是太大或者数据归一化有没有做对。遥感数据里还有一个隐蔽问题如果某类样本极少模型可能直接放弃这一类表现为 mIoU 卡在某个值不动这时候要回头看类别权重和采样策略。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() for img, mask in train_loader: img, mask img.cuda(), mask.cuda() logits model(img) loss ce(logits, mask) dice_loss(torch.softmax(logits, 1), onehot(mask)) optimizer.zero_grad() loss.backward() optimizer.step() scheduler.step() # 每个 epoch 后在验证集上算 mIoU miou evaluate(model, val_loader, num_classes) print(fepoch {epoch}, val mIoU {miou:.4f})AdamW 的weight_decay设 1e-4 是比较稳的起点余弦退火让学习率平滑下降比固定学习率更容易收敛到好的解。T_max设成总 epoch 数。验证集评估时记得model.eval()并关掉梯度否则显存会爆。4.2 学习率、批大小与显存的三角关系这三个参数互相牵制。批大小受显存限制256×256 的图块、base64 的 UNet8G 显存大概能跑 batch size 8。批大小小了梯度噪声大可以适当降低学习率或者用梯度累积。学习率太大 loss 会震荡甚至发散太小收敛慢1e-3 配 AdamW 是常见起点如果 loss 前几个 epoch 就飙到 NaN先降到 1e-4 试试。混合精度训练能省显存但遥感数据里有些操作对精度敏感开了之后要验证指标有没有掉。4.3 验证阶段怎么算 mIoU 才不出错mIoU 的计算有个容易翻车的地方混淆矩阵的累加。正确做法是把整个验证集的预测和标签逐像素累加到一个 num_classes × num_classes 的矩阵里最后一次性算 IoU而不是每个 batch 算完再平均。后者会让小 batch 的噪声放大指标不可比。def evaluate(model, loader, num_classes): model.eval() conf np.zeros((num_classes, num_classes), dtypenp.int64) with torch.no_grad(): for img, mask in loader: pred model(img.cuda()).argmax(1).cpu().numpy() mask mask.numpy() for p, t in zip(pred.flatten(), mask.flatten()): conf[t, p] 1 ious [] for i in range(num_classes): tp conf[i, i] fp conf[:, i].sum() - tp fn conf[i, :].sum() - tp ious.append(tp / (tp fp fn 1e-6)) return np.mean(ious)conf[t, p]的行是真实类别列是预测类别别搞反了。1e-6防止某类完全没出现时除零。如果某一类 IoU 明显低回去看这一类在训练集里的样本量和权重设置。5. 避坑与排查遥感 UNet 训练里最常见的五个翻车现场5.1 现象loss 一直不降mIoU 卡在 0.1 左右原因通常是数据归一化没做对或者标签的像素值不是从 0 开始的连续整数。遥感掩膜有时候用 0、255 表示两类直接送进 CrossEntropyLoss 会报错或者学出奇怪的结果。解决办法是先np.unique(mask)看一眼标签值用映射表转成 0 到 num_classes-1 的连续整数再确认影像归一化后的数值范围在 0 到 1 之间。5.2 现象验证 mIoU 比训练 mIoU 还高这基本可以断定是数据泄漏。最常见的是按图块随机划分数据集同一幅大图的相邻块同时进了训练和验证。解决办法是按整幅图划分或者按地理位置分块划分。另一个可能是验证集里背景占比特别高模型全预测背景也能拿到不错的 mIoU这时候要看各类别的 IoU 而不是只看平均值。5.3 现象训练到一半 loss 突然变成 NaN原因多半是 Dice 损失里除了零或者学习率太大导致梯度爆炸。解决办法是给 Dice 的分母加eps加梯度裁剪torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)并把学习率降一个数量级。混合精度训练下 NaN 更常见可以先关掉 AMP 排查。5.4 现象建筑边界预测得毛毛糙糙指标上不去这是 UNet 下采样丢细节的典型表现。可以尝试三个方向把输入图块从 256 提到 512减少下采样对边界的影响在跳跃连接上加注意力门控让解码器更关注边界区域后处理加条件随机场或者简单的形态学闭运算把碎斑连起来。论文里可以把这几种改进做消融对比是很实在的实验内容。5.5 现象换了台机器或换了 CUDA 版本就跑不起来深度学习环境对版本很敏感。PyTorch、CUDA、cuDNN 三者版本要匹配显卡驱动也要够新。我一般会在论文附录里写清楚环境配置用conda建独立环境把torch.__version__和torch.version.cuda打印出来存档。换机器时先跑一个最小测试脚本确认 GPU 可用再跑完整训练别一上来就开大任务。6. 从跑通到写进论文消融实验、可视化与可复现的工程习惯模型跑通只是起点毕业设计要拿得出手还得有实验设计和结果呈现。我一般会做三组对比基线 UNet、加了注意力模块的 UNet、换了损失函数的 UNet在同一个验证集上比 mIoU、F1 和参数量。消融实验不用多但每一组都要有明确的变量和可解释的结论这样论文的第四章才有东西写。可视化方面除了常规的预测掩膜对比图我建议加一张错误分布图把预测错误的像素在原图上标红一眼就能看出模型是在边界出错还是在某一类上系统性出错。这张图在答辩时非常有用评委一看就知道你对自己的模型有深入理解。论文框架上第二章讲 UNet 原理和遥感分割背景第三章讲数据和方法第四章讲实验和结果第五章讲结论和不足这个结构稳当且不容易被挑毛病。代码工程习惯上我会把配置项全部抽到一个config.yaml里包括数据路径、patch 大小、batch size、学习率、epoch 数训练脚本只读配置不写死参数。每次实验用一个独立的输出目录存模型权重、训练日志和验证指标。这样回头补实验或者复现某次结果时不用靠记忆也避免了「上次那个效果好的模型参数是多少来着」这种血泪追问。随机种子要固定torch.manual_seed、np.random.seed和 DataLoader 的worker_init_fn都要设不然两次跑出来的结果对不上论文里的数字就没法复现。最后一个习惯训练日志里把每个 epoch 的验证指标和当时的学习率一起记下来画成曲线放进论文。很多人只放 loss 曲线其实 mIoU 曲线更能说明模型什么时候开始过拟合。如果验证 mIoU 在第 30 个 epoch 达到峰值然后下降那最终模型就该取第 30 个 epoch 的权重而不是最后一个。这个细节看着小但能体现你对训练过程的掌控也是我自己踩过坑之后才养成的习惯。希望帮到你。本文还有配套的精品资源点击获取