资讯动态

基于PyTorch的遥感影像滑坡场景分类实战:从数据到模型

发布时间:2026/9/11 18:40:38 来源:尧图企业网站定制
简介面向毕业设计、课程设计与项目开发场景基于Python实现遥感影像滑坡场景分类的完整代码与项目文档。项目采用SVM分类器通过光谱特征与GLCM纹理特征提取、K-Means视觉词袋聚类、LDA主题抽象最终利用Libsvm工具完成场景分类流程清晰、方法经典。压缩包共21个文件、大小仅1.86MB以Python源码、文本配置/说明、pkl特征文件、模型文件及Markdown文档为主体其中4个py文件为核心代码4个pkl为中间特征model/lda模型文件用于加载推理md文档提供详细说明。项目源码已通过严格测试可直接参考并二次开发。资源上传后已有55人学习适合对遥感影像分类、传统机器学习流程感兴趣的初学者与工程开发人员。借助该资源可快速跑通SVMK-MeansLDA的完整分类流程理解视觉词袋在遥感场景识别中的应用并基于现有代码扩展实验或完成课程作业。1. 滑坡场景分类到底在解决什么问题为什么选Python生态滑坡是山区最常见的次生灾害之一遥感影像上的滑坡识别长期以来依赖人工目视解译效率低且标准不一。把“滑坡场景分类”做成代码任务本质上是让模型自动判断某块影像区域是否包含滑坡体或者更进一步对每个像素输出“滑坡/非滑坡”的概率。这个任务的价值在于一次训练好的模型可以快速应用于大范围卫星影像辅助灾害应急和国土调查。对毕业设计或课程设计来说它同时覆盖了图像处理、深度学习、数据标注和工程交付四个环节是很典型的“有学术背景、有落地价值”的选题。为什么用Python因为从数据读取、模型搭建到可视化评估Python生态几乎一条龙。GDAL处理栅格rasterio读取TIFFPyTorch训练网络matplotlib画混淆矩阵所有环节都有成熟库。相比C或MATLABPython让你把精力放在模型设计和实验分析上而不是重复造轮子。接下来的内容按一个完整项目应有的顺序展开数据准备、模型实现、训练调参、最后交付成别人能跑起来的代码库。这套流程不是唯一方案但我会尽量把常见做法和关键坑都讲清楚。2. 遥感影像滑坡场景分类的数据准备从Sentinel-2影像到滑坡标注2.1 获取和预处理遥感影像的常见做法滑坡场景分类的第一步是拿到带标注的影像数据。公开数据集方面有毕设常用的 Landslide4Sense、Bijie 滑坡数据集等如果没有现成数据常见做法是用 Sentinel-2 影像自己标注。Sentinel-2 提供 10 米分辨率的多光谱波段B2、B3、B4、B8对滑坡这种百平方米到平方公里级的地表变化来说足够用。拿到 L2A 级产品后先做预处理重投影到统一坐标系常用 UTM按研究区范围裁剪然后合成 RGB 或 RGBNIR 的多波段影像。不要直接拿原始 JPEG 或 PNG 用因为不同时相的影像辐射值不一致会导致模型泛化能力变差。我一般会先把影像整理成 GeoTIFF保持地理参考方便后续和标注矢量叠加。典型预处理流程如下# 使用 gdal 命令行工具重投影并裁剪 gdalwarp -t_srs EPSG:32648 -cutline study_area.shp -crop_to_cutline \ -of GTiff input_B02.tif output_B02_crop.tif如果要合成多波段建议用 Python 脚本统一处理因为滑坡数据往往来自不同轨道和时相手动操作容易漏波段。下面的代码把单波段文件读入并堆叠成多波段 GeoTIFFimport rasterio import numpy as np bands [B02.tif, B03.tif, B04.tif, B08.tif] # BGRN stacked [] for b in bands: with rasterio.open(b) as src: stacked.append(src.read(1).astype(np.float32)) profile src.profile # 复用最后一个文件的元数据 arr np.stack(stacked, axis0) profile.update(countlen(bands), dtypefloat32) with rasterio.open(landslide_input.tif, w, **profile) as dst: dst.write(arr)这段代码的逻辑很简单依次打开每个波段文件读出通道数据最后按波段顺序堆叠成(C, H, W)的数组。profile是从最后打开的源文件继承的保证了输出 TIFF 的地理坐标信息不丢失。注意astype(np.float32)这一步——滑坡识别经常需要计算 NDVI、NDWI 等指数浮点运算避免溢出。2.2 制作标注数据从矢量图斑到逐像素标签滑坡标注通常是矢量图斑在 ArcMap 或 ArcGIS Pro 里手工勾画滑坡边界保存为面要素。这个步骤看起来简单实际要花掉整个项目一半的时间。标注标准不统一不同人的勾画比例尺不同会导致模型学到边界噪声。把矢量转成与影像对齐的栅格标签时有几个关键点。一是确认矢量与影像的坐标系完全一致二是设置与影像相同的分辨率三是类别编码必须从 0 开始0 表示背景1 表示滑坡。Transform 步骤如下import geopandas as gpd from rasterio.features import rasterize gdf gpd.read_file(landslide_labels.shp) gdf gdf.to_crs(utm_crs) # 这里 utm_crs 需替换成影像的实际坐标系 with rasterio.open(landslide_input.tif) as src: meta src.meta.copy() shapes [(geom, 1) for geom in gdf.geometry] # 1 代表滑坡类别 label_raster rasterize( shapes, out_shape(meta[height], meta[width]), transformmeta[transform], fill0, dtypeuint8, ) with rasterio.open(landslide_label.tif, w, **meta) as dst: dst.write(label_raster, 1)rasterize是栅格化核心函数shapes里每个元素是(几何对象, 像元值)。out_shape和transform直接从影像元数据里取保证标签和影像像素一一对应。这里有个隐藏坑滑坡边界的羽化效果需要保留还是二值化如果后续要做分割网络建议保留 0/1 硬标签因为多数损失函数按像素独立计算模糊标签反而干扰训练。2.3 窗口裁剪与数据增强的实用参数影像尺寸通常几千乘几千像素GPU 显存放不下所以要把大影像裁剪成固定大小的 patch。常见做法是切 256×256 或 512×512带重叠地切。重叠比例 50% 能显著增加样本量同时避免滑坡体被切碎。对于走廊式滑坡单纯正方滑动窗口可能会把多个滑坡目标切成两半可以配合按标注框裁剪的方式。裁剪后的增强操作必须谨慎。滑坡在影像上表现为土体裸露、色调与周边植被差异明显所以水平翻转和垂直翻转是安全的随机旋转 90° 也是无损操作。但亮度扰动和对比度扰动要小幅度因为不同时相的 Sentinel-2 影像本身有辐射差异过度增强会让模型学到错误的颜色关联。我常用以下参数from torchvision import transforms train_transform transforms.Compose([ transforms.RandomHorizontalFlip(p0.5), transforms.RandomVerticalFlip(p0.5), transforms.RandomRotation(90, expandFalse), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.1, hue0.02), ])ColorJitter的四项参数建议这样设brightness和contrast在 0.2 以内saturation0.1 以内hue不要超过 0.05。滑坡体本身是土黄色色调偏移过大会让模型分不清滑坡和裸地。如果你做的是多时相对比灾前灾后影像拼接还需要在通道层面对齐时相先不急着增强。数据准备好后要划分训练集、验证集、测试集。滑坡数据集常见问题是分布不均衡正样本像素往往不到 10%。建议按地理区域划分而不是随机划分像素块否则同一个滑坡的 patch 会同时出现在训练集和测试集里评估结果虚高。划分比例 7:2:1 是常见做法保存成三个 txt 文件记录 patch 路径比文件目录划分更容易回溯。3. 用PyTorch搭建滑坡场景分类模型从CNN到分割网络的选型3.1 为什么选语义分割而不是普通分类网络“场景分类”四个字有两种理解给整幅影像打一个“滑坡/非滑坡”的标签或者对每个像素做分类。在遥感灾害领域后者才是真正的需求因为滑坡边界对救灾和损失评估至关重要。整图分类可以用 ResNet但输出只是类别概率拿不到滑坡位置语义分割网络输出的特征图和原图同分辨率可直接提取滑坡体范围。所以这里采用分割网络的架构。最经典的是 U-Net编码器部分用预训练的 ResNet34 或 ResNet50解码器部分逐步上采样恢复空间分辨率。滑坡边界往往不规则U-Net 的跳跃连接能把浅层边缘信息传到深层比单纯 DeepLabV3 更容易学习到细微边界。如果你的设备显存只有 6GB建议用 ResNet34 编码器输入 256×256batch size 8。模型定义如下基于 PyTorch 的torchvision.models.segmentation.deeplabv3_resnet50做微调更简单但要定制解码器结构还是直接用现成库segmentation_models_pytorch更省事import segmentation_models_pytorch as smp model smp.Unet( encoder_nameresnet34, encoder_weightsimagenet, in_channels4, # B2, B3, B4, B8 classes1, # 二分类输出一个通道 activationNone, # 不在这里接 sigmoid在损失函数里处理 )这里in_channels4对应前面堆叠的四波段影像如果你决定只用 RGB改成 3。encoder_weightsimagenet是迁移学习的关键虽然遥感影像和 ImageNet 的自然图像分布不同但浅层特征边缘、纹理是可迁移的能显著加速收敛。activationNone是为了避免在模型内部固化 sigmoid方便混合损失函数计算。3.2 自定义数据集类的完整写法PyTorch 训练要求把影像和标签封装成Dataset类。要点有两个一是文件按配对方式读取二是返回的影像张量需要归一化。遥感影像多波段的均值方差和自然图像不同不能直接用 ImageNet 的mean[0.485, 0.456, 0.406]。有人这么用也能收敛但分割精度会差一点。正确做法是统计自己的训练集# 统计每个通道的均值和标准差保存后用于训练 # 注意统计要在数据增强之前且分通道计算 means np.zeros(4) stds np.zeros(4) for idx in range(len(image_list)): with rasterio.open(image_list[idx]) as src: arr src.read().astype(np.float32) for c in range(4): means[c] arr[c].mean() stds[c] arr[c].std() means / len(image_list) stds / len(image_list) print(mean:, means, std:, stds)这个统计脚本会输出四维数组。实际训练时把它硬编码到数据集类里避免每次启动都重新统计。下面是标准 Dataset 写法import torch from torch.utils.data import Dataset import rasterio import numpy as np class LandslideDataset(Dataset): def __init__(self, img_paths, label_paths, transformNone): self.img_paths img_paths self.label_paths label_paths self.transform transform def __len__(self): return len(self.img_paths) def __getitem__(self, idx): with rasterio.open(self.img_paths[idx]) as src: image src.read().astype(np.float32) # (4, H, W) with rasterio.open(self.label_paths[idx]) as src: label src.read(1).astype(np.int64) # (H, W) # 归一化这里的 mean/std 来自上面的统计结果 mean np.array([0.02, 0.03, 0.04, 0.05]).reshape(-1, 1, 1) std np.array([0.05, 0.06, 0.07, 0.08]).reshape(-1, 1, 1) image (image - mean) / std # 转成 torch tensor注意 label 要加一个通道维 image torch.from_numpy(image) label torch.from_numpy(label).unsqueeze(0).float() # (1, H, W) if self.transform: # 这里简化处理真正使用时需要同时变换 image 和 label seed torch.initial_seed() torch.manual_seed(seed) image self.transform(image) torch.manual_seed(seed) label self.transform(label) return image, label__getitem__中需要保证 data augmentation 对影像和标签做完全一致的随机操作。上面的方法是利用torch.initial_seed()让两次变换使用相同的随机种子能保证翻转和旋转对二者的作用一致。但ColorJitter不能直接作用在 label 上实际项目里更推荐把 transform 写成分离的复合函数或使用 kornia 这类库的同步变换。3.3 在单卡和CPU上跑通训练循环的最低配置如果你只有 CPU 环境先别急着跑全尺寸训练。把影像缩小到 128×128epoch 设 3batch size 4先确认数据流和 loss 都在降低。这个过程能暴露出 90% 的代码错误文件路径缺失、张量只读、显存不足等等。PyTorch 训练循环的标准写法这里给出一个精简版device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) optimizer torch.optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-4) for epoch in range(epochs): model.train() epoch_loss 0.0 for images, labels in train_loader: images images.to(device) labels labels.to(device) logits model(images) # (B, 1, H, W) loss mixed_loss(logits, labels) optimizer.zero_grad() loss.backward() optimizer.step() epoch_loss loss.item() * images.size(0) print(fEpoch {epoch} loss: {epoch_loss / len(train_loader.dataset):.4f})AdamW是 Adam 的修正版weight decay 用 1e-4 能有效控制过拟合。model(images)的输出 shape 是(B, 1, H, W)因为classes1后面计算 Dice loss 时需要做torch.sigmoid(logits)。训练时最好只打印 loss不要每步都打印 IoU否则会拖慢训练好几倍。4. 训练、验证与调参滑坡分类任务的损失函数、评估指标与迁移学习4.1 为什么 BCE Dice 混合损失对滑坡有效滑坡分类正负像素比例悬殊一幅 512×512 影像中滑坡可能只占几百个像素。如果只用 BCE Loss模型会快速收敛到“全预测为背景”因为背景占比 95% 以上全预测背景的 loss 也很小。Dice Loss 直接优化 DICE 系数对正负样本不敏感但单独用 Dice Loss 时梯度在零点附近不稳定组合起来效果更可靠。混合损失的定义import torch import torch.nn.functional as F def dice_loss(pred, target, smooth1.0): pred torch.sigmoid(pred) pred pred.contiguous().view(-1) target target.contiguous().view(-1) intersection (pred * target).sum() dice (2. * intersection smooth) / (pred.sum() target.sum() smooth) return 1 - dice def mixed_loss(pred, target, bce_weight0.5): bce F.binary_cross_entropy_with_logits(pred, target) dice dice_loss(pred, target) return bce_weight * bce (1 - bce_weight) * dicebce_weight的默认值取 0.5但实际项目中建议先试 0.3 和 0.7。当滑坡目标很小、边界模糊时bce_weight0.7让模型更依赖逐像素交叉熵边界更干净当滑坡面积较大时Dice 权重可以提高到 0.6。smooth参数是平滑系数防止交集为 0 时除以 0一般取 1.0 或 1e-5对最终精度影响不大。4.2 验证集上的评估指标怎么算才可信评估滑坡分类模型不能只看准确率因为背景占比太高准确率即使 99% 也可能是废模型。需要关注 IoU、F1-score、Recall。滑坡救灾场景中漏检比误检更危险所以 Recall 权重通常高于 Precision。验证阶段需要每个 epoch 计算一次不能只在最后算。实现方法def calculate_metrics(outputs, labels): preds (torch.sigmoid(outputs) 0.5).int() # 阈值默认 0.5 tp ((preds 1) (labels 1)).sum().item() fp ((preds 1) (labels 0)).sum().item() fn ((preds 0) (labels 1)).sum().item() iou tp / (tp fp fn 1e-6) recall tp / (tp fn 1e-6) precision tp / (tp fp 1e-6) f1 2 * precision * recall / (precision recall 1e-6) return iou, f1, recall注意1e-6是防止除零而不是平滑指标本身。阈值 0.5 是默认选择但如果召回率太低可以调低阈值到 0.3这会增加预测面积提升召回率。具体阈值应该在验证集上选而不是在测试集上选否则会过拟合验证集。4.3 迁移学习的几个坑冻结编码器、学习率、训练多少轮预训练权重的合理使用能减少 30% 以上的训练时间。对于小数据集几千个 patch常见做法是冻结编码器只训练解码器。方式如下model smp.Unet(encoder_nameresnet34, encoder_weightsimagenet) for param in model.encoder.parameters(): param.requires_grad False冻结后初始学习率可以从 1e-3 提高到 1e-3 到 3e-3因为解码器是随机初始化的需要较大学习率快速拟合。训练 10 到 15 个 epoch 后解冻编码器把学习率降到原来的十分之一再继续训练 20 个 epoch。这样做的理由是先让解码器学会把编码器输出的特征图映射为分割结果解冻后再用较低学习率微调编码器避免破坏预训练特征。滑坡任务常见的过拟合表现是验证集 IoU 在第 10 个 epoch 达到峰值后开始下降而训练集 IoU 还在上升。此时可以早停也可以加更强的正则。注意遥感影像的太阳高度角、云影都会干扰如果模型在验证集特定区域表现差先检查标注是否把阴影误标成了滑坡。4.4 超参数选择速查表下面这张表整理了我做滑坡分类时最常用的参数范围和选择思路你也可以直接复制作为项目的实验记录表。参数常见范围我的默认值调整依据影像尺寸256×256 到 512×512512×512显存 8GB 以上用 512否则用 256batch size8 到 3216显存不足时先降 batch size而不是降分辨率学习率1e-5 到 1e-31e-4用 cos warmup 或 ReduceLROnPlateau编码器resnet34 / resnet50resnet34数据量大用 resnet50否则 resnet34 更稳损失权重bce 0.3~0.70.5正样本比例小于 5% 时提高 dice 权重训练轮数30 到 6040结合早停 patience10优化器AdamW / SGDAdamW小数据集上 AdamW 收敛更快学习率的调节参考ReduceLROnPlateauscheduler torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemax, factor0.5, patience5, verboseTrue ) # 每个 epoch 验证后调用 scheduler.step(val_f1)modemax表示监控指标最大化factor0.5表示学习率缩小一半patience5表示连续 5 个 epoch 没有提升就缩。这个调度器特别适合滑坡分类这种验证指标波动大的情况。5. 把模型和项目文档变成可交付的毕设/课设代码库5.1 项目目录结构和 requirements.txt 的一次到位写法课程设计和毕业设计交付时老师第一眼看的是能不能跑通第二眼看结构是否规范。我见过很多代码只有一个训练脚本没有数据说明别人拿到根本无从下手。推荐目录如下landslide_classification/ ├── data/ │ ├── images/ │ ├── labels/ │ └── train_val_test.txt ├── src/ │ ├── dataset.py │ ├── model.py │ ├── loss.py │ ├── train.py │ ├── predict.py │ └── utils.py ├── checkpoints/ ├── visualize/ ├── requirements.txt └── README.mdrequirements.txt要写清楚版本范围但不能固定死某个具体版本导致别人装不上。参考下面写法torch2.0.0 torchvision0.15.0 segmentation-models-pytorch0.3.0 rasterio1.3.0 geopandas0.13.0 numpy1.24.0 matplotlib3.7.0 tqdm4.64.0建议用conda create -n landslide python3.8 -y新建环境然后pip install -r requirements.txt。Python 3.8 到 3.10 兼容性最好不要用 3.12部分旧版 GDAL 和 rasterio 轮子可能装不上。5.2 模型推理脚本加载权重输出滑坡概率图交付的项目必须包含一个独立的预测脚本能读入任意尺寸的 GeoTIFF输出带地理坐标的滑坡概率图。关键点在于推理时不需要再裁剪 256×256 固定 patch用滑窗叠加预测再拼接能避免拼接缝。import torch import numpy as np import rasterio from tqdm import tqdm def predict_tile(model, image_path, output_path, window_size512, stride256, devicecuda): with rasterio.open(image_path) as src: image src.read().astype(np.float32) profile src.profile.copy() h, w image.shape[1], image.shape[2] prob_map np.zeros((h, w), dtypenp.float32) count_map np.zeros((h, w), dtypenp.float32) # 归一化参数要和训练时保持一致 mean np.array([0.02, 0.03, 0.04, 0.05]).reshape(-1, 1, 1) std np.array([0.05, 0.06, 0.07, 0.08]).reshape(-1, 1, 1) for y in tqdm(range(0, h - window_size, stride)): for x in range(0, w - window_size, stride): patch image[:, y:ywindow_size, x:xwindow_size] patch_tensor torch.from_numpy((patch - mean) / std).unsqueeze(0).to(device) with torch.no_grad(): out torch.sigmoid(model(patch_tensor)).cpu().numpy()[0, 0] prob_map[y:ywindow_size, x:xwindow_size] out count_map[y:ywindow_size, x:xwindow_size] 1 # 边缘处理最后一行和一列可能没被覆盖单独处理 prob_map[count_map 0] prob_map[count_map 0] / count_map[count_map 0] profile.update(count1, dtypefloat32) with rasterio.open(output_path, w, **profile) as dst: dst.write(prob_map, 1)滑窗的stride小于window_size时每个像素会被多次预测平均后能减少拼接边界突变。推理时torch.no_grad()是必须的否则 PyTorch 会构建计算图显存会被撑爆。最后的profile.update将输出写为单波段 float32 GeoTIFF可以在 ArcGIS Pro 里直接加载做密度分析。5.3 在项目文档里写出老师想看的三个部分项目文档不需要八股文但要把“实验、结果、反思”写到能让答辩老师快速看懂。第一部分是数据描述用了哪些影像、多少样本、标注来源、滑坡体面积分布。第二部分是实验表格至少对比 CNN 与 U-Net、有无预训练、不同损失函数在 IoU 上的差异。第三部分是结果失败分析这是区分高分和及格的关键。举例来说如果模型在阴影区域产生误检文档里要写清楚原因和尝试过的方案例如“尝试用 DEM 坡度因子过滤但当前数据未包含坡度信息后续计划融合”。这比单纯贴一张精度表更有说服力。可视化脚本可以用matplotlib把原始影像、预测概率图和标注叠加成三联图输出到visualize/目录答辩时直接展示。import matplotlib.pyplot as plt def visualize_sample(image_path, label_path, prob_path, save_path): with rasterio.open(image_path) as src: img_rgb src.read([3, 2, 1]).transpose(1, 2, 0) # 假设是B8,B3,B2 with rasterio.open(prob_path) as src: prob src.read(1) fig, axes plt.subplots(1, 2, figsize(12, 5)) axes[0].imshow(img_rgb / img_rgb.max()) axes[0].set_title(RGB Image) axes[1].imshow(prob, cmapReds, vmin0, vmax1) axes[1].set_title(Landslide Probability) plt.tight_layout() plt.savefig(save_path, dpi150, bbox_inchestight)src.read([3, 2, 1])是按通道索引读取如果你的影像波段顺序是 B2、B3、B4、B8那么索引 3 是 NIR索引 2 是 Red索引 1 是 Green组合出的“假彩色”能让植被显示为红色滑坡体显示为灰黄色对比更明显。保存图片时一定要关掉坐标轴刻度否则地理坐标和像元坐标混在一起会很乱。最终提交前从头到尾跑一遍train.py和predict.py用conda创建全新环境验证依赖完整。课程设计评审常常因为缺少某个pip install导致现场跑崩这一分钟检查能救回不少印象分。把预测结果叠加到影像上保存为 PNG也方便写进报告里作为你项目成果的直接证据。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价