基于Deeplab-ResNet的遥感建筑物变化检测实战与避坑指南
简介基于Deeplab-resnet算法的建筑物变化检测设计源码面向遥感、GIS及计算机视觉方向的研究者与开发者可用于识别建筑物新建、拆除或损毁情况。项目融合空洞卷积与残差网络的优点在高分辨率影像分割中既能保持空间结构又能逐级提取深层特征对建筑物边界划分更为精准。资源共40个文件以33个Python脚本为主体覆盖train.py训练、predict.py预测、config.py参数配置以及dataloaders数据加载、utils工具函数、loss与metrics评价指标等模块另有5个txt说明文档、授权协议和Git忽略文件压缩包仅170KB便于快速部署。目前已有330人学习浏览。工程实现上源码还整合了同步批归一化、多种骨干网络如resnet、xception、mobilenet以及VOCdevkit数据集组织方式可帮助读者理解完整的变化检测流水线。源码为相关课题提供了可直接运行的基线方案也为进一步改进空洞卷积与残差结构的融合提供了可扩展的实验框架。1. 基于 Deeplab-resnet 的建筑物变化检测这份源码能直接拿来跑实验吗做遥感变化检测的人多半被同一件事折磨过手头有两期不同时间的卫星影像想快速找出哪些地方新盖了楼、哪些地方拆了房传统方法不是误检多就是边界糊成一片。我在评估灾害损毁和违建图斑时第一轮筛选用的就是 Deeplab-resnet 这类语义分割模型——比起先做特征差分再分类的思路端到端分割更省心边界也更干净。这份基于 Deeplab-resnet 的建筑物变化检测源码打包了训练到预测的完整链路train.py 负责训练、predict.py 负责推理、config.py 管全局参数还带了 VOC 格式数据集的转换脚本和同步批归一化实现适合刚入手变化检测的研究生也适合想快速搭一个 baseline 的算法工程师。它能解决的是「两期影像输入变化建筑物掩膜输出」这件事但你得先把它跑通才知道参数和坑在哪。2. 把 Deeplab 和 ResNet 拧在一起空洞卷积、ASPP 与骨干网的拆解2.1 为什么变化检测偏爱 Deeplab-resnet变化检测本质上是一个二分类语义分割问题——每个像素要么是「变化了的建筑物」要么是「背景」。遥感影像里的建筑物尺度差异很大城中村的小平房和工业区的大厂房可能同时出现在一张图里。普通分割网络为了增大感受野通常连续下采样最经典的做法是 VGG 风格的 5 次池化到最后特征图只剩输入的 1/32。这对小目标非常不友好一栋十几米宽的民房在深层特征图里可能只剩一两个像素边界早被抹平了。Deeplab 系列的核心是把空洞卷积Dilated/Atrous Convolution引入分割任务。空洞卷积在卷积核内部插入空洞在不增加参数量、不降低分辨率的前提下扩大感受野。以 kernel_size3、dilation_rate6 为例等效感受野相当于 13x13 的普通卷积但参数还是那 9 个。ResNet 承担的是骨干特征提取任务它的残差连接解决了深层网络梯度传递的问题让网络可以堆到 101 层甚至更深而不退化。两个东西结合之后网络能同时获得大感受野和高分辨率特征图这对建筑物边界和中小目标的检测来说几乎是量身定做的。2.2 源码里的模型骨架从 backbone 到 deeplab.py打开这份源码的net目录你会看到一套完整的分割模型工程化结构。net/backbone/下面放了resnet.py、drn.py、xception.py、mobilenet.py四种特征提取网络默认是 ResNet。net/deeplab.py是主模型文件net/aspp.py是空洞空间金字塔池化模块。我直接把deeplab.py里的关键初始化逻辑拉出来看class DeepLab(nn.Module): def __init__(self, backboneresnet, output_stride16, num_classes2, sync_bnTrue, freeze_bnFalse): super(DeepLab, self).__init__() if backbone resnet: self.backbone ResNet101(output_strideoutput_stride, sync_bnsync_bn) elif backbone xception: self.backbone AlignedXception(output_strideoutput_stride, sync_bnsync_bn) # ... 其他backbone分支 self.aspp ASPP(backbone, output_stride, sync_bnsync_bn) self.decoder Decoder(num_classes, sync_bnsync_bn)output_stride是 Deeplab 体系里最关键的超参。它表示输入图像尺寸与最终特征图尺寸的比值取 16 时特征图是输入的 1/16取 8 时是 1/8。源码默认 16这是精度和显存开销的折中output_stride8 边界更精细但特征图大了两倍显存占用几乎翻番而且训练时间明显变长。我第一次在 1080Ti 上跑试着改成 8 直接 OOM后来老老实实换回 16。ASPP 模块是 Deeplab 区分于普通空洞卷积网络的标志。它并行使用多个不同空洞率的卷积分别抓取不同尺度的上下文信息。默认配置是 1x1 卷积 三个 3x3 空洞卷积rate 分别为 6、12、18 全局平均池化分支最后拼到一起过 1x1 卷积压缩通道。这里有个参数容易被忽略最后一个 rate 如果设得过大3x3 卷积的等效感受野会超过特征图尺寸边界补零带来的无效计算会变多。在 512x512 输入、output_stride16 的情况下rate18 已经是合理上限。2.3 同步批归一化多卡训练不翻车的关键源码目录里有net/sync_batchnorm/这个模块值得单独说。batchnorm.py实现了跨 GPU 的同步批归一化comm.py负责 GPU 间的通信聚合unittest.py是自测脚本。为什么要同步PyTorch 原生的nn.BatchNorm2d在分布式训练时每个 GPU 各自计算自己那块数据的均值和方差不与其他卡通信。当单卡 batch size 很小比如 2 或 4时BN 统计量噪声极大模型训练会很不稳定甚至出现 loss 震荡不收敛。sync_batchnorm 在 DistributedDataParallel 的支持下先同步所有卡上的 batch 统计量再统一做归一化。这在train.py里是这么用的if args.sync_bn is True: model nn.SyncBatchNorm.convert_sync_batchnorm(model)这行代码在单卡环境下不要开。sync_batchnorm 依赖进程组通信单进程时初始化通信组会报错。我一般只在多卡训练脚本里加这个开关并配合torch.distributed.init_process_group一起用。如果你只有一张卡老老实实保持sync_bnFalse然后靠增大单卡 batch size 来稳定 BN 统计量。2.4 loss 和 metrics变化检测到底在优化什么loss.py和metrics.py分别定义了损失函数和评估指标。源码默认的 loss 是交叉熵同时兼容ignore_index255——这是 VOC 数据集里边界和难分样本的常用标记。变化检测里类别极不平衡是常态一个城市的图斑里变化建筑可能只占 5% 的像素剩下的全是被标记为 0 的背景。纯交叉熵在这种数据上很容易让模型把所有像素都预测成背景指标看起来 95% 准确率但实际一张变化图都没检测出来。calculate_weights.py干的就是这个事——计算类别频率给 minority class 加大权重。它统计数据集中每个类别的像素占比然后生成一个权重向量传进交叉熵损失里# 常见做法: median frequency balancing class_weights median_frequency_balancing(class_counts, num_classes2) criterion nn.CrossEntropyLoss(weightclass_weights)metrics.py里通常包含 pixel accuracy、mean IoU、F1 分数。变化检测场景下我更推荐看 IoU 而不是 pixel accuracy。准确率会被大量背景像素抬高而 IoU 真正衡量的是「预测的变化区域」和「真实变化区域」的交叠程度图斑位置偏移一点IoU 直接掉十几个点。后面调阈值或调损失都应以 IoU 为准。3. 数据准备与训练流程从 VOC 格式到跑通 train.py3.1 数据怎么组织VOCdevkit 下的目录约定源码仓库里带了一个VOCdevkit/VOC2007目录骨架下面是ImageSets、JPEGImages、SegmentationClass等子目录这是 Pascal VOC 的标准布局。ImageSets/Segmentation/train.txt和val.txt里写的不是图像路径而是不含扩展名的文件名。dataloader 拿到这个名字后去JPEGImages找输入图去SegmentationClass找掩膜图。变化检测任务通常需要两期影像——我用的是 t1 和 t2 两个时间的正射影像。常见做法是把两期影像在通道维拼接形成一个 6 通道输入掩膜则是 0/1 的二值图。如果你的数据不是这个格式就需要先调整dataloaders/datasets/pascal.py里的图像加载逻辑。源码里的voc2unet.py脚本就是干这类转换的作用是把 VOC 格式的组织方式转成 U-Net 风格的数据集索引。我在实际项目中直接用voc2unet.py生成一个包含所有训练样本路径的 CSV然后重写一个 Dataset 类按行读取 t1 和 t2 图像。3.2 custom_transforms.py数据增强的顺序有讲究dataloaders/custom_transforms.py定义了训练和验证时的数据变换。典型的训练变换是随机缩放、随机裁剪、随机旋转、水平翻转最后做标准化。源码里用的是Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225])这是 ImageNet 预训练权重对应的标准化参数。如果你微调的是在 ImageNet 上预训练的 ResNet就务必保留这三个值换了自己统计的 mean/std 会导致预训练权重失效模型要从头学。有个细节是 RandomCrop 的 crop_size 要和模型输入尺寸匹配。这份源码的默认配置是 513但遥感影像不需要那么大我一般设在 384 或 512。裁剪尺寸直接决定单卡 batch size 上限512 配 batch 8 在 1080Ti 上刚好占满显存。数据增强是廉价的正则化但别同时开旋转和翻转——变化检测里建筑物的朝向是真实物理信息水平翻转没问题90 度旋转会破坏影像的北向含义建议只在训练集特别小时才用。3.3 train.py 的参数配置照着跑通一轮训练train.py是可执行的主入口config.py集中管理超参数。我用这份源码时的启动命令示例python train.py --backbone resnet --out-stride 16 --epochs 80 \ --batch-size 8 --base-lr 0.01 --gpu-ids 0 1 \ --dataset pascal --year 2007 --crop-size 512 \ --eval-interval 5 --ckpt checkpoints/参数含义--out-stride 16对应 Deeplab 的 output_stride控制特征图下采样倍数--base-lr 0.01是初始学习率配合 poly 策略衰减。train.py 的 lr_scheduler 一般实现为lr * (1 - iter/total_iter)^powerpower 默认 0.9这是分割任务的标准配置--gpu-ids 0 1指定多卡 ID配合--sync-bn使用--eval-interval 5每 5 个 epoch 跑一次验证我习惯设 2变化检测训练 loss 下降快但 IoU 波动大看得勤一点心里有底学习率是这里最容易翻车的参数。分类任务的常见初始学习率是 0.01但分割任务配合批量归一化和 poly 衰减0.01 在单卡 batch 8 下有点激进了我实测 0.007 到 0.01 是稳定区间超过 0.01 基本第二个 epoch 就会 loss 爆炸。3.4 dataloaders 的双数据集加载训练集验证集互不污染源码里dataloaders/datasets/下有pascal.py、coco.py、sbd.py、cityscapes.py、combine_dbs.py。combine_dbs.py的作用是把多个数据集拼成一个联合数据集这在有多个不相干区域的标注数据时很有用。但我提醒一句变化检测的语义是「同一个地方在两个时间点的差异」不同区域的数据拼接在一起训练模型的泛化性会受影响因为场景光照、传感器差异会把模型带偏。我自己的经验是优先保证训练集和验证集来自同一地区同一传感器跨区域泛化那是下一个课题。dataloaders 目录里utils.py有一些数据处理的公共函数比如 relabel 和去重。__init__.py里通常会暴露一个make_data_loader工厂函数根据config.py的 dataset 字段自动选择数据类。跑训练之前建议先单独运行一次python -c from dataloaders import make_data_loader; loader make_data_loader(...)确认数据路径没问题不然训练跑到一半才发现文件路径错浪费时间。4. Deeplab-resnet 实战避坑五个我踩过的坑与排查方法4.1 RGB 顺序错乱导致模型性能断崖现象训练 loss 下降正常但验证集 IoU 奇低预测图斑呈大面积噪声。原因遥感影像可能是 BGR 顺序比如 OpenCV 默认读取而模型预训练权重是在 RGB 顺序上训练的。如果加载图像后没有转成 RGB等于输入了错误的颜色通道特征图语义完全错位。解决检查 dataloader 里图像的读取方式。如果用的是cv2.imread请加一行img cv2.cvtColor(img, cv2.COLOR_BGR2RGB)如果用的是 PIL则保持Image.open的 RGB 顺序。每次换数据集都要确认这一步。4.2 batch size 太小导致 BN 统计量抖动现象训练 loss 先降后升或者每次迭代 loss 剧烈震荡验证集指标忽高忽低。原因BatchNorm 在单卡 batch size2 甚至 1 时均值和方差的估计噪声过大相当于给网络注入随机扰动。解决优先增加单卡 batch size到 8 以上。显存不够就降低 crop_size 或改用 output_stride16。实在不行可以冻结部分 BN 层源码里freeze_bn参数就是干这个的但冻结后模型表达能力受限是最后的妥协方案。4.3 类别不平衡导致模型只会输出背景现象训练 loss 很低但变化区域一个都检不出来可视化预测结果是全黑图全背景。原因变化建筑像素占比太小交叉熵损失被背景类主导模型找到的局部最优就是把所有像素预测为背景。解决确认calculate_weights.py生成的类别权重是否正确传入了损失函数。重点检查权重向量是否归一化以及CrossEntropyLoss的weight参数是不是放在了 GPU 设备的同设备上。我踩过 cuda tensor 和 cpu tensor 混用导致权重无效的坑——报错不报错但权重就是不起作用。4.4 lr_scheduler 和 optimizer 的加载顺序混乱现象断点续训后 loss 数值和训练前的预期对不上前几个 epoch 特别猛后面又掉不动。原因PyTorch 加载 checkpoint 时state_dict 需要按optimizer-lr_scheduler的顺序恢复否则学习率状态从零开始相当于用初始学习率重启训练。解决断点恢复时严格按顺序加载checkpoint torch.load(checkpoint.pth) model.load_state_dict(checkpoint[model_state]) optimizer.load_state_dict(checkpoint[optimizer_state]) lr_scheduler.load_state_dict(checkpoint[lr_scheduler_state])这样恢复出来的学习率值与中断前完全一致不会出现前几个 epoch 重新加速的怪现象。4.5 val 时忘记关 BN 的 training 模式现象验证集预测结果比训练集差很多且拖影严重。原因模型在验证时没有切换成 eval 模式BN 层仍在用当前 batch 的统计量做归一化。遥感影像通常是一整张大图切块推理单块的像素统计和整体有偏差结果一团糟。解决验证和推理前必须调用model.eval()并关掉梯度计算model.eval() with torch.no_grad(): pred model(img.unsqueeze(0))另外如果你的模型设置了freeze_bnTrue在model.eval()之后再调model.train()会被冻结 BN 一起带偏要单独做逻辑隔离。5. 从训练到成图predict.py 的推理细节和变化后处理技巧predict.py的逻辑比train.py简单得多但细节决定最终交付质量。推理的本质是把训练好的模型权重加载进来跑一次前向传播拿到每个像素属于变化班类的概率然后二值化或做颜色映射。这份源码里 predict.py 的大致结构是# 加载模型结构 model DeepLab(num_classes2, backboneresnet, output_stride16) checkpoint torch.load(checkpoints/best_model.pth, map_locationcpu) model.load_state_dict(checkpoint[model_state]) model model.cuda() model.eval() # 两期影像通道拼接后前向 t1 load_and_normalize(t1.tif) t2 load_and_normalize(t2.tif) x torch.cat([t1, t2], dim0).unsqueeze(0).cuda() with torch.no_grad(): output model(x) prob torch.softmax(output, dim1)[0, 1].cpu().numpy() # 取变化类的概率这里的输出是原始 logits取 softmax 后第 1 通道就是「建筑物变化」的概率图。概率图不要直接用 0.5 默认阈值二值化这个值一般偏保守。我更推荐的做法是先在验证集上扫一遍阈值0.3 到 0.7步长 0.05找到 IoU 最高的那个阈值再全图推理。批量处理时可以写一个简单脚本pred_mask (prob best_threshold).astype(np.uint8) * 255还有一个小技巧整张大影像建议切块推理块与块之间重叠 64 像素避免边缘部分因感受野不足产生割裂。切块后按位置拼回去重叠区取平均概率而不是硬投票拼接缝会平滑很多。推理后处理方面我通常对掩膜做一次 3x3 的中值滤波去孤立噪点再通过连通域分析滤掉面积小于 100 像素的碎块——遥感里小于这个面积的变化目标大概率是配准误差或瞬时云影。从那以后我每次跑变化检测实验都会强制走一遍这四个检查通道顺序对不对、类别权重有没有生效、验证有没有 model.eval()、阈值是不是在验证集上扫出来的。这套流程帮我挡掉过至少五次无效实验希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →