基于PyTorch的遥感地物分类实战:从数据到部署
1. 项目概述:遥感地物分类实战全流程
去年参与某农业遥感项目时,我深刻体会到传统目视解译方法的效率瓶颈——10平方公里的果园分类需要3名专业人员耗时两周完成标注。而采用本文介绍的深度学习方法后,同样任务仅需2小时即可达到95%以上的分类准确率。这个基于PyTorch的实战教程,将完整演示从数据集准备到模型部署的全套解决方案。
遥感地物分类本质上是通过像素级识别实现地表覆盖类型自动划分的计算机视觉任务。与传统方法相比,深度学习模型(特别是U-Net、DeepLab等架构)能自动学习多光谱特征的空间关联性,在道路提取、农作物监测、城市变化检测等场景表现突出。本教程特色在于:
- 使用公开的LoveDA遥感数据集(包含7类地物标签)
- 实现大尺寸遥感影像的滑动窗口预测
- 提供数据增强的完整实现方案
- 包含模型量化部署实战技巧
关键工具链:PyTorch 1.12 + TorchVision 0.13 + CUDA 11.3,推荐使用RTX 3060及以上显卡(显存≥12GB)
2. 环境配置与数据准备
2.1 开发环境搭建
对于遥感影像处理,建议创建独立conda环境:
conda create -n rs_classify python=3.8 conda install pytorch torchvision torchaudio cudatoolkit=11.3 -c pytorch pip install opencv-python rasterio matplotlib sklearn验证GPU可用性:
import torch print(torch.cuda.is_available()) # 应输出True print(torch.rand(3,3).cuda()) # 测试张量计算2.2 数据集处理
使用LoveDA数据集(下载地址需替换为实际来源)的典型目录结构:
LoveDA/ ├── Train/ │ ├── Urban/ │ │ ├── images_png/ # 256x256 RGB图像 │ │ └── masks_png/ # 单通道标签图 ├── Val/ └── Test/数据加载关键步骤:
from torch.utils.data import Dataset import rasterio class LoveDADataset(Dataset): def __init__(self, img_dir, mask_dir, transform=None): self.img_paths = [os.path.join(img_dir, f) for f in os.listdir(img_dir)] self.mask_paths = [os.path.join(mask_dir, f) for f in os.listdir(mask_dir)] self.transform = transform def __getitem__(self, idx): img = cv2.imread(self.img_paths[idx]) # HWC格式 mask = cv2.imread(self.mask_paths[idx], 0) # 单通道读取 if self.transform: aug = self.transform(image=img, mask=mask) img, mask = aug['image'], aug['mask'] return img.transpose(2,0,1), mask # 转为CHW3. 模型构建与训练
3.1 U-Net模型改进
针对遥感影像特点,对经典U-Net进行以下改进:
- 编码器使用ResNet34预训练权重
- 在跳跃连接处添加空间注意力模块
- 输出层改用Dice Loss + CrossEntropy联合损失
import segmentation_models_pytorch as smp model = smp.Unet( encoder_name="resnet34", encoder_weights="imagenet", in_channels=3, classes=7, activation=None ) # 自定义解码器 class AttentionBlock(nn.Module): def __init__(self, in_channels): super().__init__() self.channel_att = nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Conv2d(in_channels, in_channels//8, 1), nn.ReLU(), nn.Conv2d(in_channels//8, in_channels, 1), nn.Sigmoid() ) def forward(self, x): att = self.channel_att(x) return x * att3.2 训练策略
采用分阶段训练方案:
optimizer = torch.optim.AdamW([ {'params': model.encoder.parameters(), 'lr': 1e-4}, {'params': model.decoder.parameters(), 'lr': 5e-4}, {'params': model.segmentation_head.parameters(), 'lr': 1e-3} ]) scheduler = torch.optim.lr_scheduler.OneCycleLR( optimizer, max_lr=[1e-4, 5e-4, 1e-3], total_steps=200 )实测技巧:当验证集mIoU连续3个epoch不提升时,冻结编码器权重只训练解码器
4. 大图预测与后处理
4.1 滑动窗口预测
处理大尺寸遥感影像(如4096x4096)的关键方法:
def predict_large_image(model, img_path, window_size=256, stride=128): img = cv2.imread(img_path) # 原始大图 h, w = img.shape[:2] pad_h = (h // stride + 1) * stride - h pad_w = (w // stride + 1) * stride - w img = cv2.copyMakeBorder(img, 0, pad_h, 0, pad_w, cv2.BORDER_REFLECT) output = np.zeros((h+pad_h, w+pad_w), dtype=np.float32) count = np.zeros_like(output) for y in range(0, h+pad_h-window_size+1, stride): for x in range(0, w+pad_w-window_size+1, stride): patch = img[y:y+window_size, x:x+window_size] patch_tensor = torch.from_numpy(patch).permute(2,0,1).float() with torch.no_grad(): pred = model(patch_tensor.unsqueeze(0).cuda()) output[y:y+window_size, x:x+window_size] += pred.argmax(1).squeeze().cpu().numpy() count[y:y+window_size, x:x+window_size] += 1 return (output / count)[:h, :w].astype(np.uint8)4.2 后处理优化
常见问题及解决方案:
- 边缘锯齿:使用高斯模糊平滑边界
- 小区域误判:采用面积开运算滤波
- 类别不平衡:在预测结果上应用类别权重
from skimage.morphology import opening def postprocess(mask, class_weights=[1, 1.2, 1, 0.8, 1.5, 1, 1]): processed = np.zeros_like(mask) for cls in range(7): cls_mask = (mask == cls).astype(np.uint8) cls_mask = opening(cls_mask, footprint=np.ones((3,3))) if cls_mask.sum() < 10: # 去除小区域 cls_mask.fill(0) processed[cls_mask > 0] = cls return processed5. 模型部署优化
5.1 TensorRT加速
将PyTorch模型转换为TensorRT引擎:
import tensorrt as trt logger = trt.Logger(trt.Logger.WARNING) builder = trt.Builder(logger) network = builder.create_network(1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH)) parser = trt.OnnxParser(network, logger) # 转换ONNX torch.onnx.export(model, dummy_input, "model.onnx") with open("model.onnx", "rb") as f: parser.parse(f.read()) config = builder.create_builder_config() config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, 1 << 30) serialized_engine = builder.build_serialized_network(network, config) with open("model.engine", "wb") as f: f.write(serialized_engine)5.2 量化部署
8位整数量化实现:
model.qconfig = torch.quantization.get_default_qat_qconfig('fbgemm') quant_model = torch.quantization.prepare_qat(model.train()) # 校准过程... quant_model = torch.quantization.convert(quant_model.eval()) torch.jit.save(torch.jit.script(quant_model), "quant_model.pt")6. 常见问题排查
6.1 显存不足解决方案
- 减小batch size(建议≥4)
- 使用梯度累积:
optimizer.zero_grad() for i, (x,y) in enumerate(train_loader): pred = model(x.cuda()) loss = criterion(pred, y.cuda()) / accumulation_steps loss.backward() if (i+1) % accumulation_steps == 0: optimizer.step() optimizer.zero_grad()6.2 类别不平衡处理
- 样本加权采样:
class_weights = 1. / torch.bincount(dataset.labels) sampler = WeightedRandomSampler(weights, num_samples=len(weights))- 损失函数加权:
criterion = nn.CrossEntropyLoss( weight=torch.tensor([1, 1.5, 1, 2, 3, 1, 1], device='cuda') )6.3 预测结果碎片化
解决方案:
- 增加CRF后处理:
from pydensecrf import densecrf d = densecrf.DenseCRF2D(w, h, 7) d.setUnaryEnergy(unary) d.addPairwiseGaussian(sxy=3, compat=3) Q = d.inference(5)- 在损失函数中加入边界约束项
实际项目中,建议先使用512x512尺寸训练基础模型,再微调到256x256尺寸提升细节。测试阶段将滑动窗口重叠率设为50%能有效减少边缘效应。记得在数据增强中保留NDVI等遥感指数通道,这对植被分类尤为关键。