
简介面向遥感图像语义分割方向的毕业设计压缩包内提供了一套基于UNet的完整实现方案适合计算机视觉相关专业学生与需要从零搭建分割模型的入门研究者。资源共69个文件压缩后约47.26MB主要包含Python源码模型、训练与预测脚本、Jupyter Notebook交互演示、论文PDF与LaTeX排版源文件以及用于图表展示的PNG和SVG图片等目录结构根据功能划分便于按模块查阅与二次开发。内容系统覆盖了遥感图像语义分割从数据准备、预处理到UNet编码器-解码器网络实现、模型训练与测试评估的完整链路并给出了IoU、准确率、召回率等指标的计算与解读方式可帮助读者逐一解决建筑、道路、植被等多类地物分割中的常见问题。配套数据制作、训练、预测三个阶段的Notebook以及文档说明配合已有346人的学习验证可直接作为毕业设计主体代码也可作为论文写作和实验复现的参考资料节省从零搭建的时间。1. 遥感图像语义分割为什么我最后选了 UNet 而不是一开始想用的 FCN说个可能和直觉相反的事UNet 最早是给医学图像设计的但在遥感图像语义分割这个方向它反而成了最稳的基线。我做这个课题第一周拿 FCN 跑了一版结果小目标独立房屋、车辆、小型构筑物全被背景稀释掉几乎看不见换 UNet 之后跳跃连接把浅层轮廓信息直接送到解码端那些小目标才真正“回到”分割结果里。这份资源打包的东西非常完整从 create_dataset.ipynb 做数据准备到 train.py 训练到 predict.ipynb 出预测图连毕业论文 LaTeX 底稿和一篇参考论文 PDF 都在里面。如果你正在准备遥感语义分割相关的毕业设计或者已经被数据集和标签格式折磨了两周这份包能帮你少走很多弯路。2. UNet 网络结构拆解编码器、解码器和跳跃连接在遥感图上各自干什么先说结论UNet 的形状像一个左右对称的 U左边是编码器右边是解码器中间用跳跃连接把每一层“接起来”。这个结构能成为遥感分割的默认基线并不是因为它新而是它同时保住了两类关键信息——语义类别信息和空间位置信息。下面结合这类项目里最常见的 model.py 和 cnn.py 写法把每个模块的作用拆开讲。2.1 编码器从像素到高级特征的下采样过程编码器本质上是一串不断“变粗”的特征提取器。每一层做两次 3×3 卷积然后用一次 2×2 最大池化把分辨率减半同时把通道数翻倍。以 256×256 的输入为例经过 4 次下采样特征图会从 256×256 缩小到 16×16通道数从 3 涨到 512。用一句话解释网络先识别像素级的边缘、纹理再逐步组合成“屋顶”“路面”“树冠”这类语义概念。遥感影像和普通照片最大的不同是目标的尺度跨度极大。一条高速公路可能是几百像素的连续长条一栋独立别墅可能只有二三十个像素。如果只靠浅层特征小目标会被周围环境盖住如果只看最深层特征大目标没问题小目标细节又因为分辨率太低而丢失。所以编码器必须做多次下采样来扩大感受野代价是空间分辨率下降——这也是要配一个对称解码器把分辨率“还回去”的原因。2.2 解码器与跳跃连接细节和位置是怎么“找回来”的解码器的每一层先把特征图做一次上采样通常是转置卷积或双线性插值分辨率翻倍然后把来自编码器同一层的特征图在通道维度上拼接起来。这个拼接操作就是跳跃连接。它做的事情很直白解码阶段每放大一次图像就能从编码器对应位置拿回一份高分辨率的浅层特征用来补充边缘和轮廓信息。对遥感分割来说跳跃连接解决的是一个非常实际的痛点道路、建筑边界、阴影轮廓这类细节在多次池化之后信息基本丢光了。不是模型“不想学”而是信息根本没有传到底层。UNet 在每个尺度都做一次跳跃连接等于给解码器留了几份“备档”输出层融合这些备档后再做像素分类边界自然比单条路径传输的 FCN 干净很多。2.3 代码对照model.py 里的 Down 和 Up 到底长什么样网上 UNet 实现很多核心块的写法其实差不多。常见做法是先定义“双卷积”和“下采样模块”再把它们左右拼起来。这份资源的 model.py 和 cnn.py 里大概率也是这一套逻辑。import torch import torch.nn as nn class DoubleConv(nn.Module): UNet 里的基本双卷积块保持特征图尺寸不变 def __init__(self, in_ch, out_ch): super().__init__() self.conv nn.Sequential( nn.Conv2d(in_ch, out_ch, kernel_size3, padding1), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue), nn.Conv2d(out_ch, out_ch, kernel_size3, padding1), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue) ) def forward(self, x): return self.conv(x) class Down(nn.Module): 下采样块先池化减半分辨率再双卷积提特征 def __init__(self, in_ch, out_ch): super().__init__() self.pool nn.MaxPool2d(2) self.conv DoubleConv(in_ch, out_ch) def forward(self, x): return self.conv(self.pool(x))参数说明in_ch是当前层输入通道数第一次是 3RGB之后每一层按 64、128、256、512 翻倍kernel_size3配合padding1让卷积不改变特征图尺寸MaxPool2d(2)把宽高各减半。如果你跑的是多光谱影像比如 RGB 近红外四个波段初始in_ch要改成 4这步经常有人漏掉。上采样部分通常用nn.ConvTranspose2d(in_ch, out_ch, kernel_size2, stride2)把尺寸放大一倍再和编码器对应层做torch.cat([x, down_feature], dim1)。拼接后通道数翻倍后面再接一个双卷积把通道数压回去。整体参数量大概在三千万左右不算轻量训练的时候要有显存压力预期。2.4 遥感专用的调整BN、多波段输入和输入尺寸选择如果直接把医学图像那套 UNet 拿到遥感数据上大概率会遇到两个问题。第一个是输入通道很多医学分割用灰度图输入通道是 1遥感图通常至少是 RGB 三通道甚至多光谱。第二个是 BatchNorm 的使用方式这个层比想象中重要。我在实际跑数据时一般会做三件事。第一把初始in_channels做成参数而不是写死换数据集时不用动网络结构。第二保留 BatchNorm 层遥感图之间色调、对比度差异很大BN 能把特征分布拉回稳定范围不然训练过程很容易震荡。第三输入尺寸尽量取 256 或 512并且是 2 的整数次幂。UNet 有多次下采样如果输入宽高不能被 2 整除解码器上采样得到的尺寸和编码器对应层尺寸对不上拼接时会直接报错。这里给出一个典型 UNet 编码器路径的配置表方便你对照自己的模型改参阶段操作输入尺寸输出尺寸通道数输入层原图3×256×2563×256×2563编码层 1双卷积3×256×25664×256×25664下采样 1池化双卷积64×256×256128×128×128128下采样 2池化双卷积128×128×128256×64×64256下采样 3池化双卷积256×64×64512×32×32512下采样 4池化双卷积512×32×32512×16×16512输出层通道数取决于你的分类目标。如果只分割“建筑 vs 背景”二分类输出通道设 2 就够了如果是多分类比如把地块分成背景、建筑、道路、植被四类输出通道就设 4。这份资源的 demo 里应该预留了类别参数你改成自己的类别数就行。3. 数据准备把任意来源的遥感影像变成能喂给 UNet 的裁剪块做遥感分割的毕业设计工作量最大的往往不是模型而是数据。很多人上来就急着改网络结构结果数据没整理干净训练各种报错最后反过来怪模型不行。我建议先把数据管线跑通再碰网络结构顺序反了你会后悔。3.1 数据集的三种常见形态RGB 原图 单通道标签遥感分割数据落地下来一般有两种形态一种是官方已经裁好的图像和标签对另一种是整幅大影像加一个栅格或矢量标签文件需要自己裁。不管哪种训练时看到的标签基本都是单通道 PNG像素值从 0 开始连续编号比如 0 是背景1 是建筑2 是道路3 是植被。这里有一个特别容易踩的坑标签值绝对不能出现 255 这种“看起来像白色但其实越界”的数值。很多从 ArcGIS 或 QGIS 导出的掩膜背景是 0前景是 255这个格式拿来当分类标签损失函数会把 255 当成第 255 个类别轻则精度为零重则 loss 直接 NaN。所以第一步一定是先确认标签的取值范围。项目里的 create_dataset.ipynb一般就是处理第二步的读取原始大图和 mask按固定窗口裁成小块同时完成训练/验证集划分。裁之前先看一眼前景目标的尺度。无人机影像和卫星影像地面分辨率完全不同同样是 256×256 的窗口前者可能只覆盖半个街区后者能覆盖好几块地块目标尺度不一致分割效果会差很多。3.2 裁剪、缩放与归一化窗口大小该定多少我常用的经验是窗口大小至少能装住你关心的最大目标同时让每个 batch 里尽量都有前景样本。对于常见的 0.5 米分辨率遥感影像256×256 是一个稳的起点512×512 能提供更多上下文但显存占用明显上涨训练速度也会慢不少。下面这段就是我处理类似项目时常用的裁剪逻辑。import numpy as np import cv2 def crop_patches(image, mask, size256, step256): H, W image.shape[:2] patches_img, patches_mask [], [] for y in range(0, H - size 1, step): for x in range(0, W - size 1, step): p_img image[y:ysize, x:xsize] p_mask mask[y:ysize, x:xsize] # 过滤掉几乎全背景的 patch避免训练样本被空标签淹没 if p_mask.mean() 0.02: continue patches_img.append(p_img) patches_mask.append(p_mask) return np.stack(patches_img), np.stack(patches_mask)参数说明stepsize时是非重叠裁剪样本量最少stepsize//2时相邻 patch 有重叠样本量接近三倍p_mask.mean()是当前 patch 中前景像素占比阈值设 0.02 是过滤掉纯背景块又不至于把所有样本都删掉。如果你用的是二分类标签 0/1mask 直接相减就能算多分类时建议按类别判断比如只保留“至少含 N 个建筑像素”的 patch。裁剪之后如果原始分辨率不齐还要统一缩放。这里有个血泪经验对图像用双线性插值没问题但 mask 缩放必须用最近邻插值否则标签边界会被插值算法“抹糊”等于人为制造标注噪声。image_resized cv2.resize(image, (256, 256), interpolationcv2.INTER_LINEAR) mask_resized cv2.resize(mask, (256, 256), interpolationcv2.INTER_NEAREST)归一化可以用训练集统计的 mean/std也可以直接用 ImageNet 的默认值。遥感影像和自然图像色差很大我一般会在训练数据上现算 mean/std效果更稳。注意验证集和测试集的归一化必须使用训练集统计出来的 mean/std不能用测试集自己的统计值否则等于把测试数据的分布信息泄漏给模型评测结果没有参考价值。3.3 数据增强翻转让样本量翻倍遥感数据集往往不够大增强是扩样本最直接的方法。但遥感增强有个特殊性不能用任意角度旋转我一般只用 0、90、180、270 度这样的整倍数旋转否则道路、建筑的几何方向会被转乱。水平/垂直翻转对大多数遥感语义分割是安全的因为你没法说清楚哪个方向是“正”的。import numpy as np import imgaug.augmenters as iaa seq iaa.Sequential([ iaa.Fliplr(0.5), # 水平翻转 iaa.Flipud(0.5), # 垂直翻转 iaa.Affine(rotate[0, 90, 180, 270]), # 只做整 90 度旋转 iaa.Multiply((0.9, 1.1)), # 轻微亮度抖动 ]) def aug_pair(image, mask): segmap iaa.SegmentationMapOnImage( mask, shapemask.shape, nb_classes4) image, mask seq(imageimage, segmentation_mapssegmap) return image, mask.astype(np.uint8)参数说明Fliplr(0.5)表示每次有 50% 概率水平翻转Affine(rotate[...])只允许四个角度的倍数避免任意角度的重采样导致边界噪声Multiply模拟不同光照条件幅度别超过 10%否则会影响真实色调。增强时最关键的一点是图像和标签必须走同一个几何变换不然增强完标签和图像对不上训练出来的模型边界全是错的。4. 训练与评估train.py 里最容易改错的那几个参数数据管线跑通之后下一个重头戏是训练过程。深度学习训练代码翻来覆去就是“加载数据、前向、算损失、反向、更新、评测”这几步但每一步都有容易挂的地方。比如标签维度写成[batch, 1, H, W]而模型输出是[batch, num_cls, H, W]这俩直接相减就会维度报错很多人会卡半天。4.1 损失函数与 IoU 指标别只盯着 accuracy遥感分割绝大多数场景是多分类实际项目里有个常见简化如果只分割“建筑 vs 背景”这就是二分类用 sigmoid Binary Cross Entropy 就够了。如果分多类直接用nn.CrossEntropyLoss它内部会先对 logits 做 softmax再对每个像素算交叉熵不需要你手动再套一层 softmax。评估指标方面像素准确率很容易虚高。背景如果占了 90%全部预测成背景也能有 90% 准确率看着挺好看实际一点用没有。真正有参考价值的是 IoU 和 mIoU。IoU 定义是预测与真实相交面积除以预测与真实相并面积越接近 1 说明分割结果越贴近标注。def compute_iou(pred, mask, num_classes): ious [] for cls in range(num_classes): p (pred cls) t (mask cls) inter (p t).sum() union (p | t).sum() ious.append(inter / union if union 0 else 1.0) return np.mean(ious)参数说明pred必须是argmax之后的类别索引而不是概率图mask是原始标签num_classes是类别总数。循环里对每个类别单独算交并比union 0表示这个类别在当前图中完全没出现此时约定 IoU 为 1避免用 0 去惩罚“本来就不存在”的类别。4.2 train.py 训练流程逐段拆解训练循环的骨架基本固定我写一份常见 PyTorch 版本的模板对照这份资源里的 train.py 应该能很快对齐。import torch import torch.nn as nn device torch.device(cuda if torch.cuda.is_available() else cpu) model UNet(n_channels3, n_classes4).to(device) criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr1e-3) best_iou 0.0 for epoch in range(60): model.train() total_loss 0.0 for imgs, masks in train_loader: imgs imgs.to(device) masks masks.to(device).long() # 标签必须是 long 型 preds model(imgs) # 输出 (B, num_cls, H, W) loss criterion(preds, masks) optimizer.zero_grad() loss.backward() optimizer.step() total_loss loss.item() # 这里千万用 .item() 取数值 # 验证 model.eval() val_ious [] with torch.no_grad(): for imgs, masks in val_loader: imgs imgs.to(device) masks masks.to(device) preds model(imgs) pred_cls preds.argmax(dim1) val_ious.append(compute_iou( pred_cls.cpu().numpy(), masks.cpu().numpy(), num_classes4)) avg_iou float(np.mean(val_ious)) if avg_iou best_iou: best_iou avg_iou torch.save(model.state_dict(), best_model.pth)逐段说几个要点masks.to(device).long()是把标签转成LongTensorCrossEntropyLoss 不接受浮点标签loss.item()是把张量拆成 Python 浮点数避免在验证时把整个计算图保留下来验证阶段必须包torch.no_grad()否则前向过程的中间缓存会把显存占满这也是很多人验证时突然 OOM 的原因。模型保存只保留验证集 mIoU 最高的那一版预测阶段直接用best_model.pth。4.3 超参数设定与 TensorBoard 观察节奏学习率一般从 1e-3 起步后期降到 1e-4batch size 在 4、8、16 里取取决于显存epoch 建议设 60 左右配合早停策略验证集 mIoU 连续 10 个 epoch 不涨就停。这份资源里有 start_tensorboard.ps1 脚本作用是启动 TensorBoard 方便你观察训练曲线Windows 下如果双击没反应多半是 PowerShell 执行策略问题第 5 章我会专门讲怎么处理。训练时我一般只看两件事第一loss 曲线有没有稳定下降第二验证集 mIoU 是不是在涨。如果 loss 下降但 mIoU 不动先去看是不是验证集评估逻辑写错了比调模型参数更有用。另外有条件的话定期把验证集的预测图打印出来“肉眼验货”指标只能给数字图才是最终交付物。5. UNet 遥感分割避坑指南五个差点让我改题目的翻车现场这一章写实际跑数据时最常遇到的五类问题每一条都是“现象 → 原因 → 解决”的完整记录。看完至少能帮你省下一个月的 Debug 时间。5.1 训练 loss 变 NaN标签值域和归一化没对齐现象训练第一个 step 的 loss 就是 nan或者前 20 个 epoch 正常第 21 个 epoch 突然变 nan。模型参数全部变 NaN 之后再往下训练也不会恢复。原因最常见的是标签值越界。遥感标注从 GIS 软件导出时前景常写成 255而 CrossEntropyLoss 只接受 0 到num_classes-1的整数标签255 会被当作一个不存在的类别。输入图像里如果某个波段存在空值或 NaN前向传播会直接污染计算图。此外学习率过大也会让损失在训练中途震荡成 NaN。解决训练前强制做两步检查。第一步在 Dataset 的__getitem__里加断言assert mask.max() num_classes一行代码拦住所有非法标签第二步输入图像先np.nan_to_num再做归一化。如果你写的是loss_func(preds, masks.float())改成.long()这个错误在我见过的问题里占比最高。5.2 CUDA out of memory不只是 batch size 的问题现象train.py 在迭代前几个 batch 时抛出RuntimeError: CUDA out of memory。把 batch size 调到 1 仍然报错或者跑着跑着内存越占越多。原因UNet 的编码器和解码器通道数大中间还要做跳跃连接的特征拼接512×512 输入在 batch size 为 4 时显存占用非常可观。如果只在训练循环里减小 batch size别忘了验证阶段的前向过程也会建计算图一旦忘记with torch.no_grad()显存会在验证时瞬间被打满。解决把输入 patch 从 512 降到 256batch size 设 1 或 2验证和预测全部套上torch.no_grad()。如果你必须用大 batch 才能稳定训练用梯度累积模拟accumulation_steps 4 loss criterion(preds, masks) / accumulation_steps loss.backward() if (step 1) % accumulation_steps 0: optimizer.step() optimizer.zero_grad()参数说明accumulation_steps4表示每 4 个小 batch 累计一次梯度等效于把 batch size 放大了 4 倍但显存占用还是一个小 batch 的量。需要注意的是loss要先除以累积步数否则梯度等效值偏大学习率需要相应调小。5.3 验证集 IoU 高但出图没法看指标口径和数据泄漏现象验证集 mIoU 达到 0.8 以上但预测图边缘全是锯齿内部有很多小孔和目标图像明显错位。指标和肉眼观感严重不一致。原因两个隐藏问题。第一验证集 DataLoader 里也开了随机翻转或随机裁剪等于同一个目标以多种形变反复出现在训练里指标被虚高这叫数据泄漏。第二评测时把模型的概率输出直接round()成 0/1而不是先取argmax导致边界类别的判定非常不稳定。解决验证集 DataLoader 关闭一切随机操作只做中心裁剪或直接 resize。预测时用torch.argmax(preds, dim1)得到每个像素的类别索引不要手动靠概率阈值切分。修改之后如果 mIoU 降了不要慌降下来的才是真实水平。5.4 背景占比超过 90%类别不平衡导致小目标丢失现象训练 loss 收敛正常但预测结果里建筑全部消失或者道路断成一截一截。检查发现验证集的 mIoU 只涨背景那一类其他类别全挂。原因遥感图像背景占绝大多数是常态默认交叉熵会给每个像素一样的权重模型只要把所有像素都预测成背景loss 也不会太高于是少数类被彻底压制。这个现象在小目标上特别明显。解决常见做法是给损失函数加类别权重。先统计每类像素占比权重用占比的倒数做归一化class_weight torch.tensor([0.2, 2.0, 3.0, 4.0]).to(device) criterion nn.CrossEntropyLoss(weightclass_weight)参数说明这个列表的顺序对应类别 0、1、2、3背景权重压低前景类别权重抬高。权重不用太极端一般 2 到 5 倍就够加得过于猛会让训练震荡。如果加权重还是压不住再考虑 DiceLoss 或 FocalLoss但不要同时叠加太多损失容易把正常收敛的训练带偏。5.5 Windows 下 start_jupyter.ps1 报错执行策略和路径空格现象Windows 上双击 start_jupyter.ps1PowerShell 弹出“禁止运行脚本”或者脚本一闪而过没有任何反应。start_tensorboard.ps1 基本同样的问题。原因Windows 默认 PowerShell 执行策略是 Restricted不允许运行.ps1脚本。另外如果项目路径带了中文、空格比如C:\Users\张三\毕业设计(2)脚本里定位 python.exe 会失败窗口一闪而过就是路径没有正确转义。解决在 PowerShell 里执行一次授权命令然后确保项目路径是纯英文。省事一点的方案是完全绕开脚本直接用 Python 模块启动服务python -m jupyter notebook python -m tensorboard.main --logdirruns这样虽然丢了.ps1脚本的“一键启动”体验但至少不会因为系统权限问题卡在第一步。如果你喜欢批处理也可以把同样的命令写进start_jupyter.batWindows 对.bat没有执行策略限制用起来更稳妥。6. 预测与交付用 predict.ipynb 跑通推理再手动验证一次 IoU训练完之后毕业设计真正要交付的不是一组指标而是模型能在新的遥感图上分割出目标。这一步如果直接拿大影像塞给网络会因为输入尺寸太大导致显存溢出或推理时间不可接受所以通常要跑滑动窗口。6.1 大影像推理的滑动窗口拼接大图推理的常见做法是把图切成和训练时同样尺寸的 patch预测完再拼回去。重叠可以避免边界伪影我一般把step设为size // 2让相邻两个窗口有一半重合最后对重合区域取概率平均值而不是直接覆盖。由于完整的多窗口概率累加逻辑比较长关键是记住每个 patch 先得到 softmax 概率再累加到一张全图的score_map最后argmax出类别。6.2 手动验证一张图片的 IoU在不同数据上跑完推理之后建议挑两三张带实测标注的图手动算一次 IoU 做复核。不要只依赖训练时的验证指标推理流程里的预处理和训练时有差异结果就会变。import numpy as np def single_image_iou(pred_mask, gt_mask, num_classes): ious [] for cls in range(num_classes): p (pred_mask cls) t (gt_mask cls) inter (p t).sum() union (p | t).sum() ious.append(inter / union if union 0 else 1.0) return np.mean(ious), ious参数说明pred_mask是模型推理后经过拼接得到的最终类别图gt_mask是原始标注两个数组必须完全同尺寸、同坐标系。输出结果里np.mean(ious)是这一张图的 mIoUious单个类别的值可以帮你定位具体是哪一类造成指标下滑。如果某张图的 mIoU 明显低于训练验证值优先检查预处理流程大概率是缩放到推理尺寸时掩膜插值方式不对或者测试图没有做和训练一致的归一化。从那以后我每换一个数据集都会强制自己先走一遍三件事检查标签值域挑一张图做预测并用肉眼对比原图最后才跑完整训练流程。这套步骤多花十几分钟但能拦住大部分翻车事故。希望帮到你。本文还有配套的精品资源点击获取