基于U-Net的遥感图像语义分割系统:从数据标注到模型调优的完整实战 简介这份资源是面向高校计算机、遥感与人工智能方向学生的毕业设计完整方案围绕U-Net网络实现遥感图像语义分割适合作为毕业设计、课程设计或期末大作业的参考项目。压缩包共68个文件约46.93MB包含6个Python源码文件、3个Jupyter Notebook、5个LaTeX论文源文件及配套PDF、32张结果图与6个矢量图另有字体、参考文献等辅助材料覆盖数据制作、模型搭建、训练、预测与论文撰写全流程。源码经本地编译验证可运行评审分达98分难度适中内容经助教老师审定。项目目录中train.py、model.py、data.py、utils.py等模块分工清晰配合create_dataset、train、predict三个Notebook可快速复现实验论文部分按章节组织便于直接参考写作。目前已有219人学习下载适合需要完整代码与论文框架、希望快速上手遥感分割任务的同学。1. 遥感图像语义分割与 U-Net这套毕业设计到底在做什么遥感图像语义分割说白了就是给卫星或无人机拍下来的地表照片做逐像素分类这块是建筑、那块是道路、旁边是植被、远处是水体。和普通图像分类只输出一个标签不同语义分割要求对每个像素都给出类别输出一张和原图等大的掩膜图。U-Net 之所以成为这个任务里被反复选用的基线网络核心在于它的编码器-解码器结构加跳跃连接编码器不断下采样提取语义解码器逐步上采样恢复分辨率而跳跃连接把浅层的高分辨率细节直接送到对应层级缓解了上采样过程中边界糊掉的问题。对于遥感场景里大量细长道路、零散建筑、不规则水岸线这种细节保留能力很关键。这套「基于 U-Net 网络的遥感图像语义分割系统」通常包含四块数据准备与标注、U-Net 模型训练、指标评估与可视化、以及一个能上传图片看分割结果的演示界面。它适合计算机、遥感、测绘方向的本科毕业生也适合想快速跑通一个语义分割闭环的初学者。下面按「数据怎么来 → 模型怎么搭 → 怎么训 → 坑在哪 → 怎么调好」的顺序讲透每一步都给到能直接抄的参数和命令。2. 数据准备遥感图像标注与语义分割数据集制作2.1 遥感数据集的三种来源与选型理由做遥感语义分割第一步不是写模型而是搞到带标注的数据。常见做法有三条路。第一条是直接用公开数据集比如面向地表覆盖分类的遥感分割数据集类别通常包括建筑、道路、植被、水体、裸地等优点是省事、有论文可对标缺点是类别体系和你的课题未必完全一致。第二条是自己标注用无人机或公开影像切片再用标注工具逐像素勾画工作量大但贴合课题。第三条是半自动先用一个预训练模型跑出粗掩膜再人工修正能省一半以上时间。选哪条取决于你的时间预算。如果毕设周期只有两三个月我一般建议公开数据集打底跑通全流程再拿少量自标注数据做微调这样既有可复现的指标又能体现「自己做了数据」的工作量。遥感图像标注和普通标注最大的区别是地物边界往往模糊比如稀疏植被和裸地过渡带标注时一定要先定一份类别定义文档否则不同人标出来的掩膜根本没法合并。2.2 标注规范与掩膜格式落地标注工具常见的是 labelme 和 ArcGIS 类工具。labelme 输出的是 JSON里面记录多边形顶点需要转成单通道的 PNG 掩膜像素值就是类别索引0 背景、1 建筑、2 道路……。转换脚本如下import json import numpy as np from PIL import Image, ImageDraw # 类别名到索引的映射必须和训练时的类别顺序完全一致 CLASS_MAP {background: 0, building: 1, road: 2, vegetation: 3, water: 4} def json_to_mask(json_path, out_path, height, width): with open(json_path, r, encodingutf-8) as f: data json.load(f) # 初始化全背景掩膜dtype 用 uint8 足够放下 255 个类别 mask np.zeros((height, width), dtypenp.uint8) img Image.fromarray(mask) draw ImageDraw.Draw(img) for shape in data[shapes]: label shape[label] if label not in CLASS_MAP: continue # 未定义类别直接跳过避免索引越界 # labelme 存的是 [[x1,y1],[x2,y2],...] 的浮点坐标 points [tuple(p) for p in shape[points]] draw.polygon(points, fillCLASS_MAP[label]) img.save(out_path) json_to_mask(sample.json, sample_mask.png, 512, 512)逻辑说明先建一张全 0 的掩膜再按多边形逐个填充类别索引。参数上height和width必须和原图切片尺寸一致否则掩膜和图像对不齐CLASS_MAP的顺序一旦定下就不能改训练、评估、可视化三处都要引用同一份。踩坑最多的地方是坐标labelme 的坐标是相对原图的如果你先切了图再标注就要保证切图和标注用的是同一套切片规则。2.3 数据集划分与增强参数划分比例常用 7:2:1 或 8:1:1。遥感图像有个特点相邻切片高度相关如果随机划分训练集和验证集里可能出现几乎一样的图指标虚高。稳妥做法是按整幅大图划分同一幅图的切片只进一个集合。增强方面水平翻转、垂直翻转、90 度旋转对遥感图都安全因为地物没有固定朝向但颜色抖动要慎用遥感影像的波段响应和颜色有物理含义抖太狠会让模型学到假的纹理。import albumentations as A train_tf A.Compose([ A.HorizontalFlip(p0.5), A.VerticalFlip(p0.5), A.RandomRotate90(p0.5), A.RandomCrop(256, 256), # 随机裁剪到训练尺寸 A.Normalize(mean(0.485, 0.456, 0.406), std(0.229, 0.224, 0.225)), ]) val_tf A.Compose([ A.Normalize(mean(0.485, 0.456, 0.406), std(0.229, 0.224, 0.225)), ])参数说明RandomCrop的尺寸要和网络输入对齐Normalize的均值方差用 ImageNet 统计量即可遥感图也适用。注意增强必须同时作用在图像和掩膜上albumentations 会自动处理但前提是掩膜以mask关键字传入传错位置会导致图像和标签错位这是新手最常见的翻车点之一。3. U-Net 模型搭建编码器、跳跃连接与损失函数3.1 U-Net 结构拆解与遥感场景适配标准 U-Net 分四段编码器下采样、瓶颈层、解码器上采样、跳跃连接。编码器每个 stage 是两次 3x3 卷积加一次 2x2 最大池化通道数从 64 翻倍到 512解码器每次上采样后与编码器对应层特征拼接再两次卷积。遥感图像和医学图像有相似之处——目标尺度差异大、边界细所以 U-Net 的跳跃连接在这里同样吃香。但直接照搬原版有两个问题。一是遥感图输入往往更大512 或 1024原版四层下采样后感受野可能不够覆盖大块地物二是类别不均衡严重道路、水体占比远小于背景。前者可以通过加深一层或改用空洞卷积缓解后者要靠损失函数解决。import torch import torch.nn as nn class DoubleConv(nn.Module): def __init__(self, in_ch, out_ch): super().__init__() self.block nn.Sequential( nn.Conv2d(in_ch, out_ch, 3, padding1, biasFalse), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue), nn.Conv2d(out_ch, out_ch, 3, padding1, biasFalse), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue), ) def forward(self, x): return self.block(x) class UNet(nn.Module): def __init__(self, in_ch3, num_classes5): super().__init__() # 编码器通道 64 - 128 - 256 - 512 self.enc1 DoubleConv(in_ch, 64) self.enc2 DoubleConv(64, 128) self.enc3 DoubleConv(128, 256) self.enc4 DoubleConv(256, 512) self.pool nn.MaxPool2d(2) # 瓶颈层 self.bottleneck DoubleConv(512, 1024) # 解码器上采样后拼接通道数减半 self.up4 nn.ConvTranspose2d(1024, 512, 2, stride2) self.dec4 DoubleConv(1024, 512) self.up3 nn.ConvTranspose2d(512, 256, 2, stride2) self.dec3 DoubleConv(512, 256) self.up2 nn.ConvTranspose2d(256, 128, 2, stride2) self.dec2 DoubleConv(256, 128) self.up1 nn.ConvTranspose2d(128, 64, 2, stride2) self.dec1 DoubleConv(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)) 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)逻辑说明DoubleConv是 U-Net 的基本单元两次卷积加 BN 加 ReLU。torch.cat沿通道维拼接这是跳跃连接的关键拼接后通道数翻倍所以dec4的输入是 1024。参数上num_classes必须等于你的类别数含背景in_ch一般是 3RGB如果用了多光谱波段就改成对应通道数。注意ConvTranspose2d的stride2和kernel_size2配合正好把特征图放大一倍和编码器对应层尺寸对齐尺寸对不上时拼接会直接报错。3.2 损失函数与类别不均衡处理遥感分割里背景往往占 60% 以上纯交叉熵会让模型倾向于全预测背景。常见做法是交叉熵加 Dice 损失组合Dice 对前景占比敏感能拉回不均衡。再进一步可以用带权交叉熵给稀有类别更高权重。import torch.nn.functional as F def dice_loss(pred, target, num_classes, eps1e-6): # 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() dice 0.0 for c in range(num_classes): p pred[:, c] t target_onehot[:, c] inter (p * t).sum() dice 1 - (2 * inter eps) / (p.sum() t.sum() eps) return dice / num_classes def combined_loss(pred, target, num_classes, ce_weight0.5): ce F.cross_entropy(pred, target) dc dice_loss(pred, target, num_classes) return ce_weight * ce (1 - ce_weight) * dc参数说明ce_weight控制两项比例0.5 是常用起点如果前景特别少可以降到 0.3 让 Dice 主导。eps防止除零。注意 Dice 是对每个类别单独算再平均这样稀有类别不会被背景淹没。踩坑点one_hot要求 target 是 long 类型且值在[0, num_classes)内标注里出现越界索引会直接崩转换掩膜时一定要检查最大值。4. 训练、评估与可视化从命令行到指标曲线4.1 训练循环与关键超参训练脚本要包含数据加载、前向、损失、反向、验证、保存最优权重。遥感分割常用 Adam 或 SGD学习率 1e-3 到 1e-4batch size 受显存限制512 切片一般 4 到 8。import torch from torch.utils.data import DataLoader device torch.device(cuda if torch.cuda.is_available() else cpu) model UNet(in_ch3, num_classes5).to(device) optimizer torch.optim.Adam(model.parameters(), lr1e-3, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max50) best_miou 0.0 for epoch in range(50): model.train() for img, mask in train_loader: img, mask img.to(device), mask.to(device) optimizer.zero_grad() pred model(img) loss combined_loss(pred, mask, num_classes5) loss.backward() optimizer.step() scheduler.step() # 验证阶段算 mIoU保存最优 miou evaluate(model, val_loader, device, num_classes5) if miou best_miou: best_miou miou torch.save(model.state_dict(), best_unet.pth) print(fepoch {epoch} miou {miou:.4f})逻辑说明weight_decay抑制过拟合CosineAnnealingLR让学习率平滑下降比固定学习率更容易收敛到好点。参数上T_max一般设成总 epoch 数。注意验证阶段一定要model.eval()并配合torch.no_grad()否则 BN 统计量会被验证数据污染指标不可信这是很多人指标忽高忽低的玄学来源。4.2 mIoU 与混淆矩阵的正确算法mIoU 是语义分割的核心指标对每个类别算交并比再对所有类别取平均。实现时用混淆矩阵累加最稳。import numpy as np def evaluate(model, loader, device, num_classes): model.eval() conf np.zeros((num_classes, num_classes), dtypenp.int64) with torch.no_grad(): for img, mask in loader: img img.to(device) pred model(img).argmax(dim1).cpu().numpy() mask mask.numpy() # 把预测和真值拉平用 bincount 累加混淆矩阵 idx mask.flatten() * num_classes pred.flatten() conf np.bincount(idx, minlengthnum_classes ** 2).reshape(num_classes, num_classes) ious [] for c in range(num_classes): inter conf[c, c] union conf[c, :].sum() conf[:, c].sum() - inter ious.append(inter / union if union 0 else float(nan)) return np.nanmean(ious)参数说明混淆矩阵行是真值、列是预测conf[c, c]是正确像素。union用行和加列和减交集避免重复计数。注意nanmean会忽略没有出现的类别如果某类在验证集里一个像素都没有它的 IoU 无意义直接跳过更合理。踩坑点argmax要在 softmax 之前还是之后其实 argmax 对单调变换不敏感直接对 logits 取就行省一次计算。4.3 分割结果可视化与演示界面毕设通常要一个能演示的系统。最省事的是 Gradio 或 Streamlit上传图片、跑模型、返回彩色掩膜。import gradio as gr import numpy as np from PIL import Image COLORS np.array([[0,0,0],[255,0,0],[0,255,0],[0,0,255],[255,255,0]], dtypenp.uint8) def predict(image): img np.array(image.resize((512, 512))) / 255.0 tensor torch.from_numpy(img).permute(2,0,1).float().unsqueeze(0).to(device) with torch.no_grad(): pred model(tensor).argmax(dim1).squeeze().cpu().numpy() color_mask COLORS[pred] return Image.fromarray(color_mask) gr.Interface(fnpredict, inputsgr.Image(typepil), outputsgr.Image()).launch()逻辑说明把输入缩放到训练尺寸归一化后送模型取 argmax 得到类别图再用颜色表映射成可视化掩膜。参数上COLORS的行数必须等于类别数顺序和CLASS_MAP一致。注意演示时输入尺寸和训练尺寸不一致会导致效果骤降界面上最好固定 resize 到 512或者用滑动窗口推理大图。5. 避坑与排查遥感 U-Net 训练中最容易翻车的五件事5.1 掩膜索引越界导致训练直接崩现象训练第一个 batch 就报CUDA error或index out of bounds。原因标注转换时某些像素值超过了num_classes - 1比如背景被误写成 255。解决转换后加一句assert mask.max() num_classes并在数据加载时打印每张掩膜的unique值越界立刻定位到具体文件。5.2 图像与掩膜错位导致指标虚低现象训练 loss 能降但 mIoU 一直在 0.2 左右上不去可视化一看边界全歪。原因增强时图像和掩膜用了不同的随机种子或者 resize 时一个用了双线性一个用了最近邻。解决掩膜 resize 必须用最近邻插值增强统一走 albumentations 的Compose保证同步。检查方法是把图像和掩膜叠加显示边界应该严丝合缝。5.3 类别不均衡导致小目标全丢现象建筑、道路 IoU 接近 0背景 IoU 0.95。原因纯交叉熵被背景主导。解决换成前面讲的交叉熵加 Dice 组合损失或者给交叉熵加类别权重权重可以按类别像素频率的倒数设置。验证时单独看每个类别的 IoU不要只看 mIoU否则小类被平均掉你都不知道。5.4 验证指标波动大怀疑模型有问题现象相邻 epoch 的 mIoU 忽高忽低差 0.1 以上。原因验证集太小或者验证时忘了eval()导致 BN 用 batch 统计量。解决先确认model.eval()和torch.no_grad()都加了如果验证集确实小可以增大验证比例或做交叉验证。另外学习率太大也会让指标震荡试试降到 1e-4。5.5 显存不够导致 batch size 只能设 1现象512 切片 batch size 设 4 就 OOM。原因U-Net 在 512 分辨率下特征图很大尤其瓶颈层。解决三个方向——用混合精度训练torch.cuda.amp能省近一半显存把输入降到 256 再随机裁剪或者减少编码器层数。混合精度是最省事的改动几行代码精度损失很小。scaler torch.cuda.amp.GradScaler() for img, mask in train_loader: optimizer.zero_grad() with torch.cuda.amp.autocast(): pred model(img.to(device)) loss combined_loss(pred, mask.to(device), num_classes5) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()参数说明autocast自动把卷积等算子转成 fp16GradScaler防止梯度下溢。注意验证阶段不需要 scaler直接 fp32 推理即可。6. 把 mIoU 再往上推三个我反复验证过的调优技巧第一个技巧是预训练编码器。U-Net 从零训练在小数据集上很容易过拟合把编码器换成在 ImageNet 上预训练过的骨干比如 ResNet 系列只随机初始化解码器mIoU 通常能涨 3 到 5 个点。做法是把enc1到enc4替换成骨干网络的对应 stage注意通道数要对齐跳跃连接取的是 stage 输出而不是单个卷积层输出。这个改动对毕设来说性价比极高因为「用了预训练」本身就是论文里能写的一段。第二个技巧是测试时增强TTA。推理时把图像水平翻转、垂直翻转各跑一遍把 softmax 概率平均后再 argmax边界会更稳。代价是推理时间翻几倍但毕设演示对速度不敏感值得做。def predict_tta(model, img_tensor): model.eval() probs [] with torch.no_grad(): for flip in [None, 2, 3]: x img_tensor if flip is None else torch.flip(img_tensor, dims[flip]) p torch.softmax(model(x), dim1) if flip is not None: p torch.flip(p, dims[flip]) # 概率图翻回来再平均 probs.append(p) return torch.stack(probs).mean(0).argmax(dim1)参数说明dims[2]是水平翻转dims[3]是垂直翻转翻转后概率图必须翻回原方向再平均否则叠加的是错位的图。这个细节错了TTA 反而会让结果变差。第三个技巧是后处理去小连通域。遥感分割结果里常有零星的错误像素块用连通域分析把面积小于阈值的区域并到周围最大类别视觉上干净很多。用 OpenCV 的connectedComponents几行就能做阈值按你的分辨率调512 切片一般设 50 到 100 像素。最后说个我自己的习惯每次改完一个超参或结构只改一个变量跑完记录 mIoU 和每类 IoU攒成一张表。毕设答辩时老师最爱问「你这个参数为什么这么设」有这张表你就能直接说「我试过 1e-3 和 1e-4前者收敛快但后期震荡最终选了带余弦退火的 1e-3」。这种基于实验的回答比任何理论解释都有说服力。希望帮到你。本文还有配套的精品资源点击获取