FlowS-Unet卫星图变化检测模型:结构解析与工程实践
2026/9/18 22:23:25 网站建设 项目流程

简介:针对国土监察中建筑物建、拆、改、扩的监管需求,遥感图像变化检测已成为重要手段。这份基于FlowS-Unet的遥感图像建筑物变化检测文档,面向深度学习与遥感图像处理领域的研究人员、工程师及高校学生,系统阐述了如何改进U-Net以精准检测不同时期卫星影像中建筑物的变化区域。文档先从人工巡查与视频监控的局限引入,梳理传统图像处理方法在配准精度、数据适配上的痛点,继而介绍FCN、U-Net、FlowNet等深度学习模型的发展脉络,重点讲解FlowS-Unet的网络结构、细化特征融合思想及其在QuickBird影像上的实验对比与竞赛成绩,能帮助读者快速把握从问题建模到模型设计的完整思路。资源为单个docx文件,大小约322KB,阅读轻便;目前已有314人学习,适合作为遥感变化检测、语义分割方向的学习笔记或论文写作参考。

1. FlowS-Unet 是在解决卫星图变化检测的哪个痛点

国土监察业务中真正难啃的不是“哪里有房子”,而是“房子的建、拆、改、扩怎么自动发现”。人工巡查在大城市根本不现实,高清摄像头加 GPS 的方案只适合小范围试点,建设成本和维护周期都太长。卫星影像覆盖广、成本低,但拿两期影像直接做差会得到满屏噪声:不同拍摄角度导致建筑倾斜、拼接区域色调不一致、云和雾覆盖,任何一项都能把传统分割模型打回原形。

FlowS-Unet 是 2017 年阿里云天池“广东政务数据创新大赛-智能算法赛”的参赛方案,最终拿了综合第 2 名。它把两期影像合并成 8 通道输入,端到端输出变化建筑物的二值图,不需要精确配准,也不需要先把所有建筑物分割出来再相减。后处理前 F1 分数 0.933,比同条件下 U-Net 的 0.898 高出近 4 个百分点。如果你在做遥感图像变化检测、建筑物提取,或者想给 U-Net 类模型加“时相变化”能力,这套思路可以直接复用。

2. FlowS-Unet 结构拆解:编码器-解码器骨架、超列融合与分层独立预测

2.1 为什么 U-Net 和 FCN 在变化检测上都不够用

FCN 的核心问题是低分辨率输出牺牲了定位精度,它对每个像素独立分类,没有把像素与像素之间的空间关系和值关系纳入考虑,分割结果缺乏空间一致性。U-Net 通过编码器-解码器结构缓解了一部分问题,浅层特征保留位置信息,深层特征做语义分类,但它的最后一层只是把前几层特征上采样后简单融合,这种特征表达更适合“把所有建筑物分割出来”,而不是“判断哪些像素在两期影像之间发生了变化”。遥感图像本身的特殊性也放大了这个缺陷:分辨率低、建筑物差异性小、轮廓简单,再加上不同卫星拍摄角度和色调不同,模型很容易过拟合到“分割建筑物”而不是“检测变化”。

传统图像处理路线,比如均质区域识别、分水岭分割、形态学房屋指数、先验形状约束水平集模型,都依赖同一地点不同时期图像的高精度配准。实际业务中这个前提很难满足,漏检率和错检率居高不下。FlowS-Unet 的出发点就是把配准问题交给网络自己学,用 8 通道输入让模型同时看到两期影像,自行建立对应像素之间的联系。

2.2 超列与 FlowNet 细化结构带来的启发

Hariharan 等人提出的 Hypercolumn(超列)解决的是 CNN 最后一层特征缺少位置细节的问题,做法是把对应像素在各层的激活值上采样后串联起来,让最后一层特征在空间上也足够精细。FlowNet 的放大部分则是反复做反卷积和上采样,每步把分辨率提升一倍,同时把各层的光流预测连接起来。这两个思路本质上是同一件事:不同层特征的空间分辨率不同,浅层负责边界定位,深层负责语义分类,把它们融合起来才能做细粒度预测。FlowS-Unet 把这个融合逻辑放进了 U-Net 的扩张路径,在每次融合后都做独立预测。

