遥感图像滑坡识别:基于PyTorch与U-Net的语义分割实战

发布时间:2026/9/13 1:58:39
遥感图像滑坡识别:基于PyTorch与U-Net的语义分割实战 简介一份基于PyTorch框架与卷积神经网络实现遥感图像滑坡识别的完整工程面向深度学习初学者、遥感方向学生及需要完成课程设计或期末大作业的开发者。项目覆盖数据划分、模型训练与验证全流程内含AlexNet、ResNet等经典网络结构及数据划分脚本并附有已训练好的模型权重用户既可自行训练也可直接加载权重进行推理验证。压缩包共15个文件包括7个Python源码、1个pth模型文件、配置文件与说明文档等整体约52.73MB目录结构清晰方便按需查阅。目前已有70人学习使用适用于遥感灾害监测、地质预警等教学与科研场景。资源源自网络分享仅供学习交流请勿用于商业用途。对于希望将CNN应用于遥感图像分析的读者这套代码从数据准备到模型应用提供了完整参照能加快项目落地与学习进度。1. 遥感图像滑坡识别一个典型的像素级分割任务滑坡识别在遥感影像处理里本质上不是目标检测那样的“框出滑坡”而是逐像素判断这个像素属于滑坡体还是背景。灾后应急、地质灾害调查、国土空间监测都要先把“滑坡到底圈在哪”这件事做准。传统做法用光谱指数、纹理阈值在裸土和植被混合的区域很容易误判泛化能力也差。深度学习CNN把特征提取和分类做成端到端卷积层自动学习滑坡的纹理、边界和上下文精度和鲁棒性都上了一个台阶。PyTorch这边生态成熟从数据集封装、预训练主干到分布式训练都有现成组件很适合在这个任务上搭起完整流程。下面是完整落地路径主线就是把这套源码、数据集和模型拆开讲透。2. CNN从特征提取到U型分割滑坡识别模型的网络设计2.1 滑坡体在遥感影像上的特征与CNN结构图的对应关系滑坡体的影像特征和常规分割对象不太一样。首先滑坡体形状极不规则没有建筑物那种锐利直线边界其次滑坡区域的纹理是破碎的裸土、碎石、植被残骸混在一起颜色上又和周围裸地接近只用RGB阈值几乎不可能分离。好在CNN结构图天然匹配这个需求浅层卷积核捕捉边缘和纹理中层组合出斑块状Pattern深层通过扩大感受野感知滑坡体与山体、沟谷的位置关系最终在空间维度上输出逐像素分类结果。输入(3, H, W) - Conv3x3ReLU - MaxPool - Conv3x3ReLU - MaxPool - Conv3x3ReLU - 上采样 - 逐像素分类这个基础结构说明一个关键点如果网络只做全局分类最后特征图被压成一维向量位置信息就丢了。滑坡识别要求输出和输入同分辨率的掩膜所以网络必须保留空间维度这就引出了分割网络最常用的U型结构。很多开源的滑坡识别源码都采用U型设计原因就在这里——它不只是一个CNN分类器而是编码器加解码器的组合。2.2 为什么U-Net比普通CNN分类网络更适合滑坡提取滑坡边界的精细程度直接影响灾害评估中面积统计的可靠性。纯下采样卷积网络恢复不到原始分辨率直接造成小滑坡漏检和边界膨胀。U型结构通过跳跃连接把编码器各层的高分辨率特征拼到解码器对应层让边界信息不经过层层下采样就被保留下来。对滑坡这种“边缘模糊”的目标这个机制非常有效。下表是滑坡识别中备选网络的对比选型时候可以直接参考。网络结构跳跃连接参数量滑坡边界恢复能力适用场景FCN-8s无仅上采样中一般边界偏粗快速初筛U-Net同层拼接中好边界细节恢复默认首选DeepLabV3ASPP模块替代中大较好感受野大大尺度滑坡体我一般第一版直接用U-Net训练稳定、显存压力小、改造成本低。如果影像分辨率特别高比如0.5米无人飞机影像再换DeepLabV3这类带空洞卷积的结构。2.3 编码器加预训练主干的有效性滑坡数据集往往只有几百到几千张裁剪影像从头训练一个深层CNN很容易过拟合。常见做法是编码器部分换成ImageNet预训练的ResNet34或VGG16解码器保持U-Net结构。预训练权重已经学到大量纹理和边缘特征滑坡体虽然和常规物体不同但底层滤波器是通用的。训练时默认全量微调不冻结主干因为遥感影像的分布和ImageNet差异还是存在只训练解码器效果通常不够好。segmentation_models_pytorch库里封装了这些组合一行代码就能加载预训练U-Net。如果不想引入第三方库手写U-Net也很简单下面是一个最小实现片段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_classes1): super().__init__() self.enc1 DoubleConv(in_channels, 64) self.pool nn.MaxPool2d(2) self.enc2 DoubleConv(64, 128) self.enc3 DoubleConv(128, 256) self.enc4 DoubleConv(256, 512) self.enc5 DoubleConv(512, 1024) self.up5 nn.ConvTranspose2d(1024, 512, 2, stride2) self.dec5 DoubleConv(1024, 512) self.up4 nn.ConvTranspose2d(512, 256, 2, stride2) self.dec4 DoubleConv(512, 256) self.up3 nn.ConvTranspose2d(256, 128, 2, stride2) self.dec3 DoubleConv(256, 128) self.up2 nn.ConvTranspose2d(128, 64, 2, stride2) self.dec2 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)) e5 self.enc5(self.pool(e4)) d5 self.dec5(torch.cat([self.up5(e5), e4], dim1)) d4 self.dec4(torch.cat([self.up4(d5), e3], dim1)) d3 self.dec3(torch.cat([self.up3(d4), e2], dim1)) d2 self.dec2(torch.cat([self.up2(d3), e1], dim1)) return self.out(d2)DoubleConv是U-Net的基本模块做两次卷积加归一化编码器逐步压缩空间尺寸同时增加通道数解码器通过ConvTranspose2d上采样并拼接编码器对应层。最后的1x1卷积把通道压缩成1输出每个像素属于滑坡的概率。注意这里没有加Sigmoid训练时交给损失函数处理。3. 滑坡遥感数据集裁剪、标注与PyTorch数据管线3.1 遥感影像数据的来源与标注格式滑坡识别源码能顺利跑起来数据集组织是关键。常见数据源包括高分一号/二号国产卫星影像、哨兵2号多光谱影像、无人机航拍DOM。前两者是GeoTIFF格式单景影像动辄上亿像素没法直接丢进GPU。标注方面野外调查采集的多边形shp文件需要栅格化成掩膜或者直接用GIS软件导出PNG格式的标签图。数据目录建议按下面的结构组织源码里也好写路径目录内容images/裁剪后的原始影像PNG或JPGmasks/与影像同名的标签掩膜0为背景255为滑坡train.txt训练集文件名列表每行一个文件名val.txt验证集文件名列表掩膜中滑坡区域通常用255标注白色背景为0黑色。转成Tensor时除以255让标签落在0和1。3.2 大影像滑窗裁剪与重叠采样原始遥感影像太大必须做滑窗裁剪。窗口大小需要同时兼顾显存和上下文信息512×512比较平衡太小了滑坡体的上下文不足模型容易把孤立的裸土误判为滑坡太大了显存占用高batch size上不去。裁剪时加一些重叠避免滑坡体恰好被窗口边缘切断。下面是裁剪脚本的核心逻辑import numpy as np from PIL import Image import os def crop_image_with_mask(image_path, mask_path, save_dir, size512, overlap128): img np.array(Image.open(image_path)) mask np.array(Image.open(mask_path)) h, w img.shape[:2] stride size - overlap for y in range(0, h, stride): for x in range(0, w, stride): y_end min(y size, h) x_end min(x size, w) patch_img img[max(0, y_end-size):y_end, max(0, x_end-size):x_end] patch_mask mask[max(0, y_end-size):y_end, max(0, x_end-size):x_end] if patch_img.shape[0] ! size or patch_img.shape[1] ! size: pad_img np.zeros((size, size, 3), dtypenp.uint8) pad_mask np.zeros((size, size), dtypenp.uint8) pad_img[:patch_img.shape[0], :patch_img.shape[1]] patch_img pad_mask[:patch_mask.shape[0], :patch_mask.shape[1]] patch_mask patch_img, patch_mask pad_img, pad_mask Image.fromarray(patch_img).save( os.path.join(save_dir, images, f{os.path.basename(image_path)[:-4]}_{y}_{x}.png)) Image.fromarray(patch_mask).save( os.path.join(save_dir, masks, f{os.path.basename(image_path)[:-4]}_{y}_{x}.png))代码里overlap128意味着步长stride384相邻窗口有128像素重叠。边缘不足size的部分用零填充不影响训练分布。需要保证影像和掩膜的地理坐标系一致最好用GDAL读取而不是PIL避免坐标错位。3.3 归一化、数据增强与Dataset类实现遥感影像的像素值范围和自然图像不同哨兵2号多光谱数据可能是16位整型直接除以255会得到错误结果。源码里必须根据数据特性确定归一化方式8位影像除以255是常规操作16位影像先按百分位截断再归一化到0-1例如取2%到98%分位数做线性拉伸。滑坡识别的数据增强要克制过强的色彩扰动会让模型学到错误的颜色关联。import torch from torch.utils.data import Dataset from PIL import Image import numpy as np import torchvision.transforms as T class LandslideDataset(Dataset): def __init__(self, image_dir, mask_dir, file_list, use_augFalse): self.image_paths [] self.mask_paths [] with open(file_list, r) as f: for line in f: name line.strip() self.image_paths.append(f{image_dir}/{name}.png) self.mask_paths.append(f{mask_dir}/{name}.png) self.use_aug use_aug self.base_transform T.Compose([ T.ToTensor(), T.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) def __len__(self): return len(self.image_paths) def __getitem__(self, idx): image Image.open(self.image_paths[idx]).convert(RGB) mask Image.open(self.mask_paths[idx]) if self.use_aug: p np.random.rand() if p 0.5: image T.functional.hflip(image) mask T.functional.hflip(mask) p np.random.rand() if p 0.5: image T.functional.vflip(image) mask T.functional.vflip(mask) image self.base_transform(image) mask np.array(mask, dtypenp.float32) / 255.0 mask torch.from_numpy(mask).unsqueeze(0) return image, mask这里用了ImageNet的均值和标准差做标准化。但需要注意数据增强用的是镜像翻转而不是RandomResizedCrop因为滑坡体形状和上下文经随机裁剪改变过大反而不利于学习。开源代码里有人用随机亮度饱和度扰动这个按需使用我建议滑坡识别只做几何增强颜色扰动幅度要小。4. PyTorch训练滑坡识别模型损失函数、epoch与评估4.1 环境准备与PyTorch基础框架搭建训练之前先把PyTorch环境装好。常见做法是用Anaconda建独立环境命令如下conda create -n landslide python3.9 conda activate landslide pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118--index-url部分指定CUDA 11.8版本具体根据机器驱动版本调整。没有NVIDIA GPU的机器直接pip install torch torchvision装CPU版。PyTorch基础框架的核心就是Dataset、DataLoader、nn.Module、optimizer和loss这五个组件串起来下面训练脚本也是按这个顺序组织。4.2 损失函数选择BCE与Dice的组合滑坡体在影像中占比通常只有几个百分点正负样本极度不平衡。如果直接用二元交叉熵模型会学会把所有像素预测为背景因为这样损失已经足够低。业界通用的解法是BCE加Dice LossDice系数衡量预测掩膜和真实掩膜的重叠程度对类别不平衡不敏感。import torch.nn.functional as F def bce_dice_loss(pred, target): bce F.binary_cross_entropy_with_logits(pred, target) pred_prob torch.sigmoid(pred) smooth 1.0 intersection (pred_prob * target).sum() dice 1.0 - (2.0 * intersection smooth) / (pred_prob.sum() target.sum() smooth) return bce dice代码中pred是网络最后一层的原始输出logitstarget是0和1的标签张量。Dice部分不需要对target做sigmoid因为它已经是0-1。smooth防止分子分母都是零时出现除零错误。两个损失相加BCE提供像素级梯度Dice提供区域级梯度两者互补在滑坡分割上是稳定有效的组合。4.3 优化器、学习率与深度学习epoch的配合滑坡识别数据集规模通常不大训练过程对超参更敏感。优化器用AdamW初始学习率1e-4权重衰减1e-4。batch size根据显存调整512×512的输入在12GB显存上batch size设为4比较稳妥。深度学习中epoch不是拍脑袋定的滑坡识别一般100到200个epoch就够关键在于配合验证集上的Early Stopping验证IoU连续15个epoch不提升就停止。epoch lr train_loss val_iou 1 1e-4 0.482 0.312 50 5e-5 0.213 0.651 100 1e-5 0.148 0.703 120 1e-5 0.139 0.711学习率用余弦退火或者阶梯下降都行。我一般前50个epoch用固定1e-4之后每个epoch乘以0.98逐步衰减。滑坡边界这种细粒度特征在训练后期才逐渐收敛学习率降太快会导致边界学不到位。4.4 训练循环骨架与模型保存训练脚本里最关键的是验证逻辑每个epoch结束在验证集上算IoU只保存IoU最高一次的权重。这样即使后续过拟合模型文件也是历史最佳状态。def train_one_epoch(model, loader, optimizer, device): model.train() total_loss 0.0 for images, masks in loader: images images.to(device) masks masks.to(device) preds model(images) loss bce_dice_loss(preds, masks) optimizer.zero_grad() loss.backward() optimizer.step() total_loss loss.item() * images.size(0) return total_loss / len(loader.dataset) torch.no_grad() def validate(model, loader, device): model.eval() total_iou 0.0 for images, masks in loader: images images.to(device) masks masks.to(device) preds torch.sigmoid(model(images)) 0.5 intersection (preds masks.bool()).sum(dim(1, 2, 3)) union (preds | masks.bool()).sum(dim(1, 2, 3)) iou (intersection.float() / (union.float() 1e-6)).mean() total_iou iou.item() * images.size(0) return total_iou / len(loader.dataset)torch.sigmoid(model(images)) 0.5是推理阶段的二值化操作验证的时候也要评估原始阈值下的性能确保训练和评估是一致的。模型保存时推荐同时保存model.state_dict()和对应的epoch、val_iou方便回溯对比。5. 模型推理与成果导出重叠滑窗加权融合5.1 大影像推理的边缘伪影问题训练时输入是512×512裁剪块推理阶段面对整景影像不能直接resize。直接resize会破坏滑坡体的尺度信息尤其是小滑坡缩小后可能只有十几个像素。标准做法是推理时也做滑窗但窗口之间通常有重叠重叠区域的预测结果如果直接取平均值边缘处会出现明显的接缝伪影。5.2 Hann窗加权融合消除拼接痕迹重叠区域需要做加权平均权重窗口的中心高、边缘低这样拼接起来过渡平滑。Hann窗是常用选择import numpy as np import torch def hann_2d(size): hann_1d np.hanning(size) hann_2d np.outer(hann_1d, hann_1d) return torch.from_numpy(hann_2d).float() def sliding_predict(model, image, window_size512, overlap128, devicecuda): model.eval() c, h, w image.shape stride window_size - overlap prob_map torch.zeros((1, h, w), devicedevice) weight_map torch.zeros((1, h, w), devicedevice) win hann_2d(window_size).unsqueeze(0).unsqueeze(0).to(device) for y in range(0, h, stride): for x in range(0, w, stride): sub image[:, y:ywindow_size, x:xwindow_size] if sub.shape[1] window_size or sub.shape[2] window_size: pad torch.zeros((c, window_size, window_size), devicedevice) pad[:, :sub.shape[1], :sub.shape[2]] sub sub pad sub sub.unsqueeze(0) with torch.no_grad(): prob torch.sigmoid(model(sub)) prob_map[:, y:ywindow_size, x:xwindow_size] prob * win weight_map[:, y:ywindow_size, x:xwindow_size] win final_prob prob_map / (weight_map 1e-8) return final_prob代码里win就是Hann窗权重乘到每个窗口的预测结果上再累加最后除以权重和。1e-8防止图像边缘没有窗口覆盖的位置除零。stridewindow_size-overlap控制重叠度重叠越大拼接越平滑但推理时间线性增长。一般overlap取128到256之间具体看滑坡体的尺度滑坡体大的影像建议overlap取256。5.3 阈值选择与后处理融合后的概率图不是直接就是最终结果默认0.5阈值只适合概率分布居中时的场景。滑坡区域纹理破碎模型输出往往在边界处概率偏低统一用0.5会在沟壑、阴影处产生漏检。可以观察验证集上不同阈值对应的IoU曲线最佳阈值一般落在0.35到0.5之间。选好阈值之后做一次形态学开运算去除孤立的误检小斑块import cv2 final_mask np.ones((h, w), dtypenp.uint8) final_mask[prob_map.cpu().squeeze() 0.45] 1 final_mask[prob_map.cpu().squeeze() 0.45] 0 kernel np.ones((3, 3), np.uint8) final_mask cv2.morphologyEx(final_mask.astype(np.uint8), cv2.MORPH_OPEN, kernel)后处理开运算是用3×3结构元素对二值掩膜做腐蚀再膨胀孤立的小噪点面积小于结构元素会被直接消除。注意开运算也会抹掉很小的真实滑坡体运行前先观察数据的滑坡体最小尺度如果小滑坡和孤立的误检大小接近就不要做开运算。5.4 可视化验证技巧跑完推理别急着导出GeoTIFF先随机抽几块区域把原图、标注、预测结果并排拼接在一起看细节。重点看三类位置滑坡边界是否紧贴真实边缘、阴影区域是否误报、裸土区域是否和滑坡体混淆。如果边界系统性膨胀说明损失函数里BCE权重占比太高如果裸土大面积误报说明数据里负样本多样性不够需要补充非滑坡区域的裸地影像。可视化确认没问题之后再通过GDAL把掩膜写入原始GeoTIFF的地理坐标导出可供制图和统计的成果文件这样滑坡面积、周长这些指标也就能自动计算出来了。本文还有配套的精品资源点击获取