尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

遥感图像语义分割实战:UNet从数据准备到训练调优全流程

发布时间:2026/9/28 18:05:26

资讯中心
01
ARTICLE

遥感图像语义分割实战:UNet从数据准备到训练调优全流程

遥感图像语义分割实战:UNet从数据准备到训练调优全流程
简介这份毕业设计资源包围绕UNet神经网络在遥感图像语义分割中的应用展开面向计算机视觉方向的高年级本科生与研究生帮助读者理解并复现像素级分类任务涵盖建筑物、水体、植被等典型地物的分割流程。压缩包共69个文件、约46.92MB包含6个Python源码文件与3个Jupyter Notebook用于模型搭建、训练与预测32张png与6个svg图表辅助结果展示另有5个tex论文源文件、3个ttf字体及pdf报告完整覆盖从数据预处理、CNN与UNet结构实现到训练可视化的技术链路。资源中附有毕业论文与实验报告配合TensorBoard与Jupyter启动脚本便于读者对照代码复现实验、理解跳跃连接与损失函数设计并参考IoU等指标完成结果评估。目前已有299人学习下载适合作为遥感分割入门与毕业设计参考的实战范例。1. 遥感图像语义分割与 UNet从毕设选题到能跑通的工程路径遥感图像语义分割这件事真正上手做过的人都知道难点从来不在 UNet 本身。UNet 的结构 2015 年就公开了编码器-解码器加跳跃连接网上 unet 代码一搜一大把跑一个 unet 网络在 MNIST 或者 VOC 上出个结果可能半小时就搞定。但一旦把数据换成遥感图像事情就变了影像幅面动辄几千乘几千像素地物类别极不均衡标注成本高得离谱语义分割数据集制作这一步就能卡掉一半人。这个标题对应的毕设核心要解决的是「怎么把 UNet 真正用在遥感图像上从数据准备到训练到出图整条链路跑通」。适合正在做遥感方向毕设的本科生、刚转遥感 CV 的算法工程师以及需要快速验证某个区域地物分类可行性的从业者。下面按我实际做过的路径把选型理由、代码、参数和踩过的坑一条条讲清楚。2. 遥感语义分割的数据准备从原始影像到可训练样本2.1 遥感图像标注为什么不能用普通标注工具的思路普通自然图像标注一张图几个到几十个目标标完就完事。遥感图像不一样一景影像覆盖几十平方公里你要标的是每个像素的类别——建筑、道路、水体、植被、裸地。常见做法是先用 GIS 工具QGIS 或 ArcGIS把影像切片成 512×512 或 1024×1024 的瓦片再在瓦片上做像素级标注。这里有个血泪经验不要试图在一整景影像上直接标软件会卡死标注人员也会疯。标注格式我一般用两种一种是 PNG 灰度掩膜像素值 0/1/2/3 对应不同类别另一种是 GeoJSON 矢量后期再栅格化。毕设场景推荐 PNG 掩膜因为直接能被 PyTorch 的 Dataset 读取省去栅格化步骤。如果你手头只有矢量标注用 rasterio 做栅格化import rasterio from rasterio.features import rasterize import geopandas as gpd import numpy as np # 读取原始影像获取地理变换和尺寸 with rasterio.open(tile_001.tif) as src: transform src.transform out_shape (src.height, src.width) # 读取矢量标注 gdf gpd.read_file(labels_001.geojson) # 按类别字段栅格化class_id 从 1 开始0 留给背景 shapes zip(gdf.geometry, gdf[class_id]) mask rasterize( shapesshapes, out_shapeout_shape, transformtransform, fill0, dtypenp.uint8 ) # 保存为 PNG方便后续 Dataset 读取 from PIL import Image Image.fromarray(mask).save(mask_001.png)这段代码的关键参数是fill0和dtypenp.uint8。fill0表示没有标注覆盖的区域归为背景类遥感里背景通常是「其他」或「未分类」。dtype必须和你的类别数匹配如果类别超过 255 就得用 uint16但大多数毕设 5-10 类用 uint8 足够。栅格化时注意矢量坐标系要和影像一致否则掩膜会整体偏移这个坑我踩过排查了一下午才发现是 CRS 不匹配。2.2 语义分割数据集制作的目录结构与划分脚本数据集目录我习惯这样组织简单清晰后面换数据集也不用改代码dataset/ images/ train/ val/ masks/ train/ val/划分比例按 7:2:1 或 8:1:1遥感数据量少的时候验证集可以只留 10%。写个脚本自动划分别手动拖文件import os import random import shutil def split_dataset(img_dir, mask_dir, out_dir, ratios(0.7, 0.2, 0.1)): files sorted(os.listdir(img_dir)) random.seed(42) # 固定随机种子保证可复现 random.shuffle(files) n len(files) n_train int(n * ratios[0]) n_val int(n * ratios[1]) splits { train: files[:n_train], val: files[n_train:n_train n_val], test: files[n_train n_val:] } for split, names in splits.items(): img_out os.path.join(out_dir, images, split) mask_out os.path.join(out_dir, masks, split) os.makedirs(img_out, exist_okTrue) os.makedirs(mask_out, exist_okTrue) for name in names: shutil.copy(os.path.join(img_dir, name), os.path.join(img_out, name)) # 掩膜文件名和影像保持一致后缀换成 png mask_name os.path.splitext(name)[0] .png shutil.copy(os.path.join(mask_dir, mask_name), os.path.join(mask_out, mask_name)) split_dataset(raw/images, raw/masks, dataset)random.seed(42)这行别省毕设答辩时老师问「你怎么保证结果可复现」有固定种子就是标准答案。另外注意影像和掩膜的文件名必须一一对应我见过有人用不同命名规则训练时读不到掩膜直接报错还以为是代码问题。2.3 数据增强在遥感场景下的边界遥感图像增强和自然图像不太一样。翻转、旋转 90 度、180 度这些没问题因为遥感是俯视图旋转不改变地物语义。但颜色抖动要小心——遥感影像的波段反射率是有物理意义的你把 NDVI 相关的近红外波段乱调水体可能被调成植被。我一般只做几何增强颜色最多做轻微的亮度和对比度扰动。用 albumentations 写增强管道import albumentations as A from albumentations.pytorch import ToTensorV2 train_transform A.Compose([ A.RandomRotate90(p0.5), A.HorizontalFlip(p0.5), A.VerticalFlip(p0.5), A.RandomBrightnessContrast(brightness_limit0.1, contrast_limit0.1, p0.3), A.Normalize(mean(0.485, 0.456, 0.406), std(0.229, 0.224, 0.225)), ToTensorV2() ]) val_transform A.Compose([ A.Normalize(mean(0.485, 0.456, 0.406), std(0.229, 0.224, 0.225)), ToTensorV2() ])brightness_limit和contrast_limit都压到 0.1就是防止颜色扰动过头。Normalize 的均值方差用的是 ImageNet 的统计量如果你的遥感影像是 8 位 RGB 三通道可以直接用如果是多光谱得自己算数据集的均值和方差否则归一化后数值分布不对训练收敛会慢很多。3. UNet 模型搭建与训练跑通第一个 baseline3.1 UNet 代码结构拆解与遥感场景的适配点UNet 的核心就三块下采样编码器、上采样解码器、跳跃连接。网上 unet 代码很多但直接拿来跑遥感图像有几个地方要改。第一输入通道数。遥感影像可能是 4 通道RGB近红外甚至更多第一层卷积的in_channels要改。第二输出类别数。out_channels等于你的类别数包括背景。第三如果影像尺寸不是 2 的整数次幂下采样四次后尺寸可能对不上跳跃连接会报维度错误。下面是我常用的 UNet 实现结构清晰改起来方便import torch import torch.nn as nn import torch.nn.functional as F 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_classes5): super().__init__() # 编码器 self.enc1 DoubleConv(in_channels, 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) # 输出层1x1 卷积映射到类别数 self.out_conv 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_conv(d1)torch.cat([...], dim1)里的dim1是通道维度拼接这是 UNet 跳跃连接的关键。ConvTranspose2d的stride2保证上采样倍率和下采样对称。如果你的输入尺寸是 512×512经过四次 pool 变成 32×32再四次上采样回到 512×512维度刚好对上。但如果输入是 500×500下采样后是 31×31上采样回来是 496×496和编码器特征图尺寸差 4 个像素torch.cat直接报错。解决办法是训练前把影像 padding 到 2 的整数次幂或者用F.interpolate在上采样后对齐尺寸。3.2 训练脚本的关键参数与损失函数选择遥感语义分割最常见的翻车点是类别不均衡。道路、建筑可能占 30%水体占 5%稀有类别可能不到 1%。如果直接用 CrossEntropyLoss模型会倾向于预测多数类稀有类别的 IoU 惨不忍睹。我一般用 CrossEntropyLoss 加 DiceLoss 的组合或者直接用带类别权重的 CrossEntropyLoss。import torch import torch.nn as nn from torch.utils.data import DataLoader, Dataset from PIL import Image import numpy as np import albumentations as A from albumentations.pytorch import ToTensorV2 class RemoteSensingDataset(Dataset): def __init__(self, img_dir, mask_dir, transformNone): self.img_dir img_dir self.mask_dir mask_dir self.transform transform self.names sorted(os.listdir(img_dir)) def __len__(self): return len(self.names) def __getitem__(self, idx): name self.names[idx] img np.array(Image.open(os.path.join(self.img_dir, name)).convert(RGB)) mask_name os.path.splitext(name)[0] .png mask np.array(Image.open(os.path.join(self.mask_dir, mask_name))) if self.transform: augmented self.transform(imageimg, maskmask) img augmented[image] mask augmented[mask] return img, mask.long() # 类别权重根据训练集统计得到稀有类别给高权重 class_weights torch.tensor([0.5, 2.0, 1.5, 3.0, 2.5]).cuda() ce_loss nn.CrossEntropyLoss(weightclass_weights) def dice_loss(pred, target, smooth1e-6): pred torch.softmax(pred, dim1) target_one_hot F.one_hot(target, num_classespred.shape[1]).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 smooth) / (union smooth) return 1 - dice.mean() # 训练循环 model UNet(in_channels3, num_classes5).cuda() optimizer torch.optim.Adam(model.parameters(), lr1e-3) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max50) for epoch in range(50): model.train() for imgs, masks in train_loader: imgs, masks imgs.cuda(), masks.cuda() preds model(imgs) loss ce_loss(preds, masks) dice_loss(preds, masks) optimizer.zero_grad() loss.backward() optimizer.step() scheduler.step()class_weights的数值要根据你数据集的类别像素占比来定占比越低的类别权重越高。我一般先跑一遍统计脚本算出每个类别的像素数然后取倒数再归一化。dice_loss里的smooth1e-6是防止除零别省。学习率用 1e-3 配 Adam 是常规起点如果 loss 震荡就降到 1e-4。CosineAnnealingLR 的T_max设成总 epoch 数让学习率平滑降到接近零。3.3 评价指标mIoU 和 PA 到底看哪个毕设答辩老师最爱问「你的精度是多少」这时候你得说清楚是 mIoU 还是 PA。PAPixel Accuracy是像素准确率所有预测对的像素除以总像素类别不均衡时这个指标会虚高——全预测成背景也能有 80%。mIoUMean Intersection over Union是每个类别的 IoU 求平均稀有类别表现差会直接拉低 mIoU更能反映真实分割质量。def compute_miou(pred, target, num_classes): pred torch.argmax(pred, dim1) ious [] for cls in range(num_classes): pred_mask (pred cls) target_mask (target cls) intersection (pred_mask target_mask).sum().float() union (pred_mask | target_mask).sum().float() if union 0: continue # 该类别在 batch 中不存在跳过 ious.append((intersection / union).item()) return sum(ious) / len(ious) if ious else 0.0union 0时跳过这个类别否则会除零。验证时每个 batch 算一次 mIoU最后取平均。注意torch.argmax(pred, dim1)得到的是每个像素的预测类别维度从[B, C, H, W]变成[B, H, W]和 target 维度一致才能比较。4. 训练过程中的避坑与排查4.1 loss 不下降甚至上升现象训练几个 epoch 后 loss 卡在某个值不动或者突然变成 NaN。原因通常有三个学习率太大、数据归一化不对、损失函数和输出不匹配。先检查学习率把 1e-3 降到 1e-4 试试。再检查 Normalize 的均值和方差是不是和你的数据分布匹配遥感影像如果没做归一化直接送进网络像素值 0-255 会导致梯度爆炸。最后确认 CrossEntropyLoss 的输入是 logits 不是 softmax 后的概率如果模型最后加了 softmaxCrossEntropyLoss 内部会再做一次结果就错了。4.2 预测结果全是一个类别现象验证集上模型输出全是背景或者全是建筑。原因大概率是类别不均衡太严重模型找到了「全预测多数类」这个局部最优。解决办法加类别权重、加 DiceLoss、或者对稀有类别做过采样。我一般先统计训练集每个类别的像素占比如果最大类和最小类差 100 倍以上必须加权重。另外检查一下掩膜的像素值是不是从 0 开始连续如果标注时用了 1/2/3/4 但背景是 255类别数就对不上模型学出来的东西全是乱的。4.3 跳跃连接维度不匹配报错现象RuntimeError: Sizes of tensors must match except in dimension 1。原因就是前面说的输入尺寸不是 2 的整数次幂。解决办法在 Dataset 里把影像和掩膜都 resize 到 512×512或者用A.PadIfNeeded做 padding。如果不想改数据就在 UNet 的 forward 里上采样后加F.interpolate对齐d4 self.dec4(torch.cat([F.interpolate(self.up4(b), sizee4.shape[2:], modebilinear), e4], dim1))modebilinear比最近邻插值更平滑对分割边界更友好。但这是补救措施最好还是在数据准备阶段就统一尺寸。4.4 显存不够导致训练中断现象CUDA out of memory。遥感图像尺寸大UNet 的瓶颈层通道数 1024显存占用很高。解决办法把 batch size 降到 2 或 1用梯度累积模拟大 batch或者把输入尺寸从 512 降到 256再不行就把 UNet 的通道数减半64 起步改成 32 起步。梯度累积的写法accum_steps 4 for i, (imgs, masks) in enumerate(train_loader): preds model(imgs) loss ce_loss(preds, masks) / accum_steps loss.backward() if (i 1) % accum_steps 0: optimizer.step() optimizer.zero_grad()loss / accum_steps是为了让梯度尺度一致不然累积后的梯度会放大 accum_steps 倍。4.5 验证集指标远低于训练集现象训练集 mIoU 0.8验证集只有 0.4。这是过拟合的典型表现。遥感数据量少的时候特别容易发生。解决办法加数据增强、加 Dropout、加权重衰减。在 DoubleConv 里加nn.Dropout2d(0.2)优化器加weight_decay1e-4。如果还不行就用预训练编码器比如把编码器换成 ResNet34 的前几层用 ImageNet 预训练权重初始化小数据集上效果提升很明显。5. 让 UNet 在遥感场景下真正能用的几个进阶技巧5.1 用预训练编码器替换 UNet 下采样路径纯 UNet 从零训练在小数据集上很难收敛到理想精度。常见做法是把编码器换成 ResNet 或 EfficientNet 的前几层加载 ImageNet 预训练权重。这样编码器已经学会了提取边缘、纹理等底层特征微调时只需要学遥感特有的高层语义。代码上就是把enc1到enc4换成torchvision.models.resnet34(pretrainedTrue)的对应层注意第一层卷积的输入通道数要改成你的影像通道数预训练权重对不上的通道可以随机初始化或者用均值填充。5.2 多尺度推理提升边界精度遥感图像里地物边界往往比较模糊单尺度推理容易在边界处出错。我一般用多尺度推理把影像分别缩放到 0.75、1.0、1.25 倍各自跑一遍模型然后把预测概率图缩回原尺寸求平均。这样对大目标和小目标都更友好mIoU 通常能涨 1-2 个点。代价是推理时间变成三倍毕设场景完全可接受。def multi_scale_inference(model, img, scales[0.75, 1.0, 1.25]): probs [] for scale in scales: h, w img.shape[2:] resized F.interpolate(img, size(int(h * scale), int(w * scale)), modebilinear) pred torch.softmax(model(resized), dim1) pred F.interpolate(pred, size(h, w), modebilinear) probs.append(pred) return torch.stack(probs).mean(dim0)torch.stack(probs).mean(dim0)把多个尺度的概率图堆叠后取平均比直接平均 logits 更稳定因为 softmax 后的值在 0-1 之间不会出现某个尺度 logits 过大主导结果的情况。5.3 一个我常用的训练习惯每次改完模型或数据管道先拿 10 张图跑 5 个 epoch看 loss 能不能降到合理范围。如果能再上全量数据。这个习惯帮我省了无数次通宵等训练结果的时间。另外训练日志里一定要记录每个类别的 IoU不要只看平均 mIoU。有时候平均 mIoU 涨了但某个关键类别比如水体的 IoU 反而降了这种细节在毕设答辩时被问到会很被动。希望帮到你。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。