2.3 编码器:4 组卷积、池化与批量归一化

FlowS-Unet 共 17 个卷积层,3 次下采样、3 次上采样,没有全连接层。编码器部分是典型的卷积特征提取结构:4 组卷积操作,同一组内连续做 2 次 3×3 卷积,卷积核数目从浅到深依次为 32、64、128、256,每两组之间做一次池化降维,过滤高频噪声。每步卷积后使用 ReLU 激活,编码器一侧加入 BN(BatchNorm)层,让每个 batch 的特征分布相对稳定,缓解网络加深带来的梯度弥散问题。

用 PyTorch 复现编码器骨架时,我一般这样写:

import torch import torch.nn as nn class FlowSUnetEncoder(nn.Module): def __init__(self): super().__init__() # 两期影像各4通道,合并为8通道输入 self.conv1 = self._block(8, 32) # 输出 (B, 32, H, W) self.pool1 = nn.MaxPool2d(2) # 输出 (B, 32, H/2, W/2) self.conv2 = self._block(32, 64) # 输出 (B, 64, H/2, W/2) self.pool2 = nn.MaxPool2d(2) # 输出 (B, 64, H/4, W/4) self.conv3 = self._block(64, 128) # 输出 (B, 128, H/4, W/4) self.pool3 = nn.MaxPool2d(2) # 输出 (B, 128, H/8, W/8) self.conv4 = self._block(128, 256) # 输出 (B, 256, H/8, W/8) def _block(self, in_ch, out_ch): # 每组连续2次3x3卷积,卷积后接BN和ReLU return nn.Sequential( nn.Conv2d(in_ch, out_ch, 3, padding=1), nn.BatchNorm2d(out_ch), nn.ReLU(inplace=True), nn.Conv2d(out_ch, out_ch, 3, padding=1), nn.BatchNorm2d(out_ch), nn.ReLU(inplace=True), )

注意输入通道数必须是两期影像通道数之和。原始数据是 4 通道 tiff,两期合并后为 8 通道;如果换成 RGB 三通道影像,这里改成 6 即可。每次 3×3 卷积 padding 为 1 是为了保持特征图尺寸不变,池化负责降维。

编码器各阶段输出尺寸如下:

阶段操作通道数空间尺寸
输入两期影像合并8H×W
conv12×3×3 卷积 + BN32H×W
pool12×2 最大池化32H/2×W/2
conv22×3×3 卷积 + BN64H/2×W/2
pool22×2 最大池化64H/4×W/4
conv32×3×3 卷积 + BN128H/4×W/4
pool32×2 最大池化128H/8×W/8
conv42×3×3 卷积 + BN256H/8×W/8

2.4 解码器:三输入融合、Dropout 与 1×1 卷积独立预测

解码器是 FlowS-Unet 和普通 U-Net 差异最大的地方。每个融合节点的输入分三部分:上一层反卷积得到的深层抽象特征、编码器对应层生成的浅层局域特征,以及当前步骤自身反卷积的结果。这三部分通过通道连接(concatenate)融合,逐步恢复目标的细节和空间维度。融合后加 Dropout 层,随机让 50% 的隐藏节点停止工作,提高泛化能力,防止在小数据集上过拟合。之后用两次 3×3 卷积消除上采样带来的混淆效应。

关键改动是每层融合后都用 2 个 1×1 卷积核把特征向量映射到期望的类别数上。也就是说,每一层都能独立输出一个预测图,而不是只有最后一层出结果。这样做的好处是浅层特征里含有丰富的位置信息,对小建筑物的变化检测特别重要。解码器部分我这样实现:

