ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

遥感图像语义分割UNet实战:从数据标注到推理优化全流程

遥感图像语义分割UNet实战:从数据标注到推理优化全流程 简介面向遥感图像语义分割毕业设计的完整项目包以UNet神经网络为核心覆盖数据处理、模型搭建、训练预测与结果评估全流程适合计算机视觉方向的学生参考或复用。压缩包含69个文件约47MB主体包括6份Python源码及对应的pyc文件、3个Jupyter Notebook交互式实验记录、LaTeX论文源文件与PDF报告另有大量图像样本和结果图、TensorBoard与Jupyter启动脚本结构清晰便于定位。项目还提供readme说明、Git忽略文件等工程化配置可直接在本地复现训练过程。目前已有299人获取过这份资料适合需要快速开启课题或借鉴完整方案的高年级本科生。随附的毕业论文完整展示研究思路与实验细节能帮助理解遥感图像语义分割中的UNet结构、跳连设计、损失函数选择及IoU等评价指标。1. 遥感图像语义分割为什么绕不开UNet以及毕业设计用它到底值不值如果你最近在准备遥感方向的毕业设计十有八九会搜到“基于UNet的遥感图像语义分割”。这句话几乎成了这个方向的标配开场把UNet用在遥感图像上对建筑物、道路、水体、植被做像素级分类。UNet在医学图像分割上成名已久但把同样的结构平移到遥感图上表现并没那么理所当然。遥感图像的物体尺度跨度极大一张512像素的切片里可能同时出现几十米宽的河流和几个像素宽的道路这和细胞、肿瘤那种相对均匀的分布完全不同。许多人直接套用医学图像的标准UNet训练流程结果跑出来的图里道路断裂、建筑边缘毛糙、小目标直接消失。这篇笔记按我实际做过的方案把数据准备、网络结构取舍、训练参数、推理拼接和常见翻车点一次讲清楚。适合三类人正在做相关毕业设计的学生、第一次把UNet落到遥感数据上的工程师、以及想快速了解这套技术路线是否值得投入的团队。它解决的核心问题是怎么用最少的时间得到一份能过答辩、能拿去对比实验的结果。2. 遥感图像标注与切片先解决数据格式和标签错位的坑2.1 标注工具选型与标签像素值约定遥感语义分割的数据准备第一步不是写模型而是把标注格式统一好。常见的做法是用Labelme之类的开源工具在原图上画多边形导出JSON格式再转成逐像素的标签图。我一般会跳过JSON直接输出8位单通道PNG因为训练框架几乎都默认读PNG、BMP这类栅格标签而JSON还要二次转换徒增复杂度。这里有一个必须提前定死的约定类别编号要从0开始且0固定代表背景。假设你的数据有四类——背景、建筑、道路、水体那像素值约定就是0背景、1建筑、2道路、3水体。很多初学者习惯拿标注软件里“第一个画的类别”当类别1结果背景值变成1模型一训练全乱套。import numpy as np def json_to_mask(polygons, shape, class_map): polygons: list of dict, 每项含 points 坐标和 label shape: 图像高宽 (h, w) class_map: {background: 0, building: 1, road: 2, water: 3} mask np.zeros(shape, dtypenp.uint8) for poly in polygons: # 画出多边形区域fill_value 取类别编号 # 这里省略具体绘图库调用核心是保证 fill_value 来自 class_map ... return mask这里最关键的是把class_map单独做成一个常量文件后续所有数据加载、损失函数权重、评估脚本都从它读取避免在多个脚本里各写一份。标注完成后务必对每个切片做一次标签通道可视化。肉眼直接看原图很容易忽略错位但把标签图单独拉伸显示你立刻就能发现“道路跑到建筑上”之类的问题。2.2 大图滑窗切片的窗口与重叠率遥感原始影像动辄几千乘几千像素直接整图进UNet不现实。标准做法是用滑窗切成固定尺寸的patch。patch的尺寸一般取256或512。256在显存吃紧时比较稳512对中等尺度地物更友好像建筑物这类目标需要一定的上下文才能分辨边界。def sliding_window_crop(image, mask, tile_size512, stride448): 对原始影像和标签做同步切片。 stride tile_size 时切片之间有重叠能缓解目标被切断的问题。 tiles [] h, w image.shape[:2] for y in range(0, h - tile_size 1, stride): for x in range(0, w - tile_size 1, stride): img_tile image[y:y tile_size, x:x tile_size] msk_tile mask[y:y tile_size, x:x tile_size] tiles.append((img_tile, msk_tile)) # 最后一行/列往往够不到整块需要单独补一块边缘对齐的切片 if y tile_size h: ... return tiles重叠率不是越高越好。重叠意味着同样的像素被训练多次相当于一种隐式数据增强。但重叠率太高会让模型对目标边缘过度平滑推理时也很容易在拼接处留下模糊带。我一般会把stride设为 tile_size 的 75% 到 87.5%也就是重叠 12.5% 到 25%这个区间在信息利用和过拟合之间比较均衡。如果地物破碎、小目标多重叠多一点如果地物是大块连续区域重叠少一点。2.3 数据增强颜色抖动要克制几何增强放得开遥感语义分割的增强策略和自然图像分类差别很大。平移翻转这类几何增强随便用因为地物方向不敏感旋转90度、水平翻转、垂直翻转都能做。但颜色抖动要非常克制——你不能把蓝色水体的色相随机调成泛红那样模型会把水体学成裸土。遥感数据的物候差异和光照差异本来就大再人为制造辐射失真等于给模型制造无谓的类内方差。我常用的增强组合是随机水平翻转、随机垂直翻转、随机旋转90度、外加轻微的高斯模糊。高斯模糊对建筑边界有帮助因为它能模拟不同分辨率传感器对边缘的平滑。# 用 imgaug 或 albumentations 都行关键看是否支持 mask 同步变换 import albumentations as A train_transform A.Compose([ A.HorizontalFlip(p0.5), A.VerticalFlip(p0.5), A.RandomRotate90(p0.5), A.GaussianBlur(blur_limit(3, 7), p0.2) ])这里的要点是A.Compose同时接收 image 和 mask保证原图和标签做相同的空间几何变换。如果只用图像增强库而忘了传 mask模型会在完全错位的标签上训练等于白跑。颜色类增强我一律不加因为RGB遥感数据受季节、天气、传感器型号影响颜色特征本身就是不稳定的信息源加了反而破坏模型对纹理和几何结构的依赖。3. UNet结构细节与损失函数通道数、跳跃连接和类别不平衡3.1 标准UNet的双卷积和跳连接在遥感数据上的表现UNet的核心是编码器-解码器结构加跳跃连接。编码器不断下采样提取语义信息分辨率从512降到16甚至8这时候模型看到的是大范围上下文解码器再逐步上采样恢复空间分辨率。跳跃连接把编码器同尺度的特征图直接拼到解码器对应层相当于给上采样过程补充高分辨率细节信息。这个设计对遥感分割尤其合适建筑物边界需要高频细节而道路连续性需要全局上下文。但标准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)这是UNet最基本的双卷积块。注意遥感图像的BatchNorm行为。如果batch size小到2或4BatchNorm统计量会很不稳定。显存有限时要么用GroupNorm替换要么把batch size提升到8以上再配合梯度累积。我在实验中发现同样的UNet结构batch size从4提到8验证集mIoU一般能涨一到两个点这个提升不来自模型本身纯粹是BatchNorm的统计更稳定了。3.2 用预训练ResNet编码器替换UNet的encoder结构上可以让解码器保持UNet原样把纯卷积编码器换成torchvision里带ImageNet预训练权重的ResNet34。ResNet34的层数比原始UNet Encoder深它在前几层已经学到了边缘、纹理类的基础特征迁移到遥感图像上开局损失就比随机初始化低不少。import torchvision.models as models resnet models.resnet34(pretrainedTrue) encoder nn.Sequential(*list(resnet.children())[:-2])这里把ResNet34的最后两层去掉保留到layer4的输出作为最深层特征。要注意的是ResNet的layer4输出分辨率是输入的1/32对512输入就是16x16。UNet原版的深层是1/16分辨率所以网络结构里需要调整解码器第一层的输入通道数并在最深特征后补一个额外的下采样/上采样路径具体取决于你的特征图对齐策略。做毕业设计的话我建议直接用带预训练的版本单卡训练时损失下降速度肉眼可见地快省下的时间足够多跑几组对比实验。3.3 混合损失函数CrossEntropy DiceLoss 的权重配比遥感分割最大的训练障碍是类别不平衡。在一个城市切片里建筑可能占40%道路占15%车辆占不到1%。如果只用交叉熵模型会倾向于把车辆学成背景。DiceLoss对小类别更敏感因为它计算的是预测和标签的重叠度但纯DiceLoss在小目标上梯度波动大训练不稳定。常见做法是两者相加让交叉熵提供稳定的梯度让Dice约束整体重叠度。class CombinedLoss(nn.Module): def __init__(self, ce_weight0.6, dice_weight0.4): super().__init__() self.ce_weight ce_weight self.dice_weight dice_weight def forward(self, pred, mask): ce F.cross_entropy(pred, mask) dice dice_loss(pred, mask) # 自行实现或从第三方库引入 return self.ce_weight * ce self.dice_weight * dice权重配比取决于任务。如果你的类别分布整体均匀CE可以给到0.7到0.8如果小目标类别特别稀缺Dice权重提到0.5甚至0.6。但Dice权重过高会导致训练震荡我有一个阶段配到0.7loss曲线像心电图验证集分数反而下降。另一个务实做法是给稀缺类别买一个加权采样器让每个batch里都能看到足够多的稀有类别样本这比分担损失权重更直接。4. 训练全流程学习率调度、EMA和验证指标mIoU的计算口径4.1 优化器选AdamW学习率用warmup加余弦退火UNet做遥感分割优化器选AdamW几乎是默认best practice。SGD在充分调参后可能略胜一点点但它对学习率极其敏感遥感数据本身噪声大梯度方向不稳定SGD前期容易迷路。AdamW的权重衰减对UNet这种中等规模网络也友好不容易过拟合到背景。学习率方面先用几轮warmup把学习率从极小值逐步抬高再走余弦退火到接近零这种组合在语义分割任务上尤其稳。optimizer torch.optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-5) scheduler torch.optim.lr_scheduler.CosineAnnealingLR( optimizer, T_maxepochs - warmup_epochs, eta_min1e-6 )warmup一般占总epoch数的10%左右。如果你总共训80轮前8轮做warmup。学习率基线可以设在1e-4显存小导致batch size只有4左右时学习率建议降到5e-5。余弦退火的好处是后期学习率自动变小帮助模型在验证集附近更精细地收敛不用手动调低学习率分阶段降。4.2 梯度累积解决单卡显存不足的问题遥感数据标注图是8位单通道输入图像是三通道RGB显存压力主要来自分辨率。如果你只有6GB或8GB显存batch size可能连4都跑不起来。解决方法是用梯度累积——以小batch前向反向几次把梯度攒起来再更新参数。这样等效的batch size等于单次batch乘累积步数。accum_steps 4 scaler torch.cuda.amp.GradScaler() for it, (imgs, masks) in enumerate(train_loader): imgs, masks imgs.cuda(), masks.cuda() with torch.cuda.amp.autocast(): preds model(imgs) loss loss_fn(preds, masks) / accum_steps scaler.scale(loss).backward() if (it 1) % accum_steps 0: scaler.step(optimizer) scaler.update() optimizer.zero_grad()使用混合精度还能额外节约显存代价是BatchNorm统计量稍有漂移。如果显存实在紧张把输入从512降到384或者把UNet基础通道数从64降到48效果比强行上大patch更划算。注意梯度累积时机器的有效batch size变大后学习率可以对应上调但一般别超过原来的1.5倍。4.3 EMA权重平滑和mIoU的计算方式训练过程里保存模型权重如果直接保存每个epoch结束时的权重验证集分数往往波动。因为sgd或adam在训练后期仍然会在最优解附近震荡。一个简单有效的缓解方案是EMA——维护一份权重的滑动平均副本用于验证训练权重负责大步前进平均权重负责给出稳定预测。ema_model copy.deepcopy(model) ema_decay 0.99 def ema_update(ema_model, model, decayema_decay): with torch.no_grad(): for ema_p, p in zip(ema_model.parameters(), model.parameters()): ema_p.data.mul_(decay).add_(p.data, alpha1 - decay)验证指标的mIoU定义要保持一致否则实验结果没有可比性。mIoU的标准计算方法是每个类别算预测和标签的交并比然后对所有类别取平均。注意“所有类别”包括背景。如果数据里背景占比极高mIoU会被背景拉高掩盖真实类别上的失败所以在记录mIoU的同时一定要单独看每个类别的IoU尤其关注道路、车辆这类细长小目标。def compute_miou(preds, masks, num_classes): ious [] for cls in range(num_classes): pred_cls (preds cls) mask_cls (masks cls) inter (pred_cls mask_cls).sum().item() union (pred_cls | mask_cls).sum().item() iou inter / union if union 0 else 0.0 ious.append(iou) return sum(ious) / len(ious)这个函数看起来简单但它在逐像素计算时大图必须展平后整体算不能按batch分开算再平均那样会让每张图的背景占比权重不同得到的总mIoU失真。正确口径是把整个验证集所有像素汇总再算类别的并集和交集。5. 遥感语义分割典型翻车现场5个小目标、边界和拼接的坑5.1 标签类别编号错位模型在错误标签上训练了一整夜现象训练loss正常下降验证mIoU卡在某个低值不动可视化预测结果发现建筑预测成了道路道路预测成了背景。原因标注工具导出的标签顺序和训练代码里的class_map不一致最常见的是背景类别没有置0而是从1开始导致所有类别偏移一位。解决训练前写一个脚本读取某张标签图打印它的唯一像素值集合和class_map里的键值对一一核对。不要用肉眼看原图或标签的颜色显示直接统计值是最可靠的。5.2 小目标类别消失车辆在预测图上直接蒸发现象车辆类别的IoU在2%到5%之间徘徊个别验证图上车辆完全没被预测出来。原因车辆类别的像素占比可能不足1%交叉熵损失几乎不受它影响。再加上下采样到16x16特征图时一辆20像素宽的车在深层特征里已经只剩一两个像素的信息。解决训练阶段给车辆类别的交叉熵权重提高两到三倍同时把输入分辨率从512提升到640。如果显存不够只对包含车辆标注的样本做均匀下采样让每个batch都有车。推理阶段也可以对包含车辆区域的原图做二次局部放大预测。5.3 混合精度训练时验证集Nan损失值突然变成inf现象前20个epoch正常第25个epoch开始loss变成nan之后所有分数归零。原因混合精度训练把权重部分换成半精度浮点数遇到异常梯度就溢出成inf。常见触发点是学习率过高导致梯度范数爆炸或者BatchNorm在半精度下统计不稳定。解决先用梯度裁剪把最大梯度范数限制在3.0以内再把损失计算的缩放因子加上。如果梯度裁剪无效说明问题是数值不稳定可以把整个网络的关键层强制FP32或者从混合精度切换回全精度验证问题是否消失。很多情况下全精度训练虽然慢一些但对UNet这种网络反而省心。5.4 验证集分数高但整图推理结果出现接缝和纹理条带现象单patch验证mIoU达到85%把整张遥感图切成patch预测再拼回原图时patch边界处出现明显的接缝建筑边界在接缝处错位。原因滑窗推理时patch之间没有重叠模型在patch边缘的预测质量天然偏低。因为边界处的像素看到的上下文不够预测置信度整体下降拼接后就在边界处形成灰度带。解决推理时使用比训练更大的重叠率。训练时重叠加到25%推理时把stride设成patch大小的一半也就是重叠50%。每个像素取它在多个patch中的预测结果平均或者只取该像素位于patch中心区域时的预测。前者效果更稳定后者速度快一些。大多数框架里用前者更简单可靠。6. 推理阶段TTA、CRF后处理与概率输出最后三件能提分的小事训练收敛后从验证集到最终成图还有三个小技巧值得做。第一个是测试时增强TTA。推理时把原始图像做水平翻转、垂直翻转和旋转180度分别跑一次预测把四个结果的概率图相加取平均最后再取argmax作为类别。这个操作对几何类错误能稳定提升0.5到1.5个mIoU点几乎不用改动代码def predict_with_tta(model, img, flip_hTrue, flip_vTrue): probs [] x torch.from_numpy(img).float().unsqueeze(0).cuda() p0 torch.softmax(model(x), dim1) probs.append(p0) if flip_h: ph torch.softmax(model(torch.flip(x, dims[3])), dim1) probs.append(torch.flip(ph, dims[3])) # 垂直翻转同理 return torch.mean(torch.stack(probs), dim0)第二个是CRF后处理。DenseCRF能利用像素间的颜色相似性把预测图的碎块和毛刺抹平一点对道路、建筑这类有明确边界的类别效果比较明显对小碎物体反而可能直接抹没。我一般只在最终输出时对整图跑一遍参数里sxy_gaussian设在 2 到 3sxy_bilateral设在 80 到 120迭代三轮就够。跑太多次会过度平滑边界细节反而丢失。第三个技巧是输出概率图而不是直接输出argmax。保存每个像素的类别概率做成的npy文件答辩或调阈值时能随时换不同的阈值重新出图不用重跑模型。这个习惯帮我避免了无数次“阈值调了得重新预测”的返工。这一套走下来其实整个毕业设计真正花费时间的是数据标注和切片整理模型训练反而相对机械。我自己在那时会犯的一个血泪错误是急着把网络跑起来忘了先可视化标签结果一整晚训练都喂给了带偏差的数据。希望你开工前先花半小时做一次数据完整性自检。希望这套流程能帮你省下同样的一晚。本文还有配套的精品资源点击获取
返回列表