ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

遥感图像建筑物提取实战:UNet与DeepLab V3+对比

遥感图像建筑物提取实战:UNet与DeepLab V3+对比 简介本资源是一套面向高校学生与遥感图像处理初学者的完整语义分割实践项目聚焦遥感影像地物分类任务提供Deeplab V3与U-Net两种主流模型的Python实现方案适用于毕业设计、课程设计及期末大作业等学术场景。压缩包共11个文件8.67MB含4个核心Python脚本模型构建、训练、数据预处理与可视化、1份详尽的Word手册含环境配置、运行步骤与结果分析、5张遥感样本图像及1张原始遥感图代码全程中文注释结构清晰、模块解耦便于理解模型流程与调试修改。目前已有334人学习下载项目经实际验证效果良好获导师高度认可评分98分下载后仅需配置基础深度学习环境即可一键训练与推理是入门遥感图像智能解译的高性价比实践范本。 上个月接了一个遥感影像城市建筑物提取的项目需求很直接输入一张高分辨率遥感图算法自动把建筑轮廓圈出来。这种任务在深度学习里就是标准的语义分割问题做像素级分类。我在技术选型时没纠结太久直接锁定了UNet和DeepLab V3两套方案。一方面这两个模型在Python生态里的资料足够多遇到问题好排查另一方面它们代表了语义分割领域两条不同的技术路线拿来做对比实验后面写项目报告也更有说服力。整条链路跑下来从数据切片、标注格式转换、模型训练到精度评估大概花了两周半。这篇文章就是这次实践的完整复盘内容包括两个模型的原理差异、Python源码实现、训练参数选择以及我实际调参过程中踩过的坑给正在做遥感图像分割的同学一个参照。1. 项目整体设计与思路拆解1.1 遥感图像分割和普通图像分割的差异遥感图像语义分割本质上是给影像中的每一个像素分配语义类别标签比如建筑物、道路、水体、植被、耕地等。这个任务跟自然图像分割比如人像分割、街景分割有非常大的区别主要体现在三个方面。第一图像尺寸巨大。一张高分辨率遥感影像动不动就是5000×5000甚至上万像素直接送进深度学习模型显存根本扛不住。所以必须做切片处理把大图切成小图再逐个推理。第二类别分布极度不平衡。像建筑物、道路这类目标在整幅图里占的面积比例往往只有百分之几。模型如果全部预测成背景准确率也能到95%以上但实际上一栋建筑都没提取出来。第三地物尺度差异大。同一张图里可能既有占地几千平米的大型厂房也有几十平米的民房如果模型感受野固定很容易顾此失彼。这三个特点直接决定了下游的模型选型和数据处理策略。1.2 为什么同时选UNet和DeepLab V3这两个模型背后是完全不同的技术路线。UNet的核心是编码器-解码器结构加上跳跃连接编码器逐层下采样提取语义特征解码器逐层上采样恢复空间分辨率跳跃连接把编码器每一层的特征直接拼到解码器对应层上弥补下采样丢失的空间细节。这种设计对边界细节特别友好而且在小样本数据集上表现稳定。DeepLab V3则走的是另一条路。它的核心是空洞卷积和ASPP模块Atrous Spatial Pyramid Pooling空洞空间金字塔池化通过多个不同膨胀率的空洞卷积并行采样相当于用一组不同分辨率的“眼镜”同时看图像一个模块就能捕捉到多尺度的上下文信息。相比UNet它在处理大尺度地物和复杂背景时上限更高。我选这两个模型还有一个现实原因一个轻量稳定一个精度上限高正好做对照组。实际操作中UNet作为baseline快速打通流程DeepLab V3用来追求更高的精度。1.3 项目环境与代码目录组织项目主要使用Python 3.8作为运行环境深度学习框架用TensorFlow 2.13Keras接口。选TF而不是PyTorch没有特别复杂的原因主要是手头已有的预训练权重和数据处理代码都是TF生态的迁移成本低。核心依赖如下numpy1.24 opencv-python pillow tensorflow2.13 albumentations rasterio scikit-learn代码目录结构我习惯用下面的方式组织逻辑清晰后面换数据集也方便remote_sensing_seg/ ├── data/ │ ├── raw/ # 原始遥感影像和标注 │ ├── train/ # 切片后的训练数据 │ └── val/ # 验证数据 ├── src/ │ ├── dataset.py # 数据读取与增强 │ ├── unet.py # UNet模型定义 │ ├── deeplabv3p.py # DeepLab V3模型定义 │ ├── losses.py # 损失函数 │ ├── train.py # 训练入口 │ └── predict.py # 推理与后处理 ├── weights/ # 保存模型权重 ├── logs/ # TensorBoard日志 └── config.yaml # 配置参数项目实际用到的遥感数据集是公开的航空影像标注数据集包含了城市区域的建筑物标注共约3000张切片。数据集的标注以灰度掩码图形式提供0表示背景255表示建筑物。这种格式处理起来比较直接。2. 遥感图像数据集准备与预处理2.1 从原始影像到训练数据的关键步骤遥感图像分割的整个数据准备流程远没有很多人想的那么轻松。直接下载数据就开始训练是行不通的中间至少有三个步骤必须做扎实裁剪、格式转换、数据增强。先说裁剪。原始遥感影像尺寸很大我这边拿到的数据单张在2000×2000左右如果不裁切显存直接爆掉。常用的做法是把影像切成256×256或者512×512的小图。我这次选的是512×512理由是感受野更大一些模型能看到更多上下文。但要注意切片的时候必须加重叠区域overlap我的做法是512的切片大小步长stride取256也就是每一张切片跟相邻切片有128像素的重叠。重叠的目的是避免某个建筑物刚好被切在两张切片的边界上导致标签被切断模型学到残缺的目标。另外切片时要特别关注带标注的掩码图有没有错位。遥感影像和标注经常来自不同渠道偶尔会有几个像素的偏移。如果直接训练会引入大量噪声。2.2 数据增强策略与边界把控数据增强是遥感图像分割里决定成败的一环。我用的库是albumentations这个库的好处是图像和掩码同步变换不会出现只变了图、掩码没跟着变的问题。我最后采用的增强策略如下import albumentations as A train_transform A.Compose([ A.HorizontalFlip(p0.5), A.VerticalFlip(p0.5), A.RandomRotate90(p0.5), A.RandomBrightnessContrast(p0.3, brightness_limit0.1, contrast_limit0.1), A.RandomCrop(height512, width512, p1.0), ])这里面有几个容易踩的坑。第一翻转操作对遥感图是安全的因为遥感影像是俯视图不存在上下左右的方向性问题但对街景这种正视图像垂直翻转就不合适了。第二不要对遥感图做强烈的仿射变换比如随机缩放、旋转任意角度。强烈仿射会让建筑物的几何形状严重变形模型学到的特征跟真实场景对不上测试时精度反而掉。第三颜色抖动要克制。遥感影像的光谱信息是重要的判别特征如果颜色增强太强建筑物和道路这种本来颜色就接近的地物会更容易混淆。我实测下来亮度和对比度小幅调整就够了色相和饱和度基本不动。2.3 Dataset读取与One-Hot编码数据读取这块我写了一个简单的TensorFlow Dataset对接层。核心逻辑是把图片和掩码文件路径读进来分别解码、归一化、做One-Hot编码。import tensorflow as tf def read_image(path): img tf.io.read_file(path) img tf.image.decode_jpeg(img, channels3) img tf.image.resize(img, (512, 512)) img img / 127.5 - 1.0 return img def read_mask(path): mask tf.io.read_file(path) mask tf.image.decode_png(mask, channels1) mask tf.image.resize(mask, (512, 512), methodnearest) # 二值掩码转One-Hot mask_binary tf.cast(mask 0, tf.int32) one_hot tf.one_hot(mask_binary, depth2) return tf.squeeze(one_hot, axis-1)这里有个细节掩码在resize的时候method必须指定为nearest。如果用默认的双线性插值掩码里靠近边界的位置会生成中间灰度值比如0.5这个像素点既不是前景也不是背景训练的时候模型的梯度是乱的。同样的道理在预处理阶段把掩码转成One-Hot也要保证类别索引是正确的整数。3. UNet模型在Python中的实现与训练3.1 UNet结构拆解跳跃连接为什么有效UNet这个名字的来源就是它的结构看起来像一个字母U。左侧是编码器右侧是解码器中间底部是一个瓶颈层。编码器做卷机和下采样每下采样一次特征图尺寸减半、通道数加倍解码器做上采样每次先转置卷积或插值放大再把编码器对应层的特征图拼过来。跳跃连接是整个UNet的灵魂。没有跳跃连接网络就是一条普通的编码器-解码器管道下采样丢失的细节永远回不来。有了跳跃连接解码器可以直接拿到编码器在高分辨率阶段提取的浅层特征这些特征含有丰富的边缘和纹理信息对恢复建筑物的精细轮廓至关重要。我在实际实验里对比过去掉跳跃连接的版本mIoU直接掉了近6个百分点可见这个设计有多重要。3.2 可直接运行的UNet Keras实现下面是我项目中实际使用的UNet简化版代码去掉了不必要的复杂部分保持了清晰的层次结构。import tensorflow as tf from tensorflow.keras.layers import ( Input, Conv2D, MaxPooling2D, UpSampling2D, Concatenate, BatchNormalization, Dropout, Conv2DTranspose ) from tensorflow.keras.models import Model def conv_block(x, filters, kernel_size3, dropout_rate0.1): x Conv2D(filters, kernel_size, paddingsame)(x) x BatchNormalization()(x) x tf.keras.layers.ReLU()(x) x Dropout(dropout_rate)(x) x Conv2D(filters, kernel_size, paddingsame)(x) x BatchNormalization()(x) x tf.keras.layers.ReLU()(x) return x def encoder_block(x, filters): skip conv_block(x, filters) pool MaxPooling2D(pool_size(2, 2))(skip) return skip, pool def decoder_block(x, skip, filters): x Conv2DTranspose(filters, kernel_size(2, 2), strides2, paddingsame)(x) x Concatenate()([x, skip]) x conv_block(x, filters) return x def build_unet(input_shape(512, 512, 3), num_classes2): inputs Input(shapeinput_shape) # Encoder s1, p1 encoder_block(inputs, 64) s2, p2 encoder_block(p1, 128) s3, p3 encoder_block(p2, 256) s4, p4 encoder_block(p3, 512) # Bottleneck bridge conv_block(p4, 1024) # Decoder d1 decoder_block(bridge, s4, 512) d2 decoder_block(d1, s3, 256) d3 decoder_block(d2, s2, 128) d4 decoder_block(d3, s1, 64) outputs Conv2D(num_classes, 1, activationsoftmax)(d4) model Model(inputs, outputs) return model3.3 UNet训练的核心参数与BatchSize陷阱UNet的训练参数我最终定为输入尺寸512×512batch size 8初始学习率1e-3优化器Adam训练100轮。这里有一个非常隐蔽的坑我一开始没注意到。UNet网络本身不算很深但batch size只有8时BatchNormalization层的统计量会很不稳定。BN层在训练时用的是当前batch内的均值和方差batch太小这两个统计量波动很大导致验证集上的精度跟着震荡时好时坏。应对办法有两种。一是把batch size调大但这会受显存限制二是换用GroupNormalization或InstanceNormalization它们不依赖batch内的统计量在batch size小的场景下更稳定。我实测发现batch size为8时UNet用BN和用GN的最终mIoU差了将近2个百分点。所以如果你也在用比较小的batch size训练分割模型遇到验证精度一直抖动先考虑换归一化层。4. DeepLab V3模型在Python中的实现与训练4.1 DeepLab V3的网络结构与ASPP模块原理DeepLab V3在结构上可以分为编码器Encoder和解码器Decoder两段。编码器部分使用骨干网络提取特征然后接一个ASPP模块。ASPP模块的含义是“空洞空间金字塔池化”它用一组不同膨胀率的空洞卷积去并行处理同一份特征图。空洞卷积的作用可以在不增加参数量的前提下扩大感受野。膨胀率dilation rate表示卷积核相邻点的间隔rate1就是普通卷积rate6表示卷积核间隔5个像素采样。膨胀率越大感受野越大能看到的上下文越广。ASPP同时使用rate6、12、18的卷积等于让模型分别从近、中、远三个尺度看目标最后把结果拼起来这样无论是小房子还是大工厂都能被照顾到。解码器部分也是DeepLab V3区别于早期DeepLab版本的关键。ASPP输出的特征图先上采样4倍再跟骨干网络底层的低层特征拼接用卷积融合。低层特征分辨率高、细节多正好补齐空洞卷积特征图在多次下采样中丢失的边界信息。这个设计思路和UNet的跳跃连接如出一辙但实现细节和复杂度不太一样。4.2 ASPP模块代码与实现要点以下是我实现的ASPP模块基于Keras编写的兼容TF 2.x。from tensorflow.keras.layers import ( Conv2D, BatchNormalization, GlobalAveragePooling2D, Reshape, UpSampling2D, Concatenate, ReLU ) def conv_bn_relu(x, filters, kernel_size1, dilation_rate1): x Conv2D(filters, kernel_size, paddingsame, dilation_ratedilation_rate, use_biasFalse)(x) x BatchNormalization()(x) x ReLU()(x) return x def aspp_block(inputs, filters256, rates(6, 12, 18)): # 1x1卷积分支 branch1 conv_bn_relu(inputs, filters, 1) # 三个不同膨胀率的3x3空洞卷积 branch2 conv_bn_relu(inputs, filters, 3, rates[0]) branch3 conv_bn_relu(inputs, filters, 3, rates[1]) branch4 conv_bn_relu(inputs, filters, 3, rates[2]) # 全局平均池化分支 pool GlobalAveragePooling2D()(inputs) pool Reshape((1, 1, -1))(pool) pool conv_bn_relu(pool, filters, 1) pool UpSampling2D(size(inputs.shape[1], inputs.shape[2]), interpolationbilinear)(pool) x Concatenate()([branch1, branch2, branch3, branch4, pool]) x conv_bn_relu(x, filters, 1) return x实现时有一个关键注意点全局平均池化分支必须上采样回输入特征图一样的尺寸。因为后面的Concatenate要求所有分支的分辨率一致。有些教程省略了这一步直接拼接代码会报形状错误。4.3 骨干网络选型ResNet-50还是MobileNetV2DeepLab V3原论文用的是Xception作为骨干网络但在实际工程中Xception的预训练权重难找训练又慢。我的项目里做了两个版本的对比ResNet-50和MobileNetV2。ResNet-50版本精度最高参数也最大模型体积约41MB。MobileNetV2版本的模型只有不到6MB推理速度几乎快了一倍但精度会低一些。如果你做的是学术研究或者对精度要求极高选ResNet-50如果后面要落地到Web端或移动端MobileNetV2是更务实的选择。另外一个重要的点是遥感图像的分布和ImageNet差别很大骨干网络的预训练权重只能作为初始化起点不能指望它直接适合遥感场景。我在实验中发现用ImageNet预训练权重初始化的MobileNetV2在遥感数据上需要更多的训练轮次才能完全适应前期loss下降比ResNet-50慢不少。5. 训练参数与损失函数调优实战5.1 类别不平衡处理从交叉熵到组合损失训练一开始我用的是常规的交叉熵损失函数。跑了几个epoch后发现训练集loss降得很快但验证集mIoU一直上不去。看预测结果才发现模型把几乎所有的像素都预测成了背景因为建筑物在整张图里占比太小全预测为背景就已经能拿到95%以上的准确率。这就是典型的类别不平衡问题。解决办法有很多我这边用了两个组合策略。第一使用Dice Loss CrossEntropy的组合损失。Dice Loss直接优化目标区域的重叠度对前景和背景比例不敏感非常适合小目标分割。我的实现如下import tensorflow as tf def dice_coef(y_true, y_pred, smooth1e-5): y_true_f tf.reshape(y_true, [-1]) y_pred_f tf.reshape(y_pred, [-1]) intersection tf.reduce_sum(y_true_f * y_pred_f) return (2.0 * intersection smooth) / ( tf.reduce_sum(y_true_f) tf.reduce_sum(y_pred_f) smooth ) def dice_loss(y_true, y_pred): return 1.0 - dice_coef(y_true, y_pred) def combined_loss(y_true, y_pred): ce tf.keras.losses.CategoricalCrossentropy()(y_true, y_pred) dice dice_loss(y_true, y_pred) return 0.6 * ce 0.4 * dice第二配合做了前景区域加权采样。在生成Dataset的时候统计每个切片的掩码中前景像素占比对前景占比高于阈值的切片提高采样概率。这个策略不需要改模型对提升小目标分割效果立竿见影。5.2 学习率策略与优化器选择优化器方面我最初用Adam虽然收敛快但最后阶段精度上不去。后来把两个模型的学习率策略统一成了余弦退火 Warm-up精度都有一定提升。在没有现成Warm-up回调的情况下直接用Keras的ReduceLROnPlateau是最省事的方案。我设置的是监控验证集mIoU连续5个epoch不上升就把学习率乘以0.5最低降到1e-6。配合训练100轮实际用了大概65轮就收敛了。callbacks [ tf.keras.callbacks.ModelCheckpoint( weights/deeplab_best.h5, save_best_onlyTrue, monitorval_miou, modemax ), tf.keras.callbacks.ReduceLROnPlateau( monitorval_miou, factor0.5, patience5, min_lr1e-6 ), tf.keras.callbacks.EarlyStopping( patience15, restore_best_weightsTrue ) ]5.3 过拟合与训练曲线诊断遥感图像分割的过拟合常常比分类任务更隐蔽。分类任务过拟合表现为准确率到顶后开始下降而分割任务过拟合前期往往看不出明显症状训练集mIoU一直很高验证集mIoU在一个区间震荡就是不涨。我排查过拟合用了一个很土但有效的方法拿训练好的模型去跑训练集里的样本看预测掩码跟标签是不是完全贴合。如果训练集上表现完美但验证集掉点那大概率是过拟合。这时候优先调整三点减弱数据增强强度增加Dropout或者降低模型容量。还有一种常见情况是验证集loss震荡剧烈通常不是过拟合而是验证集本身太小、样本不均衡。我这边是让验证集至少保留15%的切片并且保证各类别地物的占比跟整体分布一致这样指标才有参考价值。6. 模型评估与对比实验分析6.1 评估指标的选择与计算语义分割的评估指标最常用的是mIoU平均交并比和Pixel Accuracy。mIoU计算的是每个类别的预测区域和真实区域的交集除以并集再对类别取平均。公式如下mIoU (1 / N) * sum( TP_i / (TP_i FP_i FN_i) )我实现了一个TensorFlow版本的mIoU方便在训练过程中直接作为监控指标class MiouMetric(tf.keras.metrics.Metric): def __init__(self, num_classes2, namemiou, **kwargs): super().__init__(namename, **kwargs) self.num_classes num_classes self.intersection self.add_weight(nameinter, initializerzeros) self.union self.add_weight(nameunion, initializerzeros) def update_state(self, y_true, y_pred, sample_weightNone): y_pred tf.argmax(y_pred, axis-1) y_true tf.argmax(y_true, axis-1) for cls in range(self.num_classes): pred_mask (y_pred cls) true_mask (y_true cls) inter tf.reduce_sum(tf.cast(tf.logical_and(pred_mask, true_mask), tf.float32)) union tf.reduce_sum(tf.cast(tf.logical_or(pred_mask, true_mask), tf.float32)) self.intersection.assign_add(inter) self.union.assign_add(union) def result(self): return self.intersection / (self.union 1e-7)这里有个细节循环计算每个类别的TP、FP、FN时如果数据类别特别多比如几十类用向量化运算更好我这里只有两类循环写起来方便性能也能接受。6.2 UNet与DeepLab V3的实测对比结果在同样的数据集、同样的训练策略下最终结果如下模型mIoUPixel Accuracy参数大小单张推理时间UNet0.7210.96331MB45msDeepLab V3 (ResNet-50)0.7580.97141MB70msDeepLab V3 (MobileNetV2)0.7260.9645.8MB32ms结果符合预期但并不意外。DeepLab V3(ResNet-50)在各指标上都领先尤其是mIoU比UNet高出近4个百分点。直观上看DeepLab V3在大尺度地物和复杂背景区域的表现更好UNet则在一些小目标的边缘细节上更紧凑但误检率稍高。MobileNetV2版本的DeepLab V3精度跟UNet基本持平但参数和推理速度优势非常明显。这个对比说明如果产品对实时性要求高MobileNetV2是性价比最高的选择。6.3 预测结果可视化与后处理训练完成后不能直接拿原始分割结果交差。模型输出的掩码图通常存在两类问题一是零散的噪点比如一个孤立的像素被误判为建筑物二是建筑物边缘存在锯齿和不连续。我这边加了后处理步骤用OpenCV做形态学闭运算和小连通域剔除import cv2 import numpy as np def postprocess(pred_mask, min_area100): # pred_mask 是一张0/1的单通道二值图 kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)) mask cv2.morphologyEx(pred_mask, cv2.MORPH_CLOSE, kernel) # 删除面积小于阈值的连通域 num_labels, labels, stats, _ cv2.connectedComponentsWithStats(mask, connectivity8) cleaned np.zeros_like(mask) for i in range(1, num_labels): if stats[i, cv2.CC_STAT_AREA] min_area: cleaned[labels i] 1 return cleaned这一个后处理步骤单看mIoU能提升0.5到1个百分点。虽说不算多但在实际交付里减少了大量肉眼可见的误检区。7. 常见问题与排查技巧实录7.1 显存溢出OOM怎么办训练时最容易遇到的就是显存溢出最常见原因是输入尺寸太大、batch size太大、或者模型参数太多。我的处理顺序是先把batch size从8降到4如果还溢出把输入尺寸从512降到384或者256再不行换参数量更小的骨干网络。另外一个技巧是开启混合精度训练。TensorFlow里加两行配置显存占用能减少30%左右速度还有提升。对遥感影像这种大输入分辨率场景很实用。from tensorflow.keras import mixed_precision mixed_precision.set_global_policy(mixed_float16)7.2 训练loss下降缓慢甚至不降这种情况我遇到过两次。第一次是学习率问题Adam初始学习率用了1e-2loss直接飞出天际。第二次是数据问题Dataset里图片和掩码没对齐模型相当于在学习一张图配另一张图的错误标签。排查步骤先用几十张数据过拟合看看如果几轮epoch后loss能降下来说明模型和代码没问题问数据如果loss纹丝不动检查数据管线、归一化、One-Hot编码从头捋。7.3 预测结果出现棋盘格状伪影棋盘格状伪影checkerboard artifacts通常出现在用转置卷积做上采样的模型中。严格说这不是UNet独有的问题而是转置卷积的常见问题。如果这种情况严重影响视觉效果可以把上采样方式从转置卷积换成UpSampling2D Conv2D也就是先插值再卷积。我实际对比过Conv2DTranspose在小数据集上更容易产生棋盘格而UpSampling2D Conv2D生成的结果更平滑。代价是边缘稍微柔和一些但对遥感建筑物提取影响不大。介意精度损失的话保持转置卷积也没问题。7.4 分割结果有大量盐和胡椒噪声如果预测结果上散布着密密麻麻的小噪点多到肉眼可见通常说明模型对局部区域不自信或者输入切片本身存在大量重叠导致同一个目标在不同切片上预测结果不一致。重叠区域的问题可以在推理阶段解决对重叠切片赋予非均匀的权重只有中心区域的计算结果参与最终预测边缘区域的结果直接丢弃。这种方法叫overlap-tile策略实现起来不难但能明显提升拼接区域的一致性。这也是我在做整幅大图推理时的默认做法。7.5 验证集指标高但实际效果差最后提醒一个容易被忽略的问题。有些人训练时验证集mIoU看起来很高但把模型放到新的遥感影像上一测效果很差。这种问题往往不是模型本身的问题而是数据集划分的问题。遥感影像中相邻切片之间有很强的空间相关性。如果训练集和验证集的切片来自同一张原始大图那验证集指标虚高。正确的做法是按原始影像来划分比如以影像文件为最小单位一部分原始影像整体划分给训练集另一部分整体划给验证集。这样模型在验证集上的表现才更接近真实场景的泛化能力。我在项目里一开始就是因为没注意这点导致验证集mIoU虚高了5个百分点后来重新划分数据后才得到可靠的指标。这种数据泄漏问题在遥感任务里特别容易踩因为相邻切片看起来像是不同样本实际上语义内容高度重叠。本文还有配套的精品资源点击获取
返回列表