class FlowSUnetDecoder(nn.Module): def __init__(self): super().__init__() # 上采样:反卷积,输出尺寸翻倍 self.up1 = nn.ConvTranspose2d(256, 128, 2, stride=2) self.fuse1 = self._fuse_block(128 + 128, 128) # 与编码器conv3特征拼接 self.up2 = nn.ConvTranspose2d(128, 64, 2, stride=2) self.fuse2 = self._fuse_block(64 + 64, 64) # 与编码器conv2特征拼接 self.up3 = nn.ConvTranspose2d(64, 32, 2, stride=2) self.fuse3 = self._fuse_block(32 + 32, 32) # 与编码器conv1特征拼接 # 每层用2个1x1卷积核独立预测2类(变化/不变) self.pred1 = nn.Conv2d(128, 2, 1) self.pred2 = nn.Conv2d(64, 2, 1) self.pred3 = nn.Conv2d(32, 2, 1) def _fuse_block(self, in_ch, out_ch): return nn.Sequential( nn.Dropout(0.5), nn.Conv2d(in_ch, out_ch, 3, padding=1), nn.ReLU(inplace=True), nn.Conv2d(out_ch, out_ch, 3, padding=1), nn.ReLU(inplace=True), )

pred1pred2pred3就是三个独立预测头,分别对应不同上采样阶段的特征融合结果。训练时三个预测头都会参与损失计算,推理时可以只取最后一层的输出,也可以把多层概率做加权平均。

2.5 8 通道输入的设计含义

把两期影像直接拼成 8 通道输入,是 FlowS-Unet 能绕过配准的关键。网络在卷积过程中会自动学习两幅影像之间的对应关系,不需要预先做像素级对齐。实验里用的 QuickBird 卫星影像,不同年份拍摄角度和色调都有差异,但模型依然能保持稳定的变化检测能力,说明这种设计对不可控的成像条件有天然容忍度。

3. 遥感数据预处理与训练集构造:拼接缝、云雾、多尺度切片与 z-score

3.1 原始数据的坑:16 位深、拼接痕迹、云雾覆盖

原始卫星图是 15106×15106 像素、4 通道、每像元 16 位,数据实际范围在 0 到 2774 之间,其中 0~450 和 1500~2774 两个区间占比非常少。影像由多景数据拼接而成,拼接边缘有明显痕迹,部分区域有云和雾。更麻烦的是,原始数据只包含 5% 地块的国土审批记录图和一份人工精准标注的小数据集,完整标注需要自己做。

16 位数据不能直接当作 8 位图输入网络。直接线性映射到 0~255 会让低值区间占比过大的问题被放大,直方图累积 2% 到 98% 之间的线性拉伸更实用。具体做法是:取累积直方图 2% 处的像元值作为下限 MinValue,98% 处作为上限 MaxValue;像元值在两者之间则线性拉伸到 0~255,小于 MinValue 置 0,大于 MaxValue 置 255。

import numpy as np def linear_stretch(img, low=2, high=98): # 计算累积直方图指定百分位对应的像元值 p_low, p_high = np.percentile(img, [low, high]) out = (img.astype(np.float32) - p_low) * 255.0 / (p_high - p_low) # 低于下限的置0,高于上限的置255 out = np.clip(out, 0, 255) return out.astype(np.uint8)

np.percentile计算的是整幅图的百分位点,参数[low, high]控制截断范围。低 2% 和高 2% 的离群值被裁掉,避免少数亮像素把整个拉伸范围撑开。这个函数对每个通道分别调用,不能在通道间混算,否则会破坏波段间的相关性结构。

3.2 Canny 拼接检测与 DehazeNet 微调去云雾

拼接痕迹用 Canny 边缘检测确认后,把原图分成四块,每次只取一块作为感兴趣区域,其他区域用掩膜屏蔽。这样做是为了避免在拼接缝附近做拉伸时,两边色差被同时拉进有效范围。对每一块掩膜图像单独做 2% 线性拉伸后,拼接区域的颜色差异会明显降低。

云雾处理参考了 DehazeNet 的思路。原论文里没有展开细节,我复现时一般这样处理:用带云雾的影像块和无云雾影像块做微调训练,让网络学习云雾特征并生成透射率图,再按大气散射模型反演出去雾后的图像。参数微调时学习率要比原模型小一个数量级,通常取 0.0001,否则容易把去雾模型原本学到的自然图像分布破坏掉。

3.3 人工标注、one-hot 标签与数据集划分

标注环节是遥感图像标注流程里最耗时也最决定模型上限的一步。原方案用自制标注工具绘制多边形覆盖变化的建筑物,标签做 one-hot 处理成二通道数据:变化建筑物记为 1,其他记为 0。注意这里和普通分割的差异——标注对象不是“所有建筑物”,而是“变化了的建筑物”,两期影像对比着标,工作量集中在判断“变没变”上。

数据集划分和裁剪参数如下:

数据集区域尺寸(像素)是否做数据增强用途
训练集15106×11106是,旋转/翻转/缩放模型训练
验证集15106×1000收敛判断与调参
测试集15106×3000最终效果评估

训练集采用多尺度图像块与滑动窗口结合的方式切割,图像块尺寸为 224、256、288、320 像素,区域块重叠覆盖到整幅图像。之后再通过旋转、翻转和缩放做数据增强。四个尺度的设计不是随意定的:224 适合小建筑,320 能覆盖更多上下文信息,256 和 288 作为中间档让网络适应不同建筑物尺度。

3.4 z-score 归一化:为什么要用全局统计量

不同区域的遥感图像平均亮度和像素值分布差异很大,直接训练会让网络对亮度敏感。z-score 标准化把输入数据调整到均值为 0、方差为 1 的标准正态分布,计算式为 x' = (x - μ) / δ,其中 μ 是全体样本数据的均值,δ 是全体样本数据的方差。注意这里用的是全体样本统计量,不是单张图逐图归一化。逐图归一化会抹掉图像间的真实亮度差异,模型在预测时反而会因为测试图像亮度分布不同而失效。

归一化要在数据增强完成之后、输入网络之前执行。代码实现上就是先统计所有训练图像块的均值和标准差,保存成 npy 文件,训练和推理时统一加载这两个值,避免训练和测试数据预处理不一致。

4. 多尺度交叉训练与多重损失:FlowS-Unet 的尺度鲁棒性来源

4.1 单一尺寸切片的边缘问题

全卷积网络能接受任意尺寸输入,但数据裁剪方式决定了模型能看到什么。用单一尺寸训练时,处于裁剪边缘的建筑物变化很难被正确预测,尤其是同一个建筑物被切到两张图上时,模型可能只在一张图上检测到变化,另一张图上什么都学不到。多尺度交叉训练的思路是把 224、256、288、320 四种尺寸的图块分别组成 batch,轮流输入网络训练。这样既不会让某一种尺度主导特征分布,又能在显存有限的情况下让模型见到完整的建筑物结构。

不同训练尺度和预测尺度的 F1 分数对比,可以很清楚地看到多尺度训练带来的收益:

训练尺度(像素)预测尺度(像素)F1(后处理前/后)
2242240.903 / 0.923
2562560.909 / 0.928
2882880.913 / 0.931
3203200.911 / 0.932
多尺度交叉2240.933 / 0.939
多尺度交叉2560.938 / 0.943
多尺度交叉2880.939 / 0.945
多尺度交叉3200.939 / 0.944
多尺度交叉四种尺度概率平均0.942 / 0.946

单尺度训练时,F1 分数最高只有 0.913(288 尺度),换成多尺度交叉训练后,即使只用 224 窗口预测也能到 0.933,说明模型真正学到了尺度无关的变化特征。

4.2 多重损失:每一层都要监督

FlowS-Unet 在解码器每次融合后都做独立预测,得到 pred0、pred1……predn 多个预测结果,总损失是各层损失的加权和。除最后一层外,其他层的输出特征图都比输入图像小,计算损失前需要把标签图像缩小到对应层的输出尺寸。权重 W_i 在实验中设为 1,即各层损失等权相加。这样做的好处是梯度能直接传递到浅层融合节点,浅层特征里的位置信息不会被深层语义特征淹没。

import torch.nn.functional as F def multi_scale_loss(preds, label, weights=None): # preds: 各层独立预测输出列表,元素形状为 (B, 2, H_i, W_i) # label: 原始分辨率标签,形状为 (B, 1, H, W),值为0或1 if weights is None: weights = [1.0] * len(preds) total_loss = 0.0 for pred, w in zip(preds, weights): # 把标签缩放到当前预测层的尺寸,用nearest保持离散类别 target = F.interpolate(label.float(), size=pred.shape[-2:], mode='nearest') loss = F.cross_entropy(pred, target.squeeze(1).long()) total_loss += w * loss return total_loss

