简介这份资源面向计算机、人工智能及相关专业的学生与开发者提供一套基于U-Net网络的遥感图像语义分割系统完整实现可用于毕业设计、课程设计或期末大作业。项目包含可运行的Python源码与配套论文源码经本地编译验证评审得分达98分难度适中适合具备一定深度学习基础的学习者参考。压缩包共68个文件约46.93MB涵盖6个py脚本与3个ipynb笔记本分别负责模型定义、数据处理、训练与预测流程另有32张png与6个svg用于结果展示5个tex及bib、pdf构成论文排版素材并附字体与说明文档目录结构清晰。目前已有219人学习下载。读者可据此掌握U-Net在遥感影像上的编解码结构、数据集制作、训练调参与预测可视化全流程并借助论文与代码对照理解语义分割的关键实现细节快速完成项目复现与二次开发。1. 遥感图像语义分割毕设从 U-Net 跑通到论文能写清楚遥感图像语义分割这个方向每年毕业季都会被大量计算机、测绘、遥感相关专业的同学翻出来做毕设。原因很直接数据公开、任务定义清晰、模型结构不算复杂而且 U-Net 这种编码器-解码器架构在遥感场景里确实能跑出看得过去的结果。但真正动手时问题往往不在模型本身而在“遥感图像标注怎么做”“语义分割数据集制作从哪下手”“Python 环境怎么配才不翻车”这些前置环节。这篇笔记按一线做过的路径拆一遍U-Net 为什么适合遥感语义分割、数据怎么准备、代码怎么跑、参数怎么调、论文里哪些图必须放。适合正在做基于 Python 的毕业设计、需要一套能复现的遥感图像语义分割系统的同学也适合想快速判断这个方向值不值得投入的人。2. U-Net 在遥感语义分割里的选型理由与最小可跑通路径遥感图像和自然图像有个本质差别同一张图里地物尺度差异极大。一条乡村道路可能只占几十个像素一片农田可能覆盖半张图。U-Net 的跳跃连接把编码器的高分辨率特征直接送到解码器小目标不会被深层下采样彻底抹掉这是它在遥感语义分割里长期被当作 baseline 的核心原因。另一个现实因素是数据量毕设能拿到的标注数据通常只有几百到几千张U-Net 在这种规模下比 Transformer 类模型更容易训起来不需要预训练权重也能收敛。2.1 遥感语义分割的任务定义与标签体系遥感语义分割的输出是逐像素类别。常见类别体系包括建筑、道路、水体、植被、裸地、农田。类别数直接决定输出通道数也决定损失函数怎么选。如果类别不均衡严重比如道路像素只占 3%交叉熵会被背景类主导这时候要换 Dice Loss 或带权交叉熵。标签格式常见两种单通道灰度图像素值 0/1/2… 对应类别和 RGB 彩色图。U-Net 训练时通常转成单通道用torchvision.transforms或自定义 Dataset 读入。这里有个容易忽略的点遥感图像标注工具导出的标签边界像素经常有半透明过渡直接读进来会出现 254、128 这种非整数类别值必须在 Dataset 里做一次映射或阈值化。2.2 用 Python 搭出 U-Net 最小训练脚本下面是一个可以直接跑的最小训练循环数据集假设已经整理成images/和masks/两个文件夹文件名一一对应。import os import torch import torch.nn as nn from torch.utils.data import Dataset, DataLoader from PIL import Image import numpy as np from torchvision import transforms class RemoteSensingDataset(Dataset): def __init__(self, img_dir, mask_dir, img_size256): self.img_dir img_dir self.mask_dir mask_dir self.img_size img_size self.names sorted(os.listdir(img_dir)) def __len__(self): return len(self.names) def __getitem__(self, idx): name self.names[idx] img Image.open(os.path.join(self.img_dir, name)).convert(RGB) mask Image.open(os.path.join(self.mask_dir, name)).convert(L) img img.resize((self.img_size, self.img_size)) mask mask.resize((self.img_size, self.img_size), Image.NEAREST) img transforms.ToTensor()(img) mask torch.from_numpy(np.array(mask)).long() # 把非连续标签值映射到 0..num_classes-1 mask torch.clamp(mask, 0, 5) return img, mask class UNet(nn.Module): def __init__(self, in_ch3, num_classes6): super().__init__() def block(i, o): return nn.Sequential( nn.Conv2d(i, o, 3, padding1), nn.BatchNorm2d(o), nn.ReLU(inplaceTrue), nn.Conv2d(o, o, 3, padding1), nn.BatchNorm2d(o), nn.ReLU(inplaceTrue) ) self.enc1 block(in_ch, 64) self.enc2 block(64, 128) self.enc3 block(128, 256) self.pool nn.MaxPool2d(2) self.bottleneck block(256, 512) self.up3 nn.ConvTranspose2d(512, 256, 2, stride2) self.dec3 block(512, 256) self.up2 nn.ConvTranspose2d(256, 128, 2, stride2) self.dec2 block(256, 128) self.up1 nn.ConvTranspose2d(128, 64, 2, stride2) self.dec1 block(128, 64) self.out nn.Conv2d(64, num_classes, 1) def forward(self, x): e1 self.enc1(x) e2 self.enc2(self.pool(e1)) e3 self.enc3(self.pool(e2)) b self.bottleneck(self.pool(e3)) d3 self.dec3(torch.cat([self.up3(b), 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) if __name__ __main__: device torch.device(cuda if torch.cuda.is_available() else cpu) ds RemoteSensingDataset(data/images, data/masks) dl DataLoader(ds, batch_size4, shuffleTrue, num_workers2) model UNet(num_classes6).to(device) opt torch.optim.Adam(model.parameters(), lr1e-3) loss_fn nn.CrossEntropyLoss() for epoch in range(50): model.train() total 0 for img, mask in dl: img, mask img.to(device), mask.to(device) opt.zero_grad() pred model(img) loss loss_fn(pred, mask) loss.backward() opt.step() total loss.item() print(fepoch {epoch}, loss {total/len(dl):.4f})这段代码里几个参数值得单独说。img_size256是显存和精度的折中遥感图像原始尺寸动辄 5000×5000必须切块batch_size4在 8GB 显存下比较稳想加大先看nvidia-sminum_classes6要和标签映射范围一致否则 CrossEntropyLoss 会直接报 index out of range。torch.clamp(mask, 0, 5)是血泪经验很多标注工具导出的 PNG 在边界处有 255 或 254不处理的话训练中途才崩排查起来很费时间。2.3 训练前必须确认的三个数据检查点第一图像和标签文件名是否严格一一对应。遥感数据集经常出现img_001.tif对应img_001_mask.png这种命名脚本里要统一。第二标签像素值分布。用np.unique(np.array(mask))打印一下确认只有 0 到 num_classes-1。第三图像通道数。遥感图像可能是 4 通道RGB近红外convert(RGB)会丢掉近红外如果论文里想用多光谱Dataset 里要改成读 4 通道并调整 U-Net 的in_ch。3. 遥感图像标注与语义分割数据集制作的完整操作链这一章解决“数据从哪来、怎么标、怎么切”的问题。毕设里数据集质量直接决定论文能写多深标注不规范后面调参全是玄学。3.1 公开数据集选择与自有数据补充常见可用的公开遥感语义分割数据集包括 LoveDA、ISPRS Potsdam、DeepGlobe。LoveDA 覆盖城市和农村类别有建筑、道路、水体、植被、裸地、背景比较适合毕设。如果导师要求自有数据可以用无人机或公开卫星图切片再用 QGIS 或 ArcGIS 做初步标注。选择逻辑如果论文重点是模型改进用公开数据集保证可比性如果重点是应用系统自有数据加公开数据混合但要在论文里写清楚混合比例和标注规范。3.2 用 Python 做图像切块与标签同步增强遥感图像太大必须切块。下面脚本把大图和标签同步切成 256×256并做简单增强。import os import numpy as np from PIL import Image def slide_crop(img_path, mask_path, out_img_dir, out_mask_dir, size256, stride128): img np.array(Image.open(img_path)) mask np.array(Image.open(mask_path)) h, w img.shape[:2] idx 0 for y in range(0, h - size 1, stride): for x in range(0, w - size 1, stride): img_crop img[y:ysize, x:xsize] mask_crop mask[y:ysize, x:xsize] # 过滤掉标签全为背景的块减少无效样本 if len(np.unique(mask_crop)) 1 and mask_crop[0,0] 0: continue Image.fromarray(img_crop).save(os.path.join(out_img_dir, f{idx:05d}.png)) Image.fromarray(mask_crop).save(os.path.join(out_mask_dir, f{idx:05d}.png)) idx 1 if __name__ __main__: os.makedirs(crops/images, exist_okTrue) os.makedirs(crops/masks, exist_okTrue) slide_crop(raw/big_image.tif, raw/big_mask.png, crops/images, crops/masks)stride128表示重叠一半增加样本量同时避免边界目标被切断。过滤全背景块能显著降低类别不均衡。如果显存够可以把size提到 512但要注意 U-Net 下采样四次后512 输入的最小特征图是 32×32再小的小目标就丢了。3.3 标签噪声清理与类别映射表标注噪声主要来自三处边界模糊、类别混淆建筑和道路、工具导出伪彩色。处理方式是建一张映射表把原始像素值统一映射到 0 到 N-1。原始值含义映射后0背景0128建筑1255道路264水体3192植被4254边界噪声归入最近类或丢弃映射表写进 Dataset 的__getitem__不要等到训练时再处理。论文里可以把这张表放进“数据预处理”小节审阅老师一看就知道你认真做过数据。4. 训练参数、损失函数与评估指标的调参实战模型能跑通只是起点毕设论文里真正拉开差距的是调参记录和指标分析。4.1 学习率、批大小与优化器的组合选择U-Net 在遥感语义分割里Adam 初始学习率 1e-3 是稳妥起点。如果 loss 震荡降到 3e-4如果收敛太慢用 OneCycleLR 做 warmup。批大小受显存限制但可以用梯度累积模拟大 batch。# 梯度累积示例实际 batch 4累积 4 次等效 batch 16 accum_steps 4 opt.zero_grad() for i, (img, mask) in enumerate(dl): pred model(img.to(device)) loss loss_fn(pred, mask.to(device)) / accum_steps loss.backward() if (i 1) % accum_steps 0: opt.step() opt.zero_grad()注意 loss 要除以累积步数否则梯度会放大。这个技巧在显存不够但想用大 batch 稳定 BatchNorm 时特别有用。4.2 类别不均衡下的损失函数切换遥感数据里道路、水体往往占比低。直接用 CrossEntropyLoss模型会倾向预测背景。切换顺序建议先试带权 CrossEntropy权重按类别频率倒数不行再上 Dice Loss 或 Combo Loss。class DiceLoss(nn.Module): def __init__(self, smooth1e-6): super().__init__() self.smooth smooth def forward(self, pred, target): pred torch.softmax(pred, dim1) target_onehot torch.nn.functional.one_hot(target, num_classespred.shape[1]) target_onehot target_onehot.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 self.smooth) / (union self.smooth) return 1 - dice.mean()Dice Loss 对小类别更友好但训练初期不稳定常见做法是前 10 个 epoch 用 CrossEntropy之后切 Combo0.5 CE 0.5 Dice。4.3 评估指标mIoU、F1 与混淆矩阵怎么放进论文mIoU 是语义分割标配但毕设论文里只放一个 mIoU 数字太单薄。建议同时给出每类 IoU、F1 和混淆矩阵。混淆矩阵能直观看出哪两类容易混比如建筑和道路。用 sklearn 的confusion_matrix加 matplotlib 画热力图论文里放一张就够。指标公式论文里放哪mIoU各类 IoU 平均实验结果主表F12PR/(PR)补充表混淆矩阵逐类统计分析图5. 避坑与排查遥感语义分割毕设里最容易翻车的五件事5.1 现象训练 loss 一直不降输出全是一个类别原因通常是标签映射错误或学习率过大。先打印一个 batch 的标签唯一值确认在 0 到 num_classes-1。如果标签没问题把学习率降到 1e-4 再跑 5 个 epoch 看 loss 是否下降。另一个隐蔽原因是图像归一化没做ToTensor()只把像素转到 0-1如果原始是 16 位遥感图数值范围可能是 0-65535必须手动除以 65535。5.2 现象验证集 mIoU 比训练集低很多过拟合。遥感数据集小的时候尤其明显。解决顺序先加数据增强随机翻转、旋转、色彩抖动再考虑 Dropout 或权重衰减最后才是减小模型。不要一上来就换模型毕设时间有限U-Net 加增强通常够用。5.3 现象预测结果边界锯齿严重上采样用了最近邻或双线性没有可学习参数。把ConvTranspose2d的stride2确认写对或者改用nn.Upsample(scale_factor2, modebilinear)加卷积。另一个原因是输入切块太小边界上下文不足把size从 256 提到 384 试试。5.4 现象训练中途报 CUDA out of memory先降 batch_size再降 img_size。如果都不想降用梯度累积。还要检查 DataLoader 的num_workers设太大反而占内存设成 2 或 4 即可。另外验证阶段记得torch.no_grad()否则显存会持续累积。5.5 现象论文里图表和代码对不上常见于中途改了类别数或数据划分但论文里的表没同步更新。建议在项目根目录放一个config.yaml把 num_classes、img_size、batch_size、学习率都写进去代码读配置论文写配置。这样改一处两边一致。6. 把 U-Net 遥感分割做成可展示系统的进阶技巧毕设如果只交一个训练脚本答辩时容易被问“系统在哪”。常见做法是加一个 Flask 或 Gradio 前端上传遥感图返回分割掩膜叠加图。Gradio 最快十几行代码就能跑。import gradio as gr import torch import numpy as np from PIL import Image model UNet(num_classes6) model.load_state_dict(torch.load(best_unet.pth, map_locationcpu)) model.eval() def predict(img): img img.resize((256, 256)) x torch.from_numpy(np.array(img)).permute(2, 0, 1).float().unsqueeze(0) / 255.0 with torch.no_grad(): out model(x).argmax(dim1).squeeze().numpy() color_map np.array([[0,0,0],[255,0,0],[0,255,0],[0,0,255],[255,255,0],[255,0,255]], dtypenp.uint8) return color_map[out] gr.Interface(fnpredict, inputsgr.Image(), outputsgr.Image()).launch()这段代码把模型输出映射成彩色掩膜答辩演示够用。注意load_state_dict的map_location要写否则在没 GPU 的答辩电脑上会报错。另外Gradio 默认端口可能被占用launch(server_port7861)换一个。论文里系统部分可以放一张界面截图加一段流程说明上传图像 → 预处理 → U-Net 推理 → 掩膜叠加 → 结果下载。不要写太多前端细节重点还是模型和指标。最后说一个我自己的习惯每次跑完实验把配置、命令、mIoU、每类 IoU 记在一个experiments.md里按日期排。写论文时直接翻这个文件比回忆靠谱得多。遥感语义分割毕设不难难的是数据干净、参数有记录、论文能自圆其说。希望帮到你。本文还有配套的精品资源点击获取