
简介一套面向毕业设计场景的基于UNet的遥感图像语义分割项目从数据准备、像素级标注、预处理到编码器-解码器网络设计、反向传播训练和IoU等指标评估均有涉及适合计算机视觉、遥感信息处理方向的本科生或研究生作为课题参考。压缩包共69个文件约47.26MB既有Python核心代码py/pyc也有Jupyter示例ipynb、论文LaTeX源文件tex、结果图与架构图png/svg以及说明文档代码与论文材料分开存放便于按模块查阅。该资源已有346人学习下载在同类毕设资料中具备一定参考价值。内部附有可运行demo、创建数据集与训练/预测notebook、相关中文论文PDF还有毕业论文各章节tex源码既能快速复现分割流程也能用于撰写开题、中期及最终报告时直接借鉴结构。1. 基于UNet的遥感图像语义分割毕设选题不只是一张网络结构图遥感图像语义分割和给一张街景照片做分割完全是两码事。一张遥感影像动辄上万像素边长、覆盖几平方公里里面的建筑物、道路、水体、农田在尺度上可能差几十倍类别比例极度不平衡——这恰恰考验模型在细节恢复和样本效率上的能力。作为本科或硕士毕设UNet的编码器-解码器加上跳跃连接用小规模数据集就能训练出可用结果改进空间又足够大正好满足“能跑通、有深度、可改进”三个要求。这篇笔记把从标注、切图、训练到避坑验证的完整链路拆开讲适合刚拿到题目、还在犹豫要不要换模型、或者已经跑通但精度卡住的从业者。2. UNet为什么是遥感图像语义分割的首选骨架结构与选型分析2.1 遥感图像语义分割到底难在哪和自然图像分割相比遥感影像的第一个麻烦是尺寸。Cityscapes里的街景图大多是2048×1024目标在画面里占比很大模型看一眼就知道那是车、那是人。遥感影像则不同一张高分系列或者资源系列的原始影像宽高动辄过万像素一个建筑物可能只占几十个像素道路细得像一条线。直接缩放输入小目标就只剩一两像素语义信息彻底丢失。第二个麻烦是类别分布极度不均匀。在一个典型的城区遥感场景里植被和裸土可能占70%以上建筑物占20%道路、车辆、独立树木各占几个百分点。如果直接用交叉熵损失模型只需要学会预测“植被”就能拿到很高的准确率但语义分割的考核指标mIoU会立刻暴露问题——少数类几乎全错。第三个麻烦是地物间的光谱混淆。不同材质在影像上可能呈现相近颜色塑料大棚和白色屋顶难以区分阴影下的草地和深色水体也容易混淆。这也是很多遥感分割项目选择把RGB扩展为多光谱输入的原因后面第4章会讲到in_channels怎么改。这些困难最终都指向同一个结论不能用跑自然图像分割的思路直接套遥感数据需要特别关注小目标、边缘和类别权重。这三点正是UNet结构上的强项。2.2 UNet网络结构图里的关键设计跳跃连接不只是把特征拼起来UNet的结构可以拆成三块编码器Encoder、瓶颈Bottleneck和解码器Decoder。编码器通过多次卷积和池化不断下采样把空间分辨率从512降到16甚至8同时把通道数翻上去模型在这一阶段学到的是“图像里有什么类别”这样的语义信息。解码器再用转置卷积或插值把特征图逐步放大把分辨率恢复回去这一阶段回答的问题是“这些类别具体在哪个像素位置”。真正让UNet区别于普通自编码器的是跳跃连接Skip Connection。每个编码器层输出的特征图不仅传给下一层还通过拼接的方式直接送到对应分辨率的解码器层。这样做的好处是深层的语义特征和浅层的空间细节能在最后几层融合。一个只有3像素宽的乡间小路在经过四次池化之后信息基本没了但跳跃连接把编码器第一层的原始边缘信息直接给了解码器让模型有机会重新“看见”这条路的边界。看UNet网络结构图时我一般会注意几个参数初始通道数常见32或64取决于显存、编码器深度4层或5层影像越深越大层数可以加、以及拼接维度的对齐方式。初始通道数设64时最后一层的通道数是1024模型参数量会明显涨训练时间也变长数据量小的话从32起步更稳。2.3 和FCN、DeepLabV3比UNet赢在小样本和边缘细节FCN是语义分割的开山之作把全连接层换成卷积层再用转置卷积上采样到原图大小。但FCN的上采样倍数太大一次放大32倍的结果就是边缘模糊、小目标变成色块。城市街景里还能看个大概到了遥感影像这种动辄几千像素的大图上小路和独立树基本是糊掉的。FCN语义分割在遥感里的定位更多是baseline用来证明你有对比实验。DeepLabV3系列用空洞卷积扩大感受野在尺度差异大的户外场景表现很好但对显存要求高。以ResNet101为骨干的DeepLabV3输入512×512都要6GB以上显存很多毕业设计只有一张8GB卡跑起来非常难受。而且空洞卷积的采样点比较疏对细长目标道路网、河流容易产生断裂。UNet的优势在于三个维度第一是参数效率一个从零实现的UNet只有几百万参数比预训练ResNet50做编码器的方案轻得多小数据量不容易过拟合第二是边缘恢复跳跃连接把浅层细节直接送进解码器道路、房屋边界明显更干净第三是改进方向明确UNet模型改进可以换ResNet编码器变成ResUNet可以加注意力门控变成Attention UNet也可以把输入改成多光谱融合通道这些都是毕业答辩时能讲清楚的技术点。说到这里顺便给一个选型建议如果你的数据只有几百张切片优先从原始UNet做起如果切片超过5000张编码器换成ResNet34或EfficientNet会明显提升精度如果做多光谱融合记得把in_channels从3改成4以上后面第4章有具体的模型定义。3. 遥感图像标注与数据准备从原始影像到hdict标签的完整链路3.1 标注工具与标签格式怎么选别在格式转换上浪费时间做遥感图像语义分割标注是第一个真正耗时间的环节。很多人拿到原始影像就急着写模型结果发现标签格式不统一训练时数据加载反复报错来回折腾好几天。常见做法是先用LabelMe做多边形标注。LabelMe支持导出JSON每个多边形对应一个类别标签。但JSON不能直接用于训练需要转成和影像一一对应的PNG标签图每个像素的值是该像素所属类别的整数索引。遥感领域也经常见到QGIS、ArcGIS导出的矢量shapefile思路相同最终还是栅格化成标签图。这里要提一下标签格式的约定。我习惯把标签图保存为单通道PNG类别索引从0开始背景占0其他类占1、2、3……多余类别或不确定区域给255训练时在损失函数里把255设成ignore_index。有些标注工具导出的是hdict语义分割标签结构本质还是“类别名到索引”的映射字典转成PNG时注意类别顺序要对齐否则训练出来类别含义全乱了。下面是一个把JSON多边形转成PNG标签图的参考脚本基于shapely和PILimport json import numpy as np from PIL import Image, ImageDraw def json_to_label(json_path, img_size, class_map, output_path): # class_map: {building: 1, road: 2, water: 3, background: 0} mask np.zeros((img_size[0], img_size[1]), dtypenp.uint8) 255 with open(json_path, encodingutf-8) as f: data json.load(f) for shape in data[shapes]: label_name shape[label] class_id class_map.get(label_name) if class_id is None: continue # 不认识的类别直接跳过避免污染标签 points [tuple(p) for p in shape[points]] img Image.fromarray(mask) draw ImageDraw.Draw(img) draw.polygon(points, fillclass_id) mask np.array(img) Image.fromarray(mask).save(output_path)这段代码的逻辑是先建一个全255的空白标签图然后遍历JSON里所有标注多边形按类别映射字典把对应区域填充成类别索引。注意fill完一张要重新把Image对象转回numpy数组否则下一轮绘制是在旧图上叠加。还有一个细节shapefile标注的坐标如果来自经纬度坐标需要投影转换后再栅格化坐标系的坑第5章细说。提示所有标注多边形必须互不重叠。一条道路被两个多边形重复描边会导致标签空洞或者类别覆盖模型会在这类区域学出混乱的边界。用到这个脚本时还有一点要注意你的标注类别最好不要超过10类。类别太多会让标注工作量爆炸式增长而且UNet在少数类别上的IoU会低得可怜答辩时不好解释。3.2 大影像切图滑窗尺寸、重叠率与归一化遥感影像动辄几万像素显卡吃不消常规做法是用滑窗把大影像切成训练块。切图有两个参数要决定patch_size和stride。patch_size我一般取256或512。256的优点是显存友好、小目标会占更大的像素比例缺点是上下文不够一条河流在这个块里可能只有一小段模型难判断它是河流而不是阴影。512看得更全但显存占用翻四倍而且背景比例增大类别更不均衡。折中方案是在512的图上训练测试时用滑窗预测再拼接回原图。stride决定切块之间的重叠率。stride等于patch_size就是不重叠切图会产生很多跨边界被切断的目标我一般设stride为patch_size的一半也就是50%重叠让每个目标至少完整出现在一个块里。代价是数据量翻倍训练时间变长但对分割精度帮助明显。下面是切图脚本的核心循环def crop_image(image, label, patch_size512, stride256): h, w image.shape[:2] crops_img, crops_label [], [] for y in range(0, h - patch_size 1, stride): for x in range(0, w - patch_size 1, stride): crop_img image[y:y patch_size, x:x patch_size] crop_label label[y:y patch_size, x:x patch_size] # 过滤掉标签全是255或全是背景的块减少无效训练 if (crop_label 255).all() or (crop_label 0).all(): continue crops_img.append(crop_img) crops_label.append(crop_label) return crops_img, crops_label这个循环里最有价值的判断是最后两行标签全是255或者全是背景的空块直接丢弃否则模型会花大量时间学习“预测背景”这种毫无意义的模式。补一句大图边缘不足一个patch_size时常见做法是用边缘镜像填充补足或者直接在range里丢弃边缘块这两个做法对精度影响不大但省事程度差别很大。归一化方式也会影响训练效果。如果整个训练集是同一颗卫星、同一时段的数据用全局mean和std做标准化就够如果混了多个传感器影像建议每个块单独做归一化或者按百分位截断去除极端值否则模型的收敛速度会被影像间的亮度差异拖慢。3.3 数据增强与类不平衡别让模型只会数背景遥感语义分割的数据增强策略和自然图像不完全一样。水平翻转、垂直翻转、90度旋转这类几何增强对遥感完全合理因为遥感物体没有“上下颠倒”的概念。用numpy或albumentations实现都非常简单我常用的是albumentations的Compose组合。色彩增强方面要谨慎。遥感影像有真实光谱含义把植被区域的色调大幅偏移模型的物理解释性会变差。我通常只做轻微的亮度、对比度扰动不做离谱的HSV偏移。类别不平衡问题靠增强解决不了要从损失函数和采样两个角度处理。第一个是在Dataset里对稀缺类别过采样比如单独给建筑物切片加大采样权重第二个是使用加权交叉熵或Dice Loss、Focal Loss第4章会直接给出代码。重点说一句遥感分割里Dice Loss和Focal Loss的组合基本是标配因为Focal Loss让模型关注难样本Dice Loss抵抗类别不平衡两个加在一起能明显改善道路、独立树这类小目标的召回率。数据准备这个环节做完你的文件结构应该是“影像文件夹标签文件夹一个train.txt文件”train.txt里每一行是一对影像和标签的相对路径。很多人在这一步偷懒直接遍历文件夹结果训练集和验证集出现重叠影像的切块指标虚高答辩时被老师问一句就露馅。所以train.txt和val.txt必须按原始影像来划分同一个原始影像的所有切块要么全在训练集要么全在验证集不能混。4. 用UNet训练遥感语义分割模型核心代码与参数配置4.1 模型定义从零写UNet还是用现成库到了训练阶段第一个选择是从零搭UNet还是用现成的segmentation_models_pytorch等库。我的建议是如果毕设要求讲清网络结构推荐自己写一个精简UNet并在论文里贴网络结构图如果目标是快速跑通实验对比改进效果直接使用现成库会省很多时间。下面是精简UNet的核心定义这个结构在遥感小数据集上表现稳定import torch import torch.nn as nn class DoubleConv(nn.Module): def __init__(self, in_ch, out_ch): super().__init__() self.conv 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.conv(x) class UNet(nn.Module): def __init__(self, in_channels3, num_classes7, base64): super().__init__() self.enc1 DoubleConv(in_channels, 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)几个参数说明。in_channels默认是3做多光谱融合时传4或者更多num_classes是你的类别数加背景别把背景漏了base是初始通道数显存不够从64降到32。代码里跳跃连接用的是torch.cat沿通道维拼接这是UNet原版做法。如果你改用efficientnet这类预训练编码器拼接时通道要对准这也是常见的UNet使用时的注意事项之一。4.2 损失函数组合Dice Loss Focal Loss交叉熵损失在类别均衡的数据上表现不错但遥感数据严重不均衡单独用交叉熵会让模型偏向高频类别。我在遥感分割项目中更常用Dice Loss和Focal Loss的加权组合。Dice Loss直接优化区域重合度对类别不均衡不敏感Focal Loss让模型关注那些被错分的难样本。import torch.nn.functional as F def dice_loss(pred, target, eps1e-7): # pred: [N, C, H, W] 的softmax输出, target: [N, H, W] 的类别索引 num_classes pred.shape[1] 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 eps) / (union eps) return 1 - dice.mean()注意这个Dice Loss没有对ignore_index做处理。如果你的标签里用了255作为ignore区域要在进入损失函数前先把255的像素从target里剔除或者把255替换成某个类别索引后单独做mask。很多人在跑通第一个epoch后发现loss一直不降排查到最后才发现是255像素把梯度污染了。注意Focal Loss可以用现成的实现或者用交叉熵权重替代。如果只想做最小改动给nn.CrossEntropyLoss传一个class_weight也是对不均衡的有效补救但效果上Focal Loss对难例的针对性更强。日常我习惯的权重是dice_loss * 0.5 focal_loss * 1.0具体比例不需要太过纠结0.3到0.7之间都差不多。4.3 训练参数lr、batch_size、epochs、scheduler遥感语义分割的训练参数设置网上说法很多我结合自己的经验给一组稳定起点。优化器选AdamW初始学习率1e-4到3e-4之间权重衰减1e-4或5e-5。batch_size取决于显存8GB显卡在512×512输入下batch一般只能取4到8。epochs在遥感小数据集上通常需要80到150轮因为Dice Loss收敛慢前30轮看起来进展不大是正常的。参数推荐值说明优化器AdamW比Adam更稳定的权重衰减初始lr1e-4~3e-4lr再大会震荡batch_size4~8512×512显存不够就用梯度累积调度器ReduceLROnPlateauval_mIoU停滞时降lr为1/5epochs80~150Dice Loss收敛偏慢权重衰减5e-5~1e-4防止过拟合调度器我推荐ReduceLROnPlateau而不是CosineAnnealing原因很简单遥感数据的验证指标会出现平台期平台期适当降学习率比强行按周期衰减更稳。训练时监控两项指标训练loss和验证mIoU。如果验证mIoU连续10个epoch不涨把lr降一半再跑10个epoch如果再不动就是数据或模型结构的问题不是lr的问题。还有两个容易被忽略的细节。第一是每次epoch开始前要把训练数据shuffle否则模型会学到切块的顺序验证集表现忽高忽低。第二是要保存验证mIoU最高的checkpoint而不是最后一个epoch的权重很多人的毕设结果都是从中间某个checkpoint出来的而不是最后一个。5. UNet使用时的注意事项与避坑5条血泪经验5.1 标签错位与重叠导致精度迟迟上不去现象训练loss正常下降但验证mIoU卡在0.2左右可视化预测图时发现预测结果整体偏移了几个像素建筑物轮廓和真实标签对不上。原因最常见的是影像和标签来自不同坐标系或不同分辨率的原始数据比如影像来自高分系列、标签从某在线地图导出的矢量栅格化而来两者之间存在几个像素的平移。其次是标注时多边形边缘有重叠导致同一像素被两个类别覆盖后写的类别覆盖先写的类别。解决在切图之前先用人工目视检查几组影像和标签叠加图错位严重时用OpenCV的模板匹配或手动选控制点做配准。标注重叠的问题在json_to_label脚本里加一个冲突检测统计每个像素被填充的次数超过1次的像素在可视化里用特殊颜色标出来逐一修正。这类问题越早发现越好等到模型训练完再回头查标签就浪费了。5.2 切图边缘预测出现锯齿与拼接线现象训练完用滑窗预测大图拼回完整结果后在切块接缝处出现一条明显的竖线或横线同一栋楼在接缝两边被预测成不同类别。原因滑窗切图时每个块是独立推理的块边缘的上下文信息不足模型对边缘区域的预测置信度低。stride越小拼接线越不明显但推理时间成倍上升。解决第一个办法是预测时也使用重叠滑窗stride取patch_size的一半落在重叠区域的像素取多次预测的平均值再argmax。第二个办法是给每个像素计算到块边缘的距离权重距离边缘越近权重越低用加权平均做融合。我一般用第一个办法代码简单、效果立竿见影唯一的缺点是多花一倍推理时间。如果是毕设推理只有几十张验证图这个代价完全可接受。5.3 显存不足导致训练中断现象batch_size设了8第二个epoch刚开始就报CUDA out of memory整个训练中断。原因遥感语义分割的输入是512×512甚至更大UNet编码器的特征图张量很大加上AdamW的动量缓存显存需求比想象的高。很多人误以为8GB显卡能跑batch_size16实际上一半都跑不到。解决有三个手段按顺序试。第一batch_size降到2或4这是最简单的解法第二打开梯度累积每4个小batch累积一次梯度再反向传播等效于batch_size不变但显存占用只跟单batch有关第三启用torch.cuda.amp的混合精度训练显存直接减半还能加快训练速度。如果以上都做了还是不够把输入从512降到256精度损失不大但显存需求直接降到四分之一。5.4 loss震荡不收敛像个黑匣子现象训练loss在前10个epoch里上下跳动完全没有下降趋势或者loss下降但验证mIoU纹丝不动。原因大概率是学习率过大或者数据加载环节出了问题。遥感影像经常有全黑或全白的损坏块如果不做过滤这些块的loss会异常让曲线看起来像在震荡。另一个隐蔽原因是BatchNorm在batch_size1时统计量不稳定loss曲线会剧烈波动。解决先把lr降到1e-4以下观察20个epoch再把标签中全是255或单类占比超过99%的块过滤掉。如果batch_size只有2建议把BatchNorm换成GroupNorm或者干脆用梯度累积。还有一个排查技巧单独取一个batch的数据forward一次打印pred和target的形状、取值范围排除数据加载的问题再谈调参。5.5 验证指标虚高答辩时被追问就翻车现象训练结束时验证准确率有98%你满心欢喜拿去写论文老师看了一眼说“你Buildings的IoU是多少”你发现根本没算回去一算只有0.4。原因Acc在类别不平衡的数据上没有参考价值背景占了90%把所有像素都预测成背景也有90%的准确率。UNet在遥感分割上的真正硬指标是mIoU其次F1-score和Kappa系数这些指标对少数类别敏感能真实反映建筑物、道路的分割质量。解决在验证代码里除了Acc必须同时输出per-class IoU和mIoU。这部分的实现标准做法是用sklearn的confusion_matrix先统计每个类别的TP、FP、FN再逐类计算IoU最后取平均。下一章会给出一个完整可用的评估思路。6. 多拿分的验证技巧mIoU、可视化与消融实验组合拳训练完成只是毕设的一半真正拉开分数差距的是验证和实验设计。我建议做到三件事。第一件事是把验证指标算全。只报Acc会被答辩老师一句话问住mIoU、每类IoU、F1和Kappa才是一套完整的语义分割模型评估组合。评估函数可以直接用sklearn的confusion_matrix统计然后逐类计算IoU代码量很小但答辩时讲出来效果完全不一样。计算时注意把255的ignore区域从混淆矩阵里剔除否则结果会偏向背景。第二件事是可视化预测结果。光有数字不够要把原图、标签、预测图、错分图放在一起保存成一张对比图。错分图尤其重要它把预测和标签不一致的像素高亮出来你能一眼看出模型在哪些区域犯错——是道路边缘偏了还是水体漏检。我习惯把训练过程中每个epoch的验证mIoU和对应可视化图都存档最后选几张典型图放进论文这比贴十个表格都有说服力。第三件事是做一个标准的消融实验。毕设靠“UNet”三个字拿不到高分要证明你的改进有效。比如你做了多光谱融合就分别跑RGB输入和多光谱输入的对比如果加了注意力模块就加一组不加注意力模块的对照。所有实验固定同一个随机种子否则模型初始化差异会掩盖改进带来的真实收益。我自己的血泪习惯是每个配置跑三个随机种子取平均值标准差也在论文里列出来答辩老师看到标准差小了追问的意愿都会低很多。这三个技巧都不难实现但足够让你的毕设从“调包跑通”变成“有验证、有对比、有分析”。方法总比困难多遥感数据准备的过程确实枯燥但正因为枯燥能坚持把每一步做扎实的人最后的结果都不会差。希望帮到你。本文还有配套的精品资源点击获取