缩放标签时必须用nearest模式,不能用bilinear。标签是离散类别,双线性插值会生成 0 到 1 之间的中间值,反向传播时给模型传递错误信号。单层损失使用交叉熵代价函数,表达式为 C = -(1/N) Σ [y ln a + (1-y) ln(1-a)],其中 y 是期望输出,a 是神经元实际输出。交叉熵在误差大的时候权重更新快,误差小的时候更新慢,比平方误差更适合分类任务。

4.3 优化器、学习率与收敛判断

优化器用 Adam,初始学习率 0.001。Adam 能自适应调整每个参数的学习率,省去手动调学习率衰减策略的麻烦。在 GTX 980Ti 6GB 显存环境下,多尺度 batch 要错开处理——先喂 224 的 batch,再喂 256 的 batch,依次轮转,而不是把四种尺度拼成一个超大 batch。训练到 160000 次迭代后,验证集准确率不再上升,损失值趋于平稳,模型收敛。如果训练数据分布差异特别大,可以把各层损失的权重改成按层递减,例如深层权重 1.0、浅层 0.8,让网络优先保证语义分类的准确性。

5. 基于 FlowS-Unet 的后处理与推理调优:膨胀腐蚀、孔洞填充与 F1 对比

5.1 先膨胀后腐蚀:参数依据是训练集标签

网络预测结果存在孤立点、空洞和边缘断裂。膨胀操作把与物体接触的背景点合并进来,让边界向外扩张,可以消除建筑边缘预测很弱的现象,平滑边界,同时不明显改变建筑物面积。操作内核用简单的 3×3 矩阵,连续膨胀 5 次。腐蚀则相反,消除边界点让边界向内收缩,用于去掉小且无意义的孤立区域。腐蚀的阈值面积定为 200,这个数字来自训练集标签统计——最小建筑物标记面积就是 200,小于这个面积的连通域被认为不是真实建筑。

import cv2 import numpy as np def post_process(pred, min_area=200, iterations=5): kernel = np.ones((3, 3), dtype=np.uint8) # 先膨胀5次,平滑弱边缘 dilated = cv2.dilate(pred, kernel, iterations=iterations) # 连通域分析,过滤面积小于min_area的区域 num_labels, labels, stats, _ = cv2.connectedComponentsWithStats(dilated, 8) for i in range(1, num_labels): if stats[i, cv2.CC_STAT_AREA] < min_area: dilated[labels == i] = 0 # 腐蚀回边界,消除膨胀带来的面积扩张 eroded = cv2.erode(dilated, kernel, iterations=iterations) return eroded

connectedComponentsWithStats返回每个连通域的包围盒和面积,遍历时从 1 开始,0 是背景。面积过滤放在膨胀和腐蚀之间,这样膨胀出的细小噪声连通域会在腐蚀前被清除,最终结果相比原始预测更干净。

5.2 孔洞填充:漫水填充的完整步骤

预测结果中极少大建筑物内部会出现封闭孔洞。孔洞填充用漫水填充算法实现,具体步骤是:先把原图向外扩展两个像素,填充值为背景色黑色;然后对待处理图像用 floodFill 函数从种子点开始填充连通域,种子点取 (0, 0),染成白色;此时背景全部变白,原图中的封闭孔洞因为被白色背景包围,保留为黑色;最后剪裁回原图大小,取反后与原图相加,得到填充后的结果。

def fill_holes(pred): h, w = pred.shape # 原图外扩两个像素,避免背景连通到图像边缘 canvas = np.zeros((h + 4, w + 4), dtype=np.uint8) canvas[2:2 + h, 2:2 + w] = pred # floodFill的mask需要比原图大2个像素 mask = np.zeros((h + 8, w + 8), dtype=np.uint8) cv2.floodFill(canvas, mask, (0, 0), 255) # 裁剪回原图大小后取反,与原图相加得到填充结果 filled = canvas[2:2 + h, 2:2 + w] return cv2.bitwise_or(pred, cv2.bitwise_not(filled))

floodFill第一个参数是输入单通道图像,第二个参数是填充算法的起始种子点,第三个参数是像素点被染色的新值。mask 的宽高必须比输入图像大 2,否则 OpenCV 会报错。

5.3 F1 指标与模型对比

F1 分数是精确率和召回率的调和平均数,被称为平衡 F 分数。它把标签中非 0 像素点作为正样本,0 像素点作为负样本,每个像素的分类结果都会影响最终得分。公式为 F1 = 2 × precision × recall / (precision + recall)。

三种模型在相同数据集上的对比结果如下:

序号方法F1(后处理前/后)推理时间(秒)
1FlowS-Unet0.933 / 0.94362
2FCN0.858 / 0.87350
3U-Net0.898 / 0.91359
4人工标注1.00018000

FlowS-Unet 后处理前已经比 U-Net 后处理后高 2 个百分点,后处理完成后 F1 达到 0.943。推理时间 62 秒和 U-Net 的 59 秒基本持平,说明多层独立预测并没有带来显著计算开销。预测时测试数据用滑动窗口处理,窗口大小为 256×256 像素,步长 150,步长小于窗口尺寸以保证建筑物边缘被充分覆盖。

6. 进阶技巧:多尺度平均预测、空洞填充双刃剑与可迁移落地

6.1 推理阶段的多尺度平均:免费再涨 0.4 个百分点

多尺度交叉训练后,推理时用不同窗口大小预测并平均概率,比固定单一窗口更稳。具体做法是:分别用 224、256、288、320 四种窗口对预测区域做滑动窗口推理,每个像素取四个窗口预测概率的平均值,再根据平均概率判断是变化还是不变。这个方法不需要重训模型,只在推理时多跑几次前向传播。从前面的对比表可以看到,多尺度平均的后处理前 F1 是 0.942,比单用 256 窗口的 0.938 高 0.4 个百分点。显存充足时这是性价比最高的提分手段。

推理时的窗口重叠率也要注意。步长 150、窗口 256,意味着相邻窗口有 106 像素的重叠,为什么要这么设计?因为建筑物和其周边上下文往往比窗口尺寸大得多,非重叠滑窗会把一栋建筑切碎,导致模型在窗口边缘反复预测不一致。重叠步长保证每个像素至少在多个窗口的中间位置出现一次,但代价是推理时间成倍增加,落地时可以根据时效要求调整步长。

6.2 空洞填充的双刃剑效应:真实结构不能乱填

测试结果里出现过这样的案例:一块区域真实情况是新造地基,中间带有一个真实孔洞,人工标签把它标成变化区域的一部分。空洞填充算法把这个洞填平了,反而让后处理结果偏离了真实标签。本质上,空洞填充处理的是“预测噪声导致的假洞”,但无法区分“真实存在的洞”。变化检测业务的目标是发现建、拆、改、扩,建筑内部有没有洞并不影响判断,所以这个误差对最终结果影响不大;但如果要做面积统计或生成工程量清单,这个会被“填错”的洞就会造成偏差。实际操作中我一般会统计训练集标签里真实孔洞的面积分布,如果孔洞面积占比超过连通域面积的 15%,就保留该连通域不填充。

6.3 可迁移方向与落地建议

FlowS-Unet 的结构不局限于遥感影像,任何输入是多时相或成对图像的像素级任务都能直接套用,只需调整输入通道数和输出类别数。医疗时序影像里病灶的消退和新增判断、纸币等印刷品的缺陷检测,本质都是“两幅图——一个变化图”的建模方式。

落地时我会把验证流程固定为四步:先量化输出和人工标注的 F1,再做连通域级别的面积误差统计分析,然后检查真实孔洞保留率,最后用不同区域数据做泛化抽检。这四个指标都达标,模型才能进入业务闭环。专门讲一下面积统计这块:通常要对每个变化连通域记录面积、位置和置信度,再把置信度低的区域标出来让审核人员抽查,这部分对国土监察场景尤其重要。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询