简介:本资源是一套完整的本科毕业设计项目,面向计算机、遥感、地理信息等相关专业本科生,聚焦遥感图像语义分割这一典型CV任务,以轻量级但高精度的UNet架构为核心实现端到端像素级分类。压缩包共69个文件,含6个核心Python训练/推理脚本(train.py、predict.py、model.py等)、3个Jupyter Notebook(含数据构建、训练与可视化全流程演示)、5个LaTeX论文章节源码(.tex)及完整PDF毕业论文,辅以SVG流程图、PNG结果示意图和字体/参考文献等支撑文件,整体56.42MB,结构清晰、模块解耦,便于学习理解模型搭建、数据预处理、训练调优与结果评估全链路。已有451人学习下载,源码经本地实测可直接运行,评审得分95分以上,内容由助教审定,涵盖从环境配置、UNet定制化改进到遥感影像(如AerialImage数据集)适配的关键实践细节,是毕业设计选题、课程设计与深度学习入门落地的高可靠性参考方案。 先说结论:这个选题放在本科毕设里,属于“性价比”很高的那一档。UNet原理不复杂,遥感图像语义分割的应用价值又容易讲清楚,不管是做工程实现还是写论文,都有大量现成资料可以借鉴,但又不是简单照搬就能跑通——数据预处理、模型调参、评估指标这些环节,足够让一个认真做的人学到东西,也足够让划水的人暴露问题。如果你正在选毕设题目,或者已经拿到这个项目正在啃源码,这篇内容应该能帮你少走不少弯路。
我按照“从拿到题目到完成答辩”的完整流程来拆解:先讲清楚这个项目到底要做什么、为什么选UNet;然后是一步步的实操过程,包括数据集怎么准备、网络怎么改、训练要注意什么、代码怎么组织;最后是论文写作和答辩的要点,以及一堆只有实际跑过才会踩到的坑。
1. 项目整体设计与需求拆解
1.1 这个毕业设计到底在做什么
遥感图像语义分割,说白了就是把一张卫星或无人机拍下来的图片,按像素分类成不同的地物类别。比如建筑物、道路、水体、植被、农田,每个像素都被打上一个标签。这个任务和普通图像分类最大的区别在于:图像分类只告诉你“这张图里有房子”,而语义分割要告诉你“房子具体在哪个区域、边界在哪里”。
UNet是这个任务里最经典的网络结构之一。它最初是2015年为医学图像分割设计的,但后来被大量用在遥感领域,效果一直很稳。它的名字来源于网络形状——左边编码器逐层下采样提取特征,右边解码器逐步上采样恢复分辨率,中间用跳连把同尺度的特征拼接起来,整个结构像一个U形。
放在毕设的语境下,这个项目的完整闭环是:准备遥感图像数据集 -> 写数据加载和预处理代码 -> 搭建或改进UNet模型 -> 训练并验证 -> 在测试集上评估指标 -> 可视化分割结果 -> 把整个过程写成毕业论文。每一步都有明确产出,非常适合展示“从数据到模型到应用”的完整工程能力。
1.2 为什么选UNet而不是其他分割模型
很多同学会问:DeepLabV3+、PSPNet、SegNet这些不也挺流行吗?为什么偏偏选UNet?
我的看法是,本科毕设的选题标准不应该是“最新”,而应该是“在可控难度内做出可靠结果”。UNet有几个天然优势:
- 结构直观,容易讲清楚。编码器-解码器-跳连这三个核心概念,画一张图就能说明白,答辩时哪怕被问到细节也不会慌。
- 小数据集上表现稳定。遥感数据集不像ImageNet那样动辄百万张,很多公开数据集只有几十到几百张图。UNet的参数量适中,不容易严重过拟合,训练起来也更宽容。
- 改进空间大,方便“加工作量”。这个后面细说。毕设如果想拿到高分,关键在于“比基线模型好一点”,UNet可以挂注意力模块、可以换backbone、可以改用深度可分离卷积,这些都是成熟做法,做出来就是实打实的创新点。
- 参考资料极其丰富。从官方实现到各种复现版本、博客解读、开源项目,几乎你能想到的每个细节都有人踩过坑并留下了记录,对毕设这种有时间限制的场景非常友好。
当然,如果你做的是复杂地物场景、超大尺寸遥感图或者需要实时推理,UNet不一定是最优选择,但在本科毕业设计这个定位下,用它来做“基于遥感图像的语义分割”是合理且稳妥的。
1.3 项目交付物的完整清单
先说清楚你最后要拿出哪些东西。这决定了整个项目的组织方式。
- 可运行的Python源码:包括数据集加载、模型定义、训练脚本、验证脚本、预测脚本、工具函数。不要把所有代码堆在一个文件里,要按功能拆分。
- 训练好的模型权重文件:答辩时一定会有老师要求现场跑一个demo,或者看分割效果图。如果没有训练好的权重,现场从零训练根本来不及。
- 实验记录:包括不同超参数下的训练曲线、损失变化、评估指标对比表。这是论文里“实验分析”章节的素材来源。
- 可视化结果图:原图、标签图、预测图放在一起做对比,最好挑几个“分割效果好”和“分割效果差”的典型例子,分别分析原因。
- 毕业论文:后面专门用一章讲怎么组织内容。
拿到这个项目的时候,不要急着写代码,先把目录结构规划好。我自己的习惯是:
project_root/ ├── data/ # 数据集存放位置 │ ├── images/ │ └── masks/ ├── src/ │ ├── dataset.py # 数据加载与增强 │ ├── model.py # UNet及改进版本 │ ├── train.py # 训练入口 │ ├── predict.py # 推理与可视化 │ └── utils.py # 评估指标、工具函数 ├── weights/ # 保存模型权重 ├── results/ # 训练日志、可视化结果 └── docs/ # 论文及配图素材这个结构不是装样子,而是为了让你在写论文、做实验对比、反复调试时不用在乱糟糟的文件夹里找文件。我在带学弟学妹做项目时反复强调:代码能不能跑是一回事,别人能不能看懂是另一回事,毕设答辩时老师大概率会翻你的代码结构。
2. 遥感数据集准备与预处理实测
2.1 公开遥感数据集怎么选
遥感语义分割的公开数据集不少,但适合做本科毕设的其实有限。我按“下载难度”“标注质量”“类别数量”三个维度对比过常见的几个:
| 数据集 | 场景 | 主要类别 | 图像尺寸 | 适用性评价 |
|---|---|---|---|---|
| Massachusetts Roads | 城市/乡村道路 | 道路、背景(二分类) | 1500x1500 | 结构简单,适合入门 |
| DeepGlobe Land Cover | 卫星图像 | 城市、农业、森林、水体、荒地等 | 2448x2448 | 任务贴近真实应用 |
| ISPRS Potsdam | 航空影像 | 不透水面、建筑、低矮植被、树木、汽车、背景 | 6000x6000 | 分辨率高、类别细 |
| LoveDA | 遥感图像 | 背景、建筑、道路、水体、植被等 | 1024x1024 | 类别多、有城乡差异 |
| GID(武汉大学) | 高分辨率遥感 | 建筑、农田、水域等 | 大尺寸 | 国内数据集,论文常用 |
我比较推荐用DeepGlobe Land Cover或者LoveDA起步,原因是它们已经划分好了train/validation/test,不用自己花时间处理标注数据,而且类别数量适中(5-7类),既不是二分类那种“练手级”任务,也不像ISPRS那样像素级标注极其精细导致模型难收敛。
我的建议是:如果想要高分辨率大图,选ISPRS Potsdam;如果想要省事且视角丰富,选DeepGlobe;如果论文想加“跨区域泛化”这类分析,选LoveDA。不要一开始就追求数据量大,一张遥感图动辄几千像素,直接整图输入GPU显存根本扛不住,后面会讲怎么处理。
2.2 大尺寸遥感图的切块策略
遥感图像和普通照片最大的区别是:尺寸特别大。ISPRS的图是6000x6000,DeepGlobe是2448x2448。如果直接把整张图送进网络,哪怕是最小的UNet也会直接显存溢出。
常规做法是滑窗裁剪。把大图切成若干小patch,常见尺寸是256x256或512x512,相邻patch之间可以设置overlap(重叠率),比如10%-20%的overlap,避免目标物体正好被切在边界上导致分割不连续。
写切块代码的时候需要注意几点:
- 裁切的时候必须同时处理原图和标签图,并且保证二者使用完全相同的坐标偏移。我见过有人分别写了两个循环,结果标签和图像错位,训练时损失死活降不下去。
- patch数量要控制,否则数据量膨胀得很夸张。一张2448x2448的图按512步长切,大约能切出25个patch,一个数据集几百张图就变成上万patch,训练一轮的时间会翻几倍。
- 训练和推理的策略可以不同。训练时用随机裁剪,推理时用滑窗加overlap,最后对重叠区域取平均或投票,能有效减少边界伪影。
如果你用的是现成数据集,比如DeepGlobe,官方一般不提供裁剪后的版本,需要自己写预处理脚本。这个脚本建议保留下来,因为论文里要写“数据预处理流程”,这是可以展开描述的一个小节。
2.3 数据增强:别小看这几个操作
遥感图像分割的数据增强,很多同学直接套用分类任务的套路(随机翻转、随机旋转、色彩抖动),但有几个特殊之处值得注意。
第一,旋转角度要选择90度的倍数。农田、道路、建筑在遥感图里往往有明显的方向性,旋转45度会把“道路”这种直线结构切碎,而且标签是像素级类别,不是矩形框,旋转后需要重新插值,容易造成边缘标签错位。稳妥的组合是:随机水平翻转加随机垂直翻转、旋转90/180/270度、随机裁剪,这三个就够用了。
第二,色彩增强要克制。遥感图像的色调相对稳定,光照变化不像自然图像那么剧烈。把亮度、对比度、饱和度的扰动范围调小,默认0.5的强度在遥感数据上往往过于激进,会让水体看起来像阴影、农田看起来像裸地。
第三,可以用“类别平衡采样”。遥感数据里背景类占比通常很高,道路、水体往往只占几个百分点。如果按原始分布随机采样,模型会严重偏向多数类。一种简单有效的做法是:为每个patch计算标签分布,优先采样那些“包含较多小类别像素”的patch,相当于人为提高少数类的出现频率。这个技巧在论文里写成“基于类别权重的采样策略”,比单纯说“数据不平衡”要高级得多。
2.4 标签编码与类别权重计算
遥感语义分割的标签通常是两种格式:一种是PNG图片,像素值就是类别编号(比如0=背景,1=建筑,2=道路);另一种是RGB彩色图,不同颜色代表不同类别。如果是后者,需要先做一个“颜色到类别ID”的映射表。
这一步有个常见坑:RGB标签图里可能有一些“未标注”像素,颜色值不在映射表里。处理方式是把它们统一归为背景类,或者在计算损失时忽略这些位置的loss。PyTorch的CrossEntropyLoss自带ignore_index参数,正好用来干这个。
类别权重我自己常用的是median frequency balancing,公式是:
w_c = median_freq / freq_c其中freq_c是类别c的像素占比,median_freq是所有类别频率的中位数。算完之后归一化,使得权重之和等于类别数。这样做的效果是:让“出现少”的类别在损失函数里获得更大的权重,但又不至于完全压过多数类。比起直接按比例取倒数,这个方法的数值稳定性更好,不容易出现权重爆炸。
在你的论文实验部分,可以做一个“有/无类别权重”的对比实验,这种对比做起来不费劲,但论文里能多一张表、多一个分析段落,非常划算。
3. UNet模型原理与改进方案落地
3.1 网络结构逐层拆解
UNet的核心思想是“用跳连把浅层细节和深层语义融合起来”。我拆开讲:
- 编码器(左侧):每一层由两个卷积(3x3 + ReLU)加一个2x2最大池化组成,每次池化后特征图尺寸减半、通道数翻倍。常见的初始通道数是64,经过四到五次下采样后,最底层的特征图尺寸只有输入的1/16或1/32,但它包含了最抽象的语义信息——这块区域大概是建筑、是道路、还是水体。
- 瓶颈层(底部):下采样到最深层之后再做两层卷积,是整个网络感受野最大的位置,相当于对整张图的全局理解。
- 解码器(右侧):每一层先做一个上采样(通常是转置卷积或双线性插值),把特征图尺寸翻倍、通道数减半,然后把编码器同层的特征图拼接过来(channel维度),再经过两个卷积处理。
- 输出层:最后一层用1x1卷积把通道数映射为类别数,再用Softmax得到每个像素属于各个类别的概率。
跳连的设计非常精妙。如果没有跳连,解码器只能从瓶颈层的低分辨率特征里恢复细节,道路这种细长结构的边界会非常模糊。有了跳连,解码器在每一层都能直接拿到编码器的同尺度特征,细节信息可以从浅层一路传递到输出端。
3.2 为什么这个结构适合遥感图像
我跑过几次实验后的体会是:UNet适合遥感分割,核心原因是遥感图像中的目标结构非常依赖“多尺度信息”。
一个城市区域,既要有建筑的大块轮廓(需要深层语义),又要有道路的细长连接、屋顶边缘的锐利程度(需要浅层细节)。UNet同时拿两者,跳连把浅层边缘和深层语义结合在一起,这种结构在遥感任务里比单纯的编码器-解码器(比如SegNet)效果好得多。
另外,UNet还有一个被很多人忽略的好处:它对训练数据量的要求没那么苛刻。因为跳连的存在,梯度可以更直接地回传到浅层,网络在小数据集上也能有效训练。遥感数据集的标注成本很高,很多实际项目只有几十张标注图,这种情况下UNet往往是第一个能跑出像样结果的选择。
3.3 三招改进UNet,让论文更有含金量
本科毕设想拿高分,“改进网络结构”是性价比最高的加分项。但千万不要瞎改,改得越复杂,训练越不稳定,答辩越难解释。我推荐三个方向,都是成熟、有效、容易实现的改进:
第一招:把标准卷积替换为深度可分离卷积。
深度可分离卷积把普通卷积分解为深度卷积(每个通道单独做卷积)和逐点卷积(1x1卷积融合通道信息)。参数量和计算量都会下降,尤其在下采样到16倍、32倍时,特征图通道数变成512、1024,这部分的参数节省非常可观。在遥感大图上,这能明显降低显存占用,还能加快训练速度。代价是理论上精度会有轻微下降,但实际在UNet这种本身参数冗余量较大的网络上,精度损失往往很小,有时甚至持平。
这个方向特别适合写进论文,因为你可以做一组对比实验:UNet vs 深度可分离卷积UNet,分别报告参数量、FLOPs、推理时间、mIoU。结果大概率是“精度基本持平,计算量降低XX%”,这个结论本身就是很好的实验分析素材。
第二招:加入注意力门控(Attention Gate)。
注意力门控的核心思想是:解码器在拼接编码器特征之前,先对编码器的特征做一次“重要性筛选”,把与当前解码目标相关的区域保留,不相关的区域抑制。从直觉上理解,就是让网络在恢复高分辨率特征时,主动关注那些真正属于“道路”或“建筑”的区域,而不是把背景信息也一块堆进去。
这个模块的实现代码很短,只要几十行,而且是一个独立的模块,插入UNet的跳连位置即可。它不会破坏原有结构,训练收敛也比较稳定。改进的效果通常体现在边界的清晰度上,尤其是道路、田埂这类细长目标的IoU会有可感知的提升。
第三招:用DiceLoss或FocalLoss替换交叉熵。
这个严格来说不算网络结构改进,但很多人把它和上面的改进混在一起写,效果出奇地好。原因也很直接:遥感数据类别极其不平衡,交叉熵天然偏向高频类别。DiceLoss直接优化“预测区域和真实区域的交集占比”,对少数类更友好;FocalLoss则降低易分类样本的loss权重,让模型更关注难分的边界像素。
如果你时间有限,只能做一个改进,我的建议顺序是:类别权重/损失函数 > 注意力门控 > 深度可分离卷积。因为损失函数的改动最小、收益最明显,而深度可分离卷积虽然计算量降低了,但对mIoU的直接提升不如前两个明显。
4. 核心代码实现与训练全流程
4.1 环境配置与依赖版本
遥感分割跑的是PyTorch生态,我用过一个比较省心的版本组合:
Python 3.8 或 3.9 PyTorch 1.12 或 2.0 CUDA 11.3 或 11.8(按显卡驱动选) torchvision 0.13 或 0.15 opencv-python numpy matplotlib tqdm不推荐用太新的版本,因为很多现成源码和博客例子都是基于旧版本写的,API对不上会浪费大量时间去查文档。这里有一个实测下来很稳的安装方式:先用conda创建独立环境,再通过pip安装PyTorch。建议一定要独立环境,不要直接装在base环境里,不然依赖冲突能把人逼疯。
conda create -n unet python=3.9 conda activate unet pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install opencv-python numpy matplotlib tqdm提示:如果你的显卡显存不够(小于6GB),建议把batch size设小,同时把输入patch尺寸从512降到256。模型和显存的关系并不是线性的,UNet下采样到16层时中间特征的通道数会比较大,显存往往会成为训练的第一个瓶颈。
4.2 数据加载器:Dataset类怎么写最稳
PyTorch的Dataset类是用来给模型喂数据的。遥感分割的数据加载有几个特殊处理,代码里必须体现:
import os import cv2 import numpy as np import torch from torch.utils.data import Dataset class RemoteSensingDataset(Dataset): def __init__(self, image_dir, mask_dir, image_size=512, transform=None): self.image_paths = sorted([os.path.join(image_dir, f) for f in os.listdir(image_dir)]) self.mask_paths = sorted([os.path.join(mask_dir, f) for f in os.listdir(mask_dir)]) self.image_size = image_size self.transform = transform def __len__(self): return len(self.image_paths) def __getitem__(self, idx): image = cv2.imread(self.image_paths[idx]) image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) mask = cv2.imread(self.mask_paths[idx], cv2.IMREAD_GRAYSCALE) # 随机裁剪到统一尺寸 h, w = image.shape[:2] top = np.random.randint(0, h - self.image_size + 1) left = np.random.randint(0, w - self.image_size + 1) image = image[top:top+self.image_size, left:left+self.image_size] mask = mask[top:top+self.image_size, left:left+self.image_size] # 归一化 image = image.astype(np.float32) / 255.0 mean = np.array([0.485, 0.456, 0.406]) std = np.array([0.229, 0.224, 0.225]) image = (image - mean) / std # HWC -> CHW image = np.transpose(image, (2, 0, 1)) mask = torch.from_numpy(mask.astype(np.int64)) return torch.from_numpy(image).float(), mask这里有几个容易被忽略的细节:
- 随机裁剪的边界判断:如果原图尺寸小于image_size,
np.random.randint会直接报错。要么在init里断言检查,要么对过小的图做resize。我推荐在数据集准备阶段就先统一过滤掉小于512的图,省得在训练过程中崩溃。 - mask必须是int64:PyTorch的CrossEntropyLoss要求target是LongTensor,否则会报类型不匹配。
- 归一化的mean/std:这里用了ImageNet的统计值,虽然遥感图像色域分布和自然图像有差异,但在迁移学习的框架下使用ImageNet的归一化参数是合理的,不要改成[0,1]的简单归一化,后者会让预训练模型权重失去意义(如果你用到了预训练backbone)。
- 注意文件路径排序:sorted可以保证image和mask的文件名一一对应,但两个文件夹里文件名必须完全一致,否则会出现图像和标签错位。我自己习惯把文件名统一命名为
img_0001.png和mask_0001.png,并在Dataset里加一个断言检查数量是否相等。
4.3 训练循环:必须监控这些指标
训练UNet不必写太多花哨的代码,核心循环就那些,但有几个关键点比代码本身重要得多。
学习率和优化器:遥感分割任务我推荐AdamW + OneCycleLR或CosineAnnealingLR。初始学习率设在1e-4到3e-4之间比较稳。如果训练前期损失震荡剧烈,把学习率降到3e-5重新跑;如果损失下降太慢,适当提高到5e-4,但要密切观察。
损失函数:单用交叉熵在类别不平衡时会偏向多数类;单用DiceLoss在小目标上训练不稳定。我实测效果最好的是交叉熵 + DiceLoss的组合,加权系数可以设为1:1,也可以让DiceLoss略低,比如0.5。PyTorch实现DiceLoss并不复杂,核心代码如下:
def dice_loss(pred, target, eps=1e-7): pred = torch.softmax(pred, dim=1) target_one_hot = torch.nn.functional.one_hot(target, num_classes=pred.shape[1]) target_one_hot = target_one_hot.permute(0, 3, 1, 2).float() intersection = (pred * target_one_hot).sum(dim=(2, 3)) union = pred.sum(dim=(2, 3)) + target_one_hot.sum(dim=(2, 3)) dice = (2 * intersection + eps) / (union + eps) return 1 - dice.mean()这里需要特别提醒:计算DiceLoss时不要用softmax后的概率直接做one-hot,也不要直接在logits上算,要确保pred和target_one_hot的shape完全对齐,否则很容易出现一个维度为1的广播,精度看着还可以,但实际指标全是幻觉。
早停与模型保存:建议每个epoch结束后在验证集上计算mIoU,只保存验证集mIoU最高时的权重,而不是最后一个epoch的权重。深度学习训练后期往往会出现验证指标下降(过拟合),保存最佳权重能帮你保住最好的结果。
best_iou = 0 for epoch in range(epochs): train_loss = train_one_epoch(model, loader, optimizer, criterion) val_iou, val_loss = validate(model, val_loader) if val_iou > best_iou: best_iou = val_iou torch.save(model.state_dict(), "weights/best_model.pth") print(f"Epoch {epoch:03d} | Train Loss {train_loss:.4f} | Val IoU {val_iou:.4f}")训练日志一定要保留,因为你在论文里需要画出loss曲线和IoU曲线。tqdm加不加都行,但我强烈建议把每个epoch的训练信息输出到日志文件,而不是只打印到终端。几十个epoch的训练,日志文件是排查问题的第一手证据。
4.4 评估指标:mIoU怎么算才准确
语义分割最核心的指标是mIoU(mean Intersection over Union),它表示每个类别预测区域和真实区域的交集与并集之比,再对所有类别取平均。公式不复杂:
IoU = TP / (TP + FP + FN) mIoU = (1 / C) * sum(IoU_c)其中TP是真正例,FP是假正例,FN是假阴例,C是类别数。
写代码的时候,千万不要直接循环每个像素比较,那样太慢。正确的做法是先用混淆矩阵统计每个类别的TP、FP、FN,再统一计算:
def compute_miou(pred, target, num_classes, ignore_index=255): pred = pred.view(-1) target = target.view(-1) mask = target != ignore_index pred = pred[mask] target = target[mask] confusion_matrix = np.zeros((num_classes, num_classes), dtype=np.int64) for t, p in zip(target.cpu().numpy(), pred.cpu().numpy()): confusion_matrix[t, p] += 1 intersection = np.diag(confusion_matrix) union = confusion_matrix.sum(axis=0) + confusion_matrix.sum(axis=1) - intersection iou = intersection / (union + 1e-7) return np.nanmean(iou), iou这个实现的核心逻辑是:混淆矩阵的第i行第j列表示“真实类别i被预测为类别j”的像素个数,对角线就是各类别的TP。用矩阵运算替代逐像素循环,速度能快几十倍。
还有一个细节:如果某个类别在整张验证图里没有出现(比如某张图里完全没有水体),那这个类别的IoU分子分母都是0,应该跳过不算,而不是记成0。用np.nanmean可以自动跳过NaN。
除了mIoU,建议同时报告F1分数和Pixel Accuracy(PA)。虽然mIoU是主要指标,但答辩老师可能会问“为什么用IoU而不用PA”,这时候你要能解释:PA会被背景类主导,如果背景占90%,模型把全部像素预测为背景都能得到90%的PA,但这个结果毫无意义。而IoU对每类分别计算,能更公平地反映模型在少数类上的表现。
4.5 推理与可视化:如何让结果“能看”
训练完成后,写一个predict脚本,对测试集逐张图推理,保存分割结果的可视化图。这一步对论文和答辩都极其重要,因为评审老师第一眼看到的不是你的训练曲线,而是你贴出来的分割效果图。
def predict_image(model, image_path, output_path, device, image_size=512): model.eval() image = cv2.imread(image_path) image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) h, w = image_rgb.shape[:2] # 滑窗预测 result = np.zeros((h, w), dtype=np.int64) count = np.zeros((h, w), dtype=np.float32) for top in range(0, h - image_size + 1, image_size // 2): for left in range(0, w - image_size + 1, image_size // 2): patch = image_rgb[top:top+image_size, left:left+image_size] patch = patch.astype(np.float32) / 255.0 patch = (patch - mean) / std patch = np.transpose(patch, (2, 0, 1)) patch = torch.from_numpy(patch).unsqueeze(0).float().to(device) with torch.no_grad(): output = model(patch) pred = torch.argmax(output, dim=1).squeeze(0).cpu().numpy() result[top:top+image_size, left:left+image_size] += pred count[top:top+image_size, left:left+image_size] += 1 result = np.round(result / count).astype(np.int64) # 保存:原图 + 标签 + 预测 color_map = np.array([[0, 0, 0], [255, 0, 0], [0, 255, 0], [0, 0, 255], [255, 255, 0]]) vis_pred = color_map[result] cv2.imwrite(output_path, cv2.cvtColor(vis_pred, cv2.COLOR_RGB2BGR))这里有几个关键点:
- 滑窗步长取image_size的一半,即重叠率50%。重叠区域多次预测后取平均,边缘处的预测会更稳定。
- count数组记录每个像素被预测了几次,最后做归一化。如果不做这个处理,边界区域会出现明显的“马赛克”拼接痕迹。
- 可视化时的颜色映射要统一,论文里所有图都用同一套颜色方案,否则视觉上会很混乱。
5. 论文写作:如何把项目讲出高分
5.1 论文结构怎么安排
本科毕设论文通常有固定套路,但“套路”不等于“敷衍”。我建议按以下结构组织:
- 第一章 绪论:背景与意义、国内外研究现状、主要工作内容。重点写清楚“遥感图像分割为什么重要”,不要只写“随着遥感技术的发展”这种空话,要具体到“高分遥感影像中建筑物提取对城市规划的意义”“水体提取对洪涝灾害评估的意义”等,有具体场景支撑的背景才不空洞。
- 第二章 相关技术基础:卷积神经网络基础、语义分割主流方法综述、UNet网络结构详细说明。这一章看起来是凑字数用的,但实际上是你回答答辩问题的弹药库。把卷积、池化、转置卷积、感受野这些基础概念用自己的话写清楚,答辩时被问到任何细节都能从这里找到答案。
- 第三章 数据集与预处理:数据集介绍、数据预处理流程、数据增强策略、类别不平衡处理方案。这一章是拉开差距的地方,因为很多人的论文根本不写数据预处理细节,而你如果写清楚了patch裁剪策略、类别权重计算方式,老师会认为你真的动手做过。
- 第四章 改进的UNet模型设计:基线模型、改进思路、改进后的网络结构、模块细节。重点展示你改了什么、为什么这么改、图怎么画。
- 第五章 实验设计与结果分析:实验环境、评价指标、对比实验、消融实验、可视化结果分析。这是全篇最有说服力的部分,必须用数据说话。
- 第六章 总结与展望:总结工作内容、分析不足、展望改进方向。不要写“相信未来一定会更好”这类废话,要写“本方法在xxx场景下存在xxx不足,后续可以尝试xxx”,实打实的未来工作才是老师想看到的。
5.2 实验设计:如何把对比做扎实
高分论文的实验部分,几乎都有一个共同点:实验表格非常丰富。我强烈建议至少做以下几组对比:
- 基线UNet vs 改进UNet:证明你的改进有效。如果改进后mIoU提升不明显,也别慌,可以在论文里如实写“在xx类别上提升明显,在xx类别上基本持平”,然后分析原因。
- 不同损失函数的对比:交叉熵 vs DiceLoss vs 两者结合。这个实验成本很低(每个只需要重跑一遍训练),但能显著增加论文的深度。
- 不同输入尺寸的对比:256 vs 512。可以分析大patch带来的感受野扩大,与小patch带来的细节保留之间的权衡。
- 不同优化器/LR策略的对比:Adam vs SGD、固定LR vs CosineAnnealing。这个可以简单做一个表格,作为调参经验总结。
一个容易忽略的坑是:对比实验必须保持变量一致。比如你想对比“是否加入注意力模块”,那么在两次实验中,除了模型结构外,数据增强、初始学习率、随机种子、训练epoch数、batch size都必须完全一致,否则你没法把mIoU的差异归因于网络结构。最好固定随机种子(如torch.manual_seed(42)),确保实验可复现。
5.3 图表制作:配得上“高分”两个字
论文里的图和表,质量直接决定了老师的第一印象。我的几条经验:
- 网络结构图:不要直接截取其他论文的图,也不要画得太复杂。用PPT或draw.io画清楚“编码器-解码器-跳连”的大结构,标注清楚特征图尺寸和通道数变化,一张图控制在A4纸宽度内。
- 训练曲线图:横轴是epoch,纵轴是loss和mIoU,最好把train和val画在一张图上,用不同颜色区分。注意纵轴范围不要自动缩放得过于夸张,否则曲线看起来是平的。
- 效果对比图:把“原图、标签、基线模型预测、改进模型预测”四张图拼成一行,选三到四组典型场景。一定要挑一个“改进模型明显优于基线”的例子,也要挑一个“两个模型都预测失败”的例子,然后分析失败原因。
- 结果表格:每张表都要有表头、单位、显著性加粗(比如最好结果加粗),每张表下面配一段分析文字,不要只贴表不分析。
6. 常见问题与排查技巧实录
6.1 训练时显存溢出(OOM)
这是遥感分割训练里最常见的报错。原因通常是输入图像太大或batch size太大。排查顺序:
- 把batch size减半,比如从8改成4。如果还OOM,继续减半到2或1。
- 把输入patch尺寸从512降到384或256。分割任务的输入尺寸对效果有影响,但小一点总比训练不起来强。
- 开启PyTorch的
torch.cuda.amp混合精度训练,能把显存占用降低一半左右,而且对精度影响很小。这也是论文里可以提的一个细节。 - 检查是否在代码里保存了不必要的中间变量,比如计算loss时调用了
.detach()但没有把计算图释放。
如果以上都不行,那就要考虑自己的显卡是不是太小了,可以租云GPU来做训练,毕竟毕业设计时间宝贵,不值得为了在本地跑通而浪费一个星期。
6.2 损失下降但mIoU不涨
这是一个非常典型的陷阱:训练loss在降,但验证集mIoU纹丝不动甚至下降。排查思路如下:
- 先确认验证集的评估代码没问题。很多人写的mIoU计算有bug,最常见的是类别数和混淆矩阵维度对不上,或者没有忽略ignore_index。
- 检查是不是过拟合了。看看训练loss和验证loss的差距,如果训练loss很低但验证loss很高,说明模型在学训练集的“噪声”而不是泛化特征。此时需要增强数据增强、加大权重衰减、加入早停。
- 检查是否类别极端不平衡。如果背景类占95%,道路占1%,模型只需要把所有像素都预测为背景,loss就能降得很低,但道路的IoU是0,mIoU自然上不去。解决方案就是前面提到的在损失函数里加入类别权重或DiceLoss。
我自己遇到过一个很坑的情况:训练正常、loss下降、mIoU却不涨,查了一天发现是数据加载时“图mask的颜色映射表写反了”,导致标签类别错位,模型在错误的监督信号下学习,自然学不出什么东西。
6.3 预测结果出现“网格状”或“棋盘格”伪影
这个现象在遥感分割中很常见,主要原因是推理时patch之间没有overlap,或者overlap区域没有做平均。解决办法前面已经提过:滑窗步长设置为patch尺寸的一半,重叠区域多次预测后取平均。还有一个原因是转置卷积带来的“棋盘格效应”,如果模型里有大量转置卷积,可以尝试换成双线性插值上采样加卷积的组合,虽然不是每次都明显,但值得一试。
6.4 训练速度慢到无法接受
如果在1080Ti或更低档次的卡上训练,每个epoch都要几十分钟,那肯定要优化。我的建议按优先级排列:
- 先把图像尺寸降下来,从512降到256,速度提升远大于显存优化。
- 开启
torch.backends.cudnn.benchmark = True。这个设置能让PyTorch自动寻找最适合当前输入尺寸的卷积算法,有时候能带来30%以上的速度提升。 - 检查是不是数据加载成了瓶颈。如果GPU利用率很低但CPU跑满,说明是DataLoader的num_workers设小了。增加到4或8,并把
pin_memory=True打开。 - 混合精度训练除了省显存,本身也会加速计算。
6.5 答辩时老师会问什么
最后顺手列几个我见过的、也是你大概率会被问到的答辩问题:
- “为什么选UNet而不是其他模型?” 回答思路参考本文1.2节。
- “你的改进点在哪里?为什么有效?” 这是必考题,要能说清楚你改的是哪一层、为什么这一层改完能提升效果。
- “mIoU和PA有什么区别?” 答案见4.4节。
- “你的模型在什么场景下会失效?” 诚实回答,比如“在建筑物阴影密集的区域,模型会把阴影误判为水体”,然后补充说“我分析是因为训练集里阴影样本较少,后续可以增加这类样本”。
- “你能现场跑一下吗?” 如果准备了训练好的权重和简单的demo脚本,这个回答就很从容。
写在最后:几个项目的实际心得
这个项目我前前后后带人做过几次完整流程,从数据准备到论文定稿,通常需要四到六周。我个人最大的体会是:这个题目的难度并不是“算法创新”,而是“工程过程的完整性”——能不能把数据、模型、训练、评估、可视化、论文串成一条线。UNet本身不是难点,真正拉开差距的是数据预处理是否精细、实验对比是否充分、论文写作是否扎实。
如果你正在做这个项目,我最后再分享一个建议:从第一天开始,就把所有实验结果保留下来,包括你改过的超参数、每个版本的模型权重、每个结果对应的代码版本。不要嫌麻烦,等到写论文时你会发现,最痛苦的事情不是没有结果,而是“这张图对应的实验我忘了用的是哪个配置”。可以简单做一个Excel表格,记录每次实验的日期、模型结构、数据集版本、关键超参数、mIoU和备注信息,这会让论文写作阶段轻松非常多。
这个项目做完之后可以扩展的方向也很多,比如把UNet换成UNet++或SegFormer,把单模态图像改成图像加高程数据的多模态输入,或者加入边缘监督分支来提升边界质量。如果你后面想继续做研究或者找工作,这段经历都能成为你简历上拿得出手的内容。
本文还有配套的精品资源,点击获取