ARTICLE DETAIL

资讯详情

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

基于PyTorch的CNN遥感图像滑坡识别:从数据到推理全流程

基于PyTorch的CNN遥感图像滑坡识别:从数据到推理全流程 简介一份基于深度学习CNN网络与PyTorch框架的遥感图像滑坡识别完整工程面向遥感、地灾监测及计算机视觉方向的研究者与工程师解决如何用卷积网络自动提取滑坡特征、完成端到端检测的问题。压缩包共122个文件以Python脚本为核心含train.py、trainer.py、rpn.py、resnet50.py等训练、模型构建与推理代码配套96个xml标注文件和数据集另有项目说明文档与ttf字体文件整体体积4.93MB结构紧凑。已有60人学习适合希望快速上手PyTorch滑坡识别任务、避免从零训练模型的用户。项目附带训练好的模型可直接加载使用源码涵盖了数据加载、模型设计、训练评估完整流程说明文档详细解析了设计原理与关键步骤便于二次开发或迁移至其他遥感影像地物识别场景兼顾研究验证与工程落地。1. 滑坡识别为什么不能靠肉眼和传统阈值分割滑坡识别落到遥感图像上最大的难点不是找而是区分。卫星影像里滑坡体、裸土、采石场、山体阴影常常呈现相近的灰度和纹理传统阈值分割和边缘检测在这种场景下几乎必然翻车。基于深度学习CNN网络配合PyTorch框架做遥感图像滑坡识别正好绕过这些人工特征设计的瓶颈——CNN自己学纹理、边缘、上下文关系PyTorch则把数据集加载、训练监控、断点续训这些工程活整理得相当顺手。这个方向适合正在做地质灾害调查、水电公路选线、灾后影像评估的从业者也适合刚从图像分类转入语义分割的PyTorch使用者。接下来的路径就三步准备带标注的遥感数据把大影像切成模型合适尺寸的样本训练一个能逐像素输出滑坡区域的网络最后用训练好的模型对整景影像做推理成图。2. 数据准备把遥感影像和标注转成PyTorch能吃的数据集2.1 数据从哪来、标注怎么处理滑坡识别属于语义分割任务模型学习的不是图片里有没有滑坡而是每一个像素属于滑坡还是背景。所以数据准备的起点不是影像本身而是标注文件。常见做法是拿历史滑坡编目或者人工解译的矢量面shapefile或GeoJSON作为标签再和遥感影像做几何配准。这里有个关键前提影像和标注必须严格对齐。滑坡区域如果在配准上差了三五个像素模型训练出来边界就是糊的推理结果也落不到实际地理位置上。把矢量标签转成与影像逐像素对应的栅格掩膜常见做法是用GDAL的gdal.RasterizeLayer把多边形烧录到和影像同尺寸的栅格里。滑坡类标为1背景标为0。这一步直接决定后续训练样本的质量建议单独写一个脚本做不要手动在图像软件里涂。from osgeo import gdal, ogr img_path remote_sensing.tif vec_path landslide_label.shp out_path label_mask.tif img_ds gdal.Open(img_path) vec_ds ogr.Open(vec_path) layer vec_ds.GetLayer() target_ds gdal.GetDriverByName(GTiff).Create( out_path, img_ds.RasterXSize, img_ds.RasterYSize, 1, gdal.GDT_Byte ) target_ds.SetGeoTransform(img_ds.GetGeoTransform()) target_ds.SetProjection(img_ds.GetProjection()) gdal.RasterizeLayer(target_ds, [1], layer, burn_values[1]) target_ds None这里burn_values[1]把矢量覆盖范围内的像素值写成1背景保持0。GDT_Byte用8位整型存储标签显存和磁盘开销都小。配准这一步如果发现影像和矢量存在系统性偏移优先在GIS软件里做一次平移校正再烧录不要在烧录之后指望模型自己学回来。2.2 大影像切片固定尺寸、重叠与无效区过滤遥感影像经常是几千乘几千甚至上万像素直接整图送进GPU是不可能的。最常见做法是切成256×256或512×512的patch。patch尺寸需要在感受野和显存占用之间折中256×256在训练时更灵活512×512能保留更多上下文信息滑坡这种较大尺度的目标用512更稳。我一般习惯用512×512配合batch_size8在12GB显存级别的显卡上跑起来比较舒服。切片时建议带一部分重叠尤其当目标跨越patch边界时。训练阶段重叠不是必需的但推理阶段必须重叠采样否则拼接痕迹会非常重这一点在第4章细说。import numpy as np import tifffile as tiff image tiff.imread(remote_sensing.tif) # shape: (H, W, C) label tiff.imread(label_mask.tif) # shape: (H, W) patch_size 512 stride 256 # 训练阶段可大于patch_size一半推理阶段建议等于patch_size一半 patches_img [] patches_lbl [] for y in range(0, image.shape[0] - patch_size 1, stride): for x in range(0, image.shape[1] - patch_size 1, stride): img_patch image[y:ypatch_size, x:xpatch_size] lbl_patch label[y:ypatch_size, x:xpatch_size] # 过滤掉完全没有滑坡像素的纯背景块 if (lbl_patch 1).sum() 50: continue patches_img.append(img_patch) patches_lbl.append(lbl_patch)stride决定patch之间的密度。训练时重叠太多会导致相邻样本高度相似模型容易见过拟合的假象重叠太少又会漏掉跨边界的滑坡目标。(lbl_patch 1).sum() 50这个阈值把几乎不含滑坡的patch扔掉了——如果全留下背景类占比会冲到99%模型很快就学会全部输出背景。2.3 类别不平衡与数据增强的实用配置滑坡识别里正样本像素占比极低是常态。某个区域滑坡可能只占整个影像的0.3%即便切片打下来含滑坡的patch里滑坡像素占比也常常不超过15%。单纯用交叉熵损失函数模型会忽略滑坡类。常见做法是在损失函数里给滑坡类加权这个权重需要按实际比例算不推荐拍脑袋写死。import torch import torch.nn as nn # 统计整个训练集里滑坡像素和背景像素的数量 num_bg (label_arrays 0).sum() num_slide (label_arrays 1).sum() weight_bg 1.0 weight_slide num_bg / (num_slide 1e-6) # 滑坡权重可以到几十甚至上百 criterion nn.CrossEntropyLoss( weighttorch.tensor([weight_bg, weight_slide], devicedevice) )weight_slide按背景与前景的比例设定是一个合理的起点。如果滑坡占0.3%权重给到300倍会让训练初期震荡明显我一般先给计算值的0.5倍再观察损失曲线再调整。数据增强方面遥感图像的增强和三通道自然照片略有不同。翻转和旋转是安全的因为滑坡地形没有方向性色彩抖动要谨慎同一景影像内辐射差异不大但不同批次影像之间可能存在传感器差异。用Lighting和饱和度扰动可以增加鲁棒性但不要用太大的值把地物本来的光谱特征破坏掉。from torchgeo.transforms import AugmentationSequential train_transform AugmentationSequential( torchgeo.transforms.RandomHorizontalFlip(p0.5), torchgeo.transforms.RandomVerticalFlip(p0.5), torchgeo.transforms.RandomRotation90(angles[0, 90, 180, 270]), torchgeo.transforms.RandomSolarization(p0.2), )RandomRotation90按90度倍数旋转不会引入插值噪声对遥感影像和标签同时变换是安全的。RandomSolarization模拟高光过曝区域让模型在滑坡体高亮部分不至于失手。2.4 自定义Dataset把影像和标签绑在一起数据准备好之后写一个PyTorch的Dataset子类让__getitem__每次返回影像patch和对应标签patch。滑坡识别里有个容易被忽视的小坑如果影像带NaN值或无效值需要先做掩膜处理不能把NaN直接喂给网络。import torch from torch.utils.data import Dataset import numpy as np class LandslideDataset(Dataset): def __init__(self, img_patches, lbl_patches, transformNone): self.imgs img_patches.astype(np.float32) self.lbls lbl_patches.astype(np.int64) self.transform transform def __len__(self): return len(self.imgs) def __getitem__(self, idx): img torch.from_numpy(self.imgs[idx]).permute(2, 0, 1) # HWC - CHW lbl torch.from_numpy(self.lbls[idx]) return img, lblpermute(2, 0, 1)是把HWC格式转成PyTorch默认的CHW格式这一步漏了会直接报维度错误。输入做了float32归一化标签保留int64索引后续给CrossEntropyLoss用。训练好的模型在PyTorch的生态里保存和加载都很直接这个Dataset结构在后面推理阶段也能复用。3. 模型与训练用PyTorch搭CNN并跑通滑坡分割3.1 网络结构选用编码器-解码器比普通分类CNN更适合滑坡识别是逐像素分类任务直接把VGG或ResNet套上来只能输出整图类别无法画出滑坡边界。业界更常见的方案是采用编码器-解码器结构的CNN比如U-Net或DeepLabV3。U-Net的跳连接能把浅层边缘细节和高层语义信息结合对滑坡这种边界不规则的目标非常合适。如果项目里标注数量不多U-Net相对小的参数量也更容易训起来。这个项目标题里明确写了CNN和PyTorch不一定要用大而全的语义分割库。自己搭一个精简的U-Net结构完全够用而且更容易针对单波段或多波段遥感影像改输入通道数。下面是一个可跑的编码器-解码器骨架import torch import torch.nn as nn import torch.nn.functional as F class ConvBlock(nn.Module): def __init__(self, in_ch, out_ch): super().__init__() self.conv1 nn.Conv2d(in_ch, out_ch, 3, padding1) self.bn1 nn.BatchNorm2d(out_ch) self.conv2 nn.Conv2d(out_ch, out_ch, 3, padding1) self.bn2 nn.BatchNorm2d(out_ch) def forward(self, x): x F.relu(self.bn1(self.conv1(x))) x F.relu(self.bn2(self.conv2(x))) return x class LandslideUNet(nn.Module): def __init__(self, in_channels3, num_classes2): super().__init__() self.enc1 ConvBlock(in_channels, 32) self.enc2 ConvBlock(32, 64) self.enc3 ConvBlock(64, 128) self.pool nn.MaxPool2d(2) self.mid ConvBlock(128, 256) self.dec3 ConvBlock(256 128, 128) self.dec2 ConvBlock(128 64, 64) self.dec1 ConvBlock(64 32, 32) self.out nn.Conv2d(32, num_classes, 1) def forward(self, x): e1 self.enc1(x) e2 self.enc2(self.pool(e1)) e3 self.enc3(self.pool(e2)) m self.mid(self.pool(e3)) d3 F.interpolate(m, sizee3.shape[2:], modebilinear, align_cornersFalse) d3 self.dec3(torch.cat([d3, e3], dim1)) d2 F.interpolate(d3, sizee2.shape[2:], modebilinear, align_cornersFalse) d2 self.dec2(torch.cat([d2, e2], dim1)) d1 F.interpolate(d2, sizee1.shape[2:], modebilinear, align_cornersFalse) d1 self.dec1(torch.cat([d1, e1], dim1)) return self.out(d1)in_channels3对RGB影像直接用如果是多光谱遥感影像比如额外加了近红外波段改成4或更多即可。滑坡体在近红外波段上常呈现与红波段不同的反射特性有近红外数据时建议加上。torch.cat做跳连接时要求两侧特征图的空间尺寸一致所以解码器里用F.interpolate显式对齐而不是靠反卷积猜测尺寸。3.2 训练流程与关键参数训练CNN模型时滑坡识别有几个参数值得认真调学习率、batch size、训练轮数。PyTorch生态里常见管线上先用Adam优化器初始学习率放在1e-4量级配合ReduceLROnPlateau在验证集损失停滞时自动降学习率。batch size取决于显存512×512输入下batch8是一个能平衡显存和BatchNorm稳定性的入门值。import torch.optim as optim from torch.utils.data import DataLoader device torch.device(cuda if torch.cuda.is_available() else cpu) model LandslideUNet(in_channels3, num_classes2).to(device) optimizer optim.Adam(model.parameters(), lr1e-4, weight_decay1e-5) scheduler optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemax, factor0.5, patience5 ) train_loader DataLoader(train_dataset, batch_size8, shuffleTrue, num_workers4) val_loader DataLoader(val_dataset, batch_size8, shuffleFalse, num_workers4) best_iou 0.0 num_epochs 60 for epoch in range(num_epochs): model.train() total_loss 0.0 for imgs, lbls in train_loader: imgs, lbls imgs.to(device), lbls.to(device) optimizer.zero_grad() logits model(imgs) loss criterion(logits, lbls) loss.backward() optimizer.step() total_loss loss.item() * imgs.size(0) avg_loss total_loss / len(train_loader.dataset) val_iou evaluate_iou(model, val_loader, device) # 自行实现见第6章 scheduler.step(val_iou) if val_iou best_iou: best_iou val_iou torch.save(model.state_dict(), best_model.pth)modemax配合val_iou使用当交并比连续5个epoch不涨就把学习率减半这样后期不容易在最优解附近震荡。torch.save只保存state_dict权重文件小换机器加载也方便不必整模型序列化。训练好的模型保存下来之后推理阶段再torch.load回来。3.3 训练过程怎么看损失下降不代表模型真学会了滑坡识别里一个典型的翻车现场是训练损失降得挺好看但验证集IoU几乎为零。原因是滑坡像素太少模型把所有像素都预测为背景交叉熵损失依然很低。只看损失曲线的确会被骗。推荐在训练循环里每个epoch用验证集做一次IoU评估并打印出来。训练收敛速度方面遥感影像通常比自然图像更难学因为纹理复杂且光照不一致。60轮是一个相对保守的上限配合学习率衰减一般30到40轮左右能见到验证IoU的峰值。如果出现验证集IoU不涨反跌而训练集IoU还在上升说明模型已经过拟合这时该做的是增强数据或加Dropout而不是继续加轮数。环境配置上PyTorch的GPU版本安装需要先确认CUDA和显卡驱动常见做法是conda create -n landslide python3.10然后pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121。安装pytorch教程在官网就能找到按GPU版安装而不是CPU版训练速度差距非常大。4. 推理落地加载训练好的模型并输出滑坡区域4.1 加载权重与滑动窗口推理模型训练好之后权重文件是一个best_model.pth几十兆到一两百兆不等。推理阶段先把模型恢复到eval模式这个细节容易漏漏了会影响BatchNorm层的统计值导致同样的输入在训练状态和eval状态下输出差异很大。对整景影像推理滑动窗口必须设置重叠。简单地把影像切块让模型逐个预测再拼起来会在patch边界留下明显的条带痕迹。原因很简单patch中心区域的预测可靠边缘区域的感受野比较有限模型见得少预测置信度低。正确做法是让窗口重叠只取每个窗口中心区域的结果。import torch import numpy as np import tifffile as tiff model.load_state_dict(torch.load(best_model.pth, map_locationdevice)) model.to(device) model.eval() full_img tiff.imread(remote_sensing.tif).astype(np.float32) H, W, C full_img.shape patch_size 512 stride 256 # 推理时建议patch_size的一半 prob_map np.zeros((H, W), dtypenp.float32) weight_map np.zeros((H, W), dtypenp.float32) # 中心区域权重高边缘权重低拼接时按权重累加 y_weights np.hanning(patch_size).reshape(-1, 1) x_weights np.hanning(patch_size).reshape(1, -1) window_weights y_weights * x_weights with torch.no_grad(): for y in range(0, H - patch_size 1, stride): for x in range(0, W - patch_size 1, stride): patch full_img[y:y patch_size, x:x patch_size] patch_tensor torch.from_numpy(patch).permute(2, 0, 1).unsqueeze(0).to(device) logits model(patch_tensor) # (1, num_classes, H, W) probs torch.softmax(logits, dim1) # 取滑坡类概率 slide_prob probs[0, 1].cpu().numpy() prob_map[y:y patch_size, x:x patch_size] slide_prob * window_weights weight_map[y:y patch_size, x:x patch_size] window_weights prob_map / weight_mapnp.hanning生成的窗函数形状和patch尺寸一致中心权重为1边缘趋近于0。多个patch的预测在重叠区按加权平均合并拼接痕迹基本可以压到视觉上看不出来的程度。stride256配合patch_size512意味着每个像素至少被两个patch覆盖重叠越多拼接越平滑代价是推理时间增加。4.2 概率图转二值标签阈值选择与后处理prob_map是每个像素属于滑坡的概率值在0到1之间。要不要把它直接转成0/1标签取决于业务用途。灾害应急需要一张明确的滑坡边界通常取0.5作为阈值。如果更在意召回率漏掉真实滑坡的代价更高可以把阈值降到0.3如果更在意精确率减少误报面积阈值可以上调到0.6以上。from scipy import ndimage threshold 0.5 binary (prob_map threshold).astype(np.uint8) # 去掉面积过小的孤立区域通常是噪声 labeled, num_features ndimage.label(binary) sizes ndimage.sum(binary, labeled, range(num_features 1)) min_area 100 # 像素按影像分辨率调整 mask np.zeros_like(labeled, dtypebool) for i, size in enumerate(sizes): if size min_area: mask | (labeled i) cleaned mask.astype(np.uint8) tiff.imwrite(landslide_result.tif, cleaned)ndimage.label把连通的滑坡区域编号min_area按影像地面分辨率调整如果一景影像的像元是2米100像素大约对应400平方米的滑坡体这个尺度贴近实际地质灾害调查的最小统计单位。清理孤立像素后结果就可以直接导进GIS软件和已有滑坡编目叠加检查了。4.3 推理性能优化思路如果整景影像非常大逐patch推理会相当耗时。PyTorch里可以用torch.utils.data.TensorDataset配合DataLoader把patch批量送进去或者用半精度推理model.half()配合torch.cuda.amp.autocast()。遥感影像推理通常不需要极低延迟批量处理能接近GPU的饱和吞吐。另一个思路是调整窗口尺寸512×512在常见显存下已经足够不必为了提速强行用更小的patch——由于上下文信息变少小patch的预测质量会更差。5. 避坑指南滑坡识别项目里常见的5个踩坑点5.1 标签和影像错位模型训练期间看起来能收敛推理结果却对不上实景现象训练损失正常下降验证集IoU也不错但在整景影像上推理出来的滑坡位置和实际地形总差了一截。原因数据准备时矢量标注和遥感影像的坐标系或投影参数不一致导致烧录出来的掩膜系统性偏移几个像素。CNN确实有抗轻微错位的能力但超过三五个像素就会让边界学歪。解决烧录掩膜前先比较影像的地理范围和矢量范围用QGIS或Python里的rasterio叠加检查。具体操作是把烧录后的掩膜叠加在影像上随机抽几个滑坡边界看是否吻合。如果发现整体偏移用gdal.Warp或仿射变换修正影像和矢量的配准关系不要寄希望于模型自己纠正。5.2 类别不平衡导致模型全部预测背景现象训练几轮后验证集IoU仍然是0模型输出的二值图全黑。原因滑坡像素占整个数据集的比重太低模型发现只要把所有像素预测为背景损失就很低根本没必要学习滑坡特征。解决先统计数据集里正负像素比例再在损失函数里给滑坡类加权。权重设置我一般用背景像素数除以滑坡像素数作为起点实际训练时观察第一个epoch的损失值。如果损失值比加权前高出太多说明权重过大需要降低。也可以用数据增强中的过采样策略让每个batch里至少有一半patch是含滑坡的避免有些batch全是背景样本。5.3 训练集和验证集来自同一景影像导致指标虚高现象训练时IoU冲到0.9以上换到另一景影像上推理却惨不忍睹。原因滑坡识别项目里常见的数据划分方式是随机切片但同一景影像的切片之间有大量空间重叠模型相当于用近亲样本验证学到的其实是对这一景影像的记忆。解决按影像划分数据集一景影像的patch全部进训练集或验证集不能混在一起。更严格的做法是按地理位置划分比如用不同地区的影像分别做训练和验证。训练好的模型如果只在单一地区数据上训练要意识到空间泛化能力的边界换到地貌差异大的地区大概率需要重新训练或微调。5.4 推理时重叠窗口拼接线明显现象整景推理结果图上有规则的网格状痕迹尤其在滑坡边界处断断续续。原因没有采用加权拼接或者重叠窗口的中心区域置信度高、边缘置信度低拼接时边缘预测被直接叠加导致条带感。解决推理时采用第4章的window_weights方案用np.hanning窗函数对重叠区域加权平均。如果拼接线依然存在适当减小推理stride比如从patch_size减小到patch_size的一半重叠越多拼接越自然代价是推理时间翻倍。5.5 PyTorch环境不稳版本不匹配导致训练中断或GPU不可用现象torch.cuda.is_available()返回False或者训练到一半报CUDA out of memory。原因PyTorch、CUDA工具包、显卡驱动三者版本不匹配是最常见的环境问题。安装pytorch教程看起来简单但GPU版需要注意CUDA版本对应关系。解决先用nvidia-smi查看驱动支持的CUDA版本再选择对应版本的PyTorch安装命令。常见稳定组合是PyTorch 2.x配上CUDA 11.8或12.1。显存不足时先调小batch size再考虑换patch_size256最后才考虑换卡。多卡训练场景下显存不够也常见于num_workers设置过高导致内存爆炸这个可以调成4或者2。6. 验证与进阶把训练好的模型用交并比和滑窗策略做到可交付6.1 用IoU和召回率做模型的体检训练好的模型不能只看损失下降就收工。滑坡识别业务里最常用的评估指标是IoU和召回率前者衡量预测区域和真实标注的重合程度后者衡量漏检比例。IoU计算本身不复杂但遥感影像上滑坡区域通常占比小需要按滑坡类单独计算背景类IoU没有意义。def evaluate_iou(model, loader, device): model.eval() intersection 0 union 0 with torch.no_grad(): for imgs, lbls in loader: imgs, lbls imgs.to(device), lbls.to(device) logits model(imgs) preds logits.argmax(dim1) pred_slide (preds 1) true_slide (lbls 1) inter (pred_slide true_slide).sum().item() un (pred_slide | true_slide).sum().item() intersection inter union un iou intersection / (union 1e-6) return iouargmax直接取预测类别省去了阈值调节。IoU到0.5以上可以认为模型已经学到滑坡的基本形态0.7以上边界比较可信0.3以下基本不可用。如果IoU低但召回率尚可说明预测区域和真实区域有偏移优先检查标签配准问题而非模型结构。6.2 进阶方向多波段输入和时序影像RGB三波段是大多数项目的起点但滑坡识别在红、绿、蓝基础上增加近红外波段通常能显著提升对植被覆盖和裸露地表的区分能力。改成四通道输入时只需要在模型初始化时把in_channels改成4并确保数据加载阶段对应通道数匹配其余结构不用动。项目标题里带高分辨率遥感图像如果手上有多光谱数据建议尽量用它。时序影像方面滑坡发生前后的影像变化包含了更直接的信息。把这个方向从单帧识别升级为变化检测模型结构上可以用双输入的孪生网络也可以用U-Net的变体把前后两期影像按通道拼接输入训练思路和单帧分割几乎一致。6.3 部署与交接时的习惯训练好的模型如果要交给不具备PyTorch环境的同事使用建议导出为TorchScript格式避免在目标机器上重新装一整套深度学习环境。TorchScript导出的核心动作是torch.jit.trace前提是输入尺寸固定滑动窗口推理时正好满足这个条件。如果不要求独立运行保留best_model.pth和一份训练参数说明即可所有关键参数写在一个文本配置里别人接手时不用翻代码找默认值。我的习惯是每个项目训完都顺手把以下内容记到README或项目说明里数据来源与标注方法、patch尺寸与stride、损失函数权重、训练轮数与最佳IoU、推理脚本用法、测试机PyTorch版本。这个项目标题里已经包含项目说明正式的落地交付文档会比模型权重本身更重要。半年后回头看一个模型能快速复现训练过程和推理逻辑比调高两个点IoU更有价值。希望这些做法帮你在自己的滑坡识别数据上少走弯路。本文还有配套的精品资源点击获取
返回列表