简介:基于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_size=3、dilation_rate=6 为例,等效感受野相当于 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, backbone='resnet', output_stride=16, num_classes=2, sync_bn=True, freeze_bn=False): super(DeepLab, self).__init__() if backbone == 'resnet': self.backbone = ResNet101(output_stride=output_stride, sync_bn=sync_bn) elif backbone == 'xception': self.backbone = AlignedXception(output_stride=output_stride, sync_bn=sync_bn) # ... 其他backbone分支 self.aspp = ASPP(backbone, output_stride, sync_bn=sync_bn) self.decoder = Decoder(num_classes, sync_bn=sync_bn)output_stride是 Deeplab 体系里最关键的超参。它表示输入图像尺寸与最终特征图尺寸的比值,取 16 时特征图是输入的 1/16,取 8 时是 1/8。源码默认 16,这是精度和显存开销的折中:output_stride=8 边界更精细,但特征图大了两倍,显存占用几乎翻番,而且训练时间明显变长。我第一次在 1080Ti 上跑,试着改成 8 直接 OOM,后来老老实实换回 16。
ASPP 模块是 Deeplab 区分于普通空洞卷积网络的标志。它并行使用多个不同空洞率的卷积,分别抓取不同尺度的上下文信息。默认配置是 1x1 卷积 + 三个 3x3 空洞卷积(rate 分别为 6、12、18)+ 全局平均池化分支,最后拼到一起过 1x1 卷积压缩通道。这里有个参数容易被忽略:最后一个 rate 如果设得过大,3x3 卷积的等效感受野会超过特征图尺寸,边界补零带来的无效计算会变多。在 512x512 输入、output_stride=16 的情况下,rate=18 已经是合理上限。
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_bn=False,然后靠增大单卡 batch size 来稳定 BN 统计量。
2.4 loss 和 metrics:变化检测到底在优化什么
loss.py和metrics.py分别定义了损失函数和评估指标。源码默认的 loss 是交叉熵,同时兼容ignore_index=255——这是 VOC 数据集里边界和难分样本的常用标记。变化检测里类别极不平衡是常态:一个城市的图斑里,变化建筑可能只占 5% 的像素,剩下的全是被标记为 0 的背景。纯交叉熵在这种数据上很容易让模型把所有像素都预测成背景,指标看起来 95% 准确率,但实际一张变化图都没检测出来。
calculate_weights.py干的就是这个事——计算类别频率,给 minority class 加大权重。它统计数据集中每个类别的像素占比,然后生成一个权重向量,传进交叉熵损失里:
# 常见做法: median frequency balancing class_weights = median_frequency_balancing(class_counts, num_classes=2) criterion = nn.CrossEntropyLoss(weight=class_weights)metrics.py里通常包含 pixel accuracy、mean IoU、F1 分数。变化检测场景下我更推荐看 IoU 而不是 pixel accuracy。准确率会被大量背景像素抬高,而 IoU 真正衡量的是「预测的变化区域」和「真实变化区域」的交叠程度,图斑位置偏移一点,IoU 直接掉十几个点。后面调阈值或调损失,都应以 IoU 为准。
3. 数据准备与训练流程:从 VOC 格式到跑通 train.py
3.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)^power,power 默认 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 size=2 甚至 1 时,均值和方差的估计噪声过大,相当于给网络注入随机扰动。
- 解决:优先增加单卡 batch size到 8 以上。显存不够就降低 crop_size 或改用 output_stride=16。实在不行可以冻结部分 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_bn=True,在model.eval()之后再调model.train()会被冻结 BN 一起带偏,要单独做逻辑隔离。
5. 从训练到成图:predict.py 的推理细节和变化后处理技巧
predict.py的逻辑比train.py简单得多,但细节决定最终交付质量。推理的本质是把训练好的模型权重加载进来,跑一次前向传播拿到每个像素属于变化班类的概率,然后二值化或做颜色映射。这份源码里 predict.py 的大致结构是:
# 加载模型结构 model = DeepLab(num_classes=2, backbone='resnet', output_stride=16) checkpoint = torch.load('checkpoints/best_model.pth', map_location='cpu') 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], dim=0).unsqueeze(0).cuda() with torch.no_grad(): output = model(x) prob = torch.softmax(output, dim=1)[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()、阈值是不是在验证集上扫出来的。这套流程帮我挡掉过至少五次无效实验,希望帮到你。
本文还有配套的精品资源,点击获取