简介这份资源是面向计算机、人工智能、通信工程等专业学生与教师的高分毕业设计项目包主题为基于Python与UNet网络的遥感图像语义分割已通过导师评审并取得95分答辩成绩。包内共69个文件约46.93MB涵盖6个Python源码文件、3个Jupyter Notebook实验脚本、5个LaTeX论文源文件及配套PDF论文、32张结果图与6个矢量图另附部署脚本、字体与参考文献等资料结构完整、开箱即用。项目包含数据制作、模型搭建、训练与预测全流程代码并配有毕业论文各章节源码便于读者理解UNet在遥感影像分割中的实现细节与实验设计。目前已有189人学习下载适合作为毕业设计、课程设计或项目立项的参考模板也可在此基础上修改扩展实现其他分割任务。1. 遥感语义分割毕设从 UNet 跑通到论文可复现的那条线遥感图像语义分割这件事真正卡住大多数人的不是模型结构而是从拿到数据到跑出一张能写进论文的分割图之间那条又长又碎的工程链路。毕业设计选这个方向通常意味着你手里会有一个打包好的项目Python 写的 UNet、一份论文源码、一份部署文档外加全部数据资料。听起来很完整但真上手时你会发现数据怎么组织、标签怎么对齐、训练多久收敛、显存不够怎么办、推理结果怎么评估这些才是决定你能不能按时交稿的东西。这篇笔记就按一线做项目的顺序把基于 Python 和 UNet 的遥感图像语义分割从环境、数据、训练到部署和论文复现整条线拆开讲适合正在做毕设、需要一套能跑通又能写进论文的落地方案的读者。遥感图像和自然图像最大的差别在于同一类地物在不同季节、不同分辨率、不同传感器下差异极大而不同类地物又可能长得很像。UNet 之所以在这个任务里长期被当作基线是因为它的编码器-解码器加跳跃连接结构能在下采样丢空间信息之后靠跳跃连接把浅层的高分辨率特征接回来对小目标地物和边界比较友好。毕设里选它不是因为它最强而是因为它足够稳、代码足够短、论文里好解释改进空间也清晰。接下来我会按「先立住原理和选型再动手复现最后讲坑和进阶」的顺序展开中间每一步都尽量给到能直接抄的参数和命令。2. UNet 在遥感分割里的结构选型与数据组织2.1 为什么遥感分割的基线还是 UNet遥感语义分割的输出是逐像素分类输入往往是大尺寸影像比如 512×512 甚至更大。UNet 的下采样路径负责提取语义上采样路径负责恢复分辨率跳跃连接把编码器每一级的特征拼到解码器对应层。这个设计对遥感场景有两个直接好处一是建筑物、道路这类有明确边界的类别边界定位更准二是小目标比如零星植被或小型水体不会在下采样过程中被彻底抹掉。选型时要清楚几个边界。UNet 原始结构参数量在 7M 到 30M 之间取决于通道基数。遥感数据集通常类别不均衡背景占比高直接训练会让模型偏向多数类。常见做法是在损失函数上做文章比如交叉熵加 Dice 的组合或者用带权交叉熵。另一个边界是输入尺寸遥感影像如果直接整图送入显存吃不消所以必须切块。切块大小和重叠度会直接影响边界拼接效果这个后面会细讲。我一般会把 UNet 当作「可控基线」先让它跑出一个能看的 mIoU再谈改进。改进方向常见的有换编码器为 ResNet 或 EfficientNet、加注意力模块、改跳跃连接为相加或门控。但毕设阶段先把基线跑稳比堆模块重要得多。2.2 数据目录怎么摆才能让训练脚本直接读遥感数据集的常见格式是影像和标签分开存放标签是单通道的索引图像素值 0、1、2 对应不同类别。训练脚本一般按文件名配对读取。一个能直接跑的组织方式如下dataset/ ├── train/ │ ├── images/ # 训练影像png 或 tif │ └── masks/ # 对应标签单通道索引图 ├── val/ │ ├── images/ │ └── masks/ └── test/ ├── images/ └── masks/影像和标签必须同名比如001.png对应001.png。如果标签是彩色图需要先转成索引图否则训练时类别数会对不上。转换时要注意调色板顺序很多公开数据集用固定颜色映射转错会导致类别整体偏移。import numpy as np from PIL import Image # 将彩色标签转为单通道索引图 def color_to_index(mask_path, color_map): mask np.array(Image.open(mask_path).convert(RGB)) index np.zeros(mask.shape[:2], dtypenp.uint8) for idx, color in enumerate(color_map): match np.all(mask color, axis-1) index[match] idx return index # color_map 顺序必须和训练配置里的类别顺序一致 color_map [(0, 0, 0), (255, 0, 0), (0, 255, 0), (0, 0, 255)] index color_to_index(dataset/train/masks/001.png, color_map) Image.fromarray(index).save(dataset/train/masks/001_index.png)这段代码的逻辑是逐颜色匹配把 RGB 标签映射成 0 到 N-1 的索引。参数color_map的顺序就是类别编号顺序必须和后面训练时的num_classes以及损失函数里的类别权重对应。如果某个像素不属于任何颜色会保持 0所以背景色一定要放在第一个。转换完建议抽查几张用np.unique看索引值是否落在预期范围内。2.3 切块与增强遥感数据绕不开的两步遥感影像尺寸大直接训练不现实。切块时有两个参数最关键块大小和重叠度。块大小一般取 256 或 512取决于显存。重叠度取 0.25 到 0.5太小会导致边界处拼接有缝太大则样本冗余。切块后要做数据增强遥感常用的有随机翻转、旋转 90 度、颜色抖动。注意旋转 90 度对标签也要同步旋转否则影像和标签会错位。import random import numpy as np def random_augment(image, mask): # 随机水平翻转 if random.random() 0.5: image np.fliplr(image) mask np.fliplr(mask) # 随机垂直翻转 if random.random() 0.5: image np.flipud(image) mask np.flipud(mask) # 随机旋转 90 度的整数倍 k random.randint(0, 3) image np.rot90(image, k) mask np.rot90(mask, k) return image.copy(), mask.copy()增强函数里每一步都对影像和标签做同样操作这是遥感分割最容易翻车的地方之一。颜色抖动只对影像做不能动标签。另外旋转后如果块大小不是正方形要重新裁剪回固定尺寸。实际训练时建议把增强写在 Dataset 类里每个 epoch 随机应用而不是提前生成增强后的文件否则数据量会膨胀好几倍。3. 用 Python 把 UNet 训练跑起来的最小闭环3.1 环境与依赖别在版本上浪费时间遥感分割项目常见依赖是 PyTorch、OpenCV、NumPy、Pillow、tqdm。Python 版本建议 3.8 到 3.10太新的版本有时第三方库轮子不全。安装时用国内源能省不少时间。下面是一套能直接用的安装命令# 创建虚拟环境避免污染系统 Python python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate # 用国内源安装依赖 pip install torch torchvision -i https://pypi.tuna.tsinghua.edu.cn/simple pip install opencv-python numpy pillow tqdm -i https://pypi.tuna.tsinghua.edu.cn/simple装完后先验证 PyTorch 能不能识别 GPUimport torch print(torch.__version__) print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0) if torch.cuda.is_available() else CPU only)如果cuda.is_available()返回 False先检查显卡驱动和 CUDA 版本是否匹配再检查安装的 torch 是不是 CPU 版本。这一步不解决后面训练会慢到无法接受。CPU 也能跑但遥感数据量大一个 epoch 可能要几十分钟毕设时间紧的话不建议。3.2 UNet 模型定义与关键参数下面是一个精简版 UNet通道基数设为 64适合 256×256 输入和 4 到 8 类分割任务。代码里保留了跳跃连接和双卷积块去掉了不必要的封装方便直接改。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_classes4, base64): super().__init__() # 编码器 self.down1 DoubleConv(in_ch, base) self.down2 DoubleConv(base, base * 2) self.down3 DoubleConv(base * 2, base * 4) self.down4 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.conv4 DoubleConv(base * 16, base * 8) self.up3 nn.ConvTranspose2d(base * 8, base * 4, 2, stride2) self.conv3 DoubleConv(base * 8, base * 4) self.up2 nn.ConvTranspose2d(base * 4, base * 2, 2, stride2) self.conv2 DoubleConv(base * 4, base * 2) self.up1 nn.ConvTranspose2d(base * 2, base, 2, stride2) self.conv1 DoubleConv(base * 2, base) self.out nn.Conv2d(base, 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)参数说明in_ch是输入通道RGB 影像为 3多光谱可以改成对应波段数num_classes是类别数必须和标签索引最大值加一一致base是通道基数显存不够就降到 32但太小会影响精度。跳跃连接用torch.cat拼接拼接后通道数翻倍所以后面的DoubleConv输入通道要对应。如果输入尺寸不是 16 的倍数上采样和拼接时尺寸会对不上建议切块时取 256 或 512。3.3 训练循环与损失函数选择遥感分割类别不均衡是常态损失函数建议用交叉熵加 Dice 的组合。Dice 对前景类别更敏感能缓解背景主导的问题。import torch.nn.functional as F def dice_loss(pred, target, num_classes): # pred: [B, C, H, W] logits, target: [B, H, W] pred F.softmax(pred, dim1) target_onehot F.one_hot(target, num_classes).permute(0, 3, 1, 2).float() intersection (pred * target_onehot).sum(dim(2, 3)) union pred.sum(dim(2, 3)) target_onehot.sum(dim(2, 3)) dice (2 * intersection 1e-6) / (union 1e-6) return 1 - dice.mean() def combined_loss(pred, target, num_classes): ce F.cross_entropy(pred, target) dc dice_loss(pred, target, num_classes) return ce dc训练循环里几个关键参数学习率初始 1e-3用 Adam 或 SGDbatch size 根据显存取 4 到 16。每个 epoch 后在验证集上算 mIoU保存最好的权重。import torch.optim as optim from tqdm import tqdm device torch.device(cuda if torch.cuda.is_available() else cpu) model UNet(in_ch3, num_classes4, base64).to(device) optimizer optim.Adam(model.parameters(), lr1e-3) scheduler optim.lr_scheduler.StepLR(optimizer, step_size20, gamma0.5) for epoch in range(100): model.train() for image, mask in tqdm(train_loader): image, mask image.to(device), mask.to(device) optimizer.zero_grad() pred model(image) loss combined_loss(pred, mask, num_classes4) loss.backward() optimizer.step() scheduler.step() # 验证和保存逻辑按需补充学习率调度用 StepLR每 20 个 epoch 降一半适合训练周期在 100 左右的情况。如果 loss 震荡厉害先把学习率降到 5e-4。验证时记得把模型切到eval()并关闭梯度否则显存会持续增长。4. 推理、评估与部署文档里最容易缺的部分4.1 大图推理与重叠拼接训练时是切块推理时如果直接对整图预测显存会爆。常见做法是滑窗推理窗口大小和训练一致重叠度取 0.5每个像素取多次预测的平均或投票。import numpy as np import torch def sliding_inference(model, image, window256, stride128, 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) for y in range(0, h, stride): for x in range(0, w, stride): y2 min(y window, h) x2 min(x window, w) y1 max(0, y2 - window) x1 max(0, x2 - window) patch image[y1:y2, x1:x2] tensor torch.from_numpy(patch).permute(2, 0, 1).float().unsqueeze(0) / 255.0 with torch.no_grad(): pred torch.softmax(model(tensor.to(device)), dim1)[0].cpu().numpy() prob_map[:, y1:y2, x1:x2] pred count_map[y1:y2, x1:x2] 1 prob_map / np.maximum(count_map, 1) return np.argmax(prob_map, axis0)参数stride控制重叠程度取窗口一半时重叠 50%边界过渡最自然。count_map用来归一化避免边缘区域因为预测次数少而概率偏低。推理完记得把结果保存成索引图方便后续算指标和可视化。4.2 评估指标mIoU 和混淆矩阵怎么算论文里最常报的是 mIoU 和各类 IoU。计算时先累加混淆矩阵再按类别求交并比。def compute_confusion(pred, target, num_classes): mask (target 0) (target num_classes) hist np.bincount( num_classes * target[mask].astype(int) pred[mask], minlengthnum_classes ** 2 ).reshape(num_classes, num_classes) return hist def miou_from_confusion(hist): iou np.diag(hist) / (hist.sum(axis1) hist.sum(axis0) - np.diag(hist) 1e-6) return np.nanmean(iou), iou混淆矩阵的行是真实类别列是预测类别。nanmean是为了跳过没有出现的类别否则会拉低整体指标。论文里除了 mIoU建议把各类 IoU 也列出来尤其是小目标类别这样能体现模型的实际表现而不是只报一个平均数。4.3 部署文档该写什么才不算糊弄部署文档不是把训练脚本再抄一遍而是要写清楚别人拿到项目后怎么复现。至少包含环境版本、数据目录结构、训练命令、推理命令、权重文件位置、预期指标。如果项目里有可视化脚本也要写清楚输入输出。常见做法是提供一个requirements.txt和一个README把关键命令按顺序列出来。部署文档里最容易缺的是数据预处理步骤比如标签转换和切块这两步不写清楚别人拿到数据也跑不起来。5. 避坑与排查遥感 UNet 训练里那些血泪经验5.1 现象训练 loss 一直不降mIoU 卡在很低的值原因通常是标签索引和类别数不匹配或者标签是彩色图没转索引。解决方法是先抽查标签的np.unique确认像素值范围。如果标签是 0 和 255而num_classes设成 2模型会把 255 当成越界类别loss 计算直接出错或异常。另一个原因是学习率太大导致模型在初期就发散。先把学习率降到 1e-4 试一个 epoch看 loss 是否下降。5.2 现象验证集 mIoU 比训练集低很多这是过拟合的典型表现。遥感数据集如果样本少过拟合会很快出现。解决方法是加数据增强、加权重衰减、或者减少模型通道基数。另一个容易被忽略的点是训练集和验证集如果来自同一区域空间相关性会导致验证指标虚高最好按区域划分而不是随机划分。如果验证集 mIoU 突然掉到 0检查验证时是否忘了model.eval()BatchNorm 在训练模式下会用当前 batch 的统计量导致结果不稳定。5.3 现象推理结果在块边界有明显接缝原因是滑窗推理时重叠度不够或者拼接时直接覆盖而没有做平均。解决方法是把stride降到窗口的一半并用概率图累加后归一化。如果接缝仍然明显检查训练时的切块方式和推理时是否一致训练时如果做了归一化推理时也要做同样的归一化。另一个隐藏原因是卷积的 padding 模式如果训练和推理用了不同的 padding边界特征会不一致。5.4 现象显存不够batch size 降到 1 还是爆先确认输入尺寸是不是太大256×256 配 base64 通常 8G 显存够用。如果不够把 base 降到 32或者用混合精度训练。混合精度在 PyTorch 里用torch.cuda.amp就能开能省将近一半显存。另外检查是否有梯度累积之外的张量没释放比如在验证时忘了torch.no_grad()导致计算图一直保留。5.5 现象论文里的指标和代码跑出来的对不上常见原因是评估时用了不同的忽略类别设置或者混淆矩阵计算时把背景也算进去了。论文里如果写了忽略背景代码里也要对应忽略。另一个原因是数据划分不同论文用的测试集可能和代码里的不一样。复现时先固定随机种子再对齐数据划分最后对齐评估脚本。如果指标差异在 1% 以内通常是随机性导致不用太纠结。6. 从跑通到写进论文改进方向与复现验证的具体技巧6.1 改进 UNet 的三个低风险方向毕设里改进模型最怕改完指标反而降了。低风险的方向有三个换编码器、加注意力、改损失函数。换编码器可以把 UNet 的下采样部分换成预训练的 ResNet34 或 EfficientNet-B0利用 ImageNet 预训练权重加速收敛。加注意力可以在跳跃连接处加 SE 模块或 CBAM代码量小解释起来也容易。改损失函数可以试试 Focal Loss 加 Dice对类别不均衡更友好。这三个方向里换编码器通常提升最明显但要注意预训练权重输入通道如果是 3多光谱数据需要改第一层卷积。# 在跳跃连接后加 SE 注意力的简化写法 class SEBlock(nn.Module): def __init__(self, ch, reduction16): super().__init__() self.fc nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Conv2d(ch, ch // reduction, 1), nn.ReLU(inplaceTrue), nn.Conv2d(ch // reduction, ch, 1), nn.Sigmoid(), ) def forward(self, x): return x * self.fc(x)SE 模块放在DoubleConv之后对通道做重标定。reduction控制压缩比例16 是常用值太小会增加参数太大则效果不明显。加完后重新训练对比基线 mIoU如果提升不到 1%可能只是随机波动建议多跑几个种子取平均。6.2 复现验证怎么确认自己的结果可信复现验证的核心是固定变量。先固定随机种子包括 Python、NumPy、PyTorch 的种子。然后固定数据划分把训练、验证、测试的文件列表存成 txt每次训练都读同一份。接着固定评估脚本确保 mIoU 的计算方式和论文一致。最后跑三次不同种子看指标的波动范围。如果波动在 0.5% 以内说明结果稳定如果波动很大检查数据增强里有没有引入不可控的随机性。import random import numpy as np import torch def set_seed(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic True torch.backends.cudnn.benchmark Falsecudnn.deterministic设为 True 会让训练变慢但结果可复现。如果追求速度可以设为 False但每次结果会有细微差异。论文里如果强调可复现建议打开这个开关。6.3 一个具体技巧用测试时增强再涨一点指标测试时增强是在推理阶段对同一张图做多次变换比如翻转和旋转把预测结果平均后再取 argmax。这个方法不用重新训练代码量小通常能涨 0.5% 到 1.5% 的 mIoU。实现时把滑窗推理包一层对每个 patch 做 4 次或 8 次变换预测后反变换回原方向再累加概率。def tta_predict(model, patch, num_classes4): preds [] for k in range(4): aug np.rot90(patch, k).copy() tensor torch.from_numpy(aug).permute(2, 0, 1).float().unsqueeze(0) / 255.0 with torch.no_grad(): pred torch.softmax(model(tensor.to(device)), dim1)[0].cpu().numpy() pred np.rot90(pred, -k, axes(1, 2)) preds.append(pred) return np.mean(preds, axis0)这个技巧的代价是推理时间变成原来的 4 倍如果测试集不大完全值得。注意反变换时np.rot90的 axes 参数要对应概率图的维度否则方向会错。做完 TTA 后把结果和基线对比如果提升明显可以在论文里单独写一小节。我自己做这类项目时最大的教训是别一上来就改模型。先把基线跑通把数据、评估、复现这条线理顺再动结构。很多时候指标上不去问题不在模型而在标签或者评估脚本。希望这些步骤能帮你少走点弯路顺利把毕设跑完。本文还有配套的精品资源点击获取
