简介:变化检测是遥感影像分析中的核心任务,常用于城市规划、灾害监测与资源管理。这份基于Transformer实现的遥感影像变化检测算法项目,将深度学习强大的特征提取与全局依赖建模能力引入传统变化检测流程,面向需要高精度、高效率检测方案的研究人员与工程师。资源包共61个文件,压缩后约57.55MB,其中包含20个Python源码模块、2个可执行的Shell脚本、1个预训练权重文件、1份Markdown说明文档与1个文本说明,以及36张PNG格式的可视化结果图,目录结构涵盖datasets、models、networks、checkpoints等,便于按模块学习与二次开发。目前已有225人学习下载,适合作为遥感方向课程设计、毕业设计或工程落地的参考。借助项目源码,读者可逐步复现训练流程,理解Transformer在变化检测中的具体实现细节,并根据自身数据调整网络结构与超参数;同时,脚本与评估工具也提供了清晰的实验结果验证思路,帮助快速定位问题并优化模型性能。
1. 基于Transformer的遥感影像变化检测:为什么传统分割网络在双时相任务上吃亏
遥感影像变化检测这两年很多人从CNN切到Transformer,原因很直接:把两期影像输入分割网络,哪怕像素值差异极大,模型也要判断场景是否真的变了。比如卫星同一地点拍摄的两张图,云影、光照、植物物候稍微一变,像素差值能拉满,但语义上什么都没变;纯卷积的感受野有限,很容易把这种伪变化当成真变化。基于Transformer的变化检测算法用自注意力做全局建模,在“变化检测-基于Transformer实现的遥感影像变化检测算法”这类项目里,核心就是先把两期特征对齐,再做差分和逐像素分类。这个方向适合做遥感算法或CV落地的工程师练手,单卡就能跑通,也是理解Transformer架构在图像分割上怎么发力的一个典型入口。
2. 双时相变化检测的方案选型:从拼接输入到Swin Transformer骨干
2.1 早期融合与双分支(Siamese)到底差在哪
常见做法是把t1和t2两张影像直接在通道维拼起来,变成6通道输入,丢给UNet或DeepLabV3。这种早期融合实现最简单,代码几十行,但它让网络自己隐式学习“哪里对应哪里”。对于同一个位置,两期影像可能因为传感器、季节、云影导致值差很多,CNN在小感受野下只能学到一个局部颜色差异的模板,泛化能力很差。而且6通道输入在预训练权重上没法直接加载,需要从头训,消耗大。
所以基于Transformer的变化检测项目普遍改用双分支(Siamese)结构:两个共享权重的编码器分别提取t1和t2的特征,再在深层做差分或拼接。这样做的原因是权重共享保证了两次提取落在同一个特征空间里,后面对比才是公平的。你拿Swin Transformer当骨干时,Siamese结构并不会让参数量翻倍,因为两个分支共享同一套权重,只是输入不同。这在训练时的显存开销上会稍微高一点,因为同时要过t1和t2两遍前向,不过效果通常值得。
常见做法中还有第二种进阶级方案:在共享编码器之后,把两个时相的特征序列喂给一个时间注意力层,让模型自己学习“变化的位置”。这个思路和自然语言处理里的Transformer encoder就很像了,相当于把双时刻变化检测看成双序列的对应关系学习。代价是训练收敛更慢,对数据量要求也更高,中小型项目里容易过拟合。所以我的选择是:先用共享权重Swin提取特征,再用差分和拼接的组合头做分类,这样既保留Transformer的全局感受野,又能快速收敛。
2.2 为什么注意力机制能把“伪变化”压下去
纯CNN的分割模型靠的是局部感受野的堆叠,要覆盖大范围变化必须把网络做深,或者靠空洞卷积。遥感影像里的建筑、农田、林地往往跨度几十到上百像素,一次卷积只能看到一小块,远处的关系只能层层传递,位置信息会被稀释。
Transformer的自注意力机制把特征图展成一串token,每个位置的token可以和全图任意位置直接交互。放在双时相变化检测里,意味着模型可以在一期影像里看到“这是楼房的屋顶”,在二期影像里把同样的屋顶和邻居关系拉进感受野,就算光照变了、屋顶颜色变了,它依然能判断“这不是变化”。这就是语义变化检测比像素级差分强的地方。热词里常提到的Vision Transformer和Swin Transformer都属于这个家族,区别在于ViT是第一代全局建模,Swin用窗口注意力加层次化设计,更适合图像分割和分割类任务。
选择Swin Transformer作为骨干还有一个工程原因:它有和UNet类似的层级结构,可以方便地接UNet式的跳跃连接,也可以直接换成现成的分割解码器。项目里如果要跑通基于Transformer实现的遥感影像变化检测算法,用Swin做编码器是目前最常用的落地路径。比直接上纯ViT更省显存,也比自己写时间注意力更稳妥。
这里要区分一下ViT和Swin:ViT把整张图切成16x16的patch,全局注意力计算复杂度是二次增长,遥感影像通常512甚至2048大小,直接上ViT会非常吃力。Swin把注意力限制在窗口内,窗口之间通过shift交叉,复杂度是线性的,这也就是为什么很多基于Transformer的变化检测项目以Swin为骨干。另一个用Swin的原因是它可以加载ImageNet预训练权重。图像分类预训练虽然域不一样,但前几层的边缘和纹理特征对遥感影像非常有用,从零训练的收敛速度明显更慢。
2.3 项目代码结构:四大模块,缺一不可
从开源项目源码头开始组织代码,我一般会这样拆目录,方便先跑通再改:
change_detection_project/ ├── configs/ │ └── train.yaml # 超参数配置 ├── datasets/ │ ├── __init__.py │ ├── change_dataset.py # 双时相影像对读取 │ └── transforms.py # 数据增强 ├── models/ │ ├── encoder.py # Swin Transformer编码器 │ ├── change_head.py # 变化检测头 │ └── criterion.py # 损失函数 ├── train.py # 训练入口 ├── infer.py # 推理入口 └── utils/ └── metrics.py # F1/IoU计算这个结构把数据、模型、训练、推理分开,最重要的是configs/。训练参数不要散落在代码里改,因为遥感变化检测的调参很频繁,统一放在yaml里能省很多事。很多优质项目实战源码包也是这种结构,拿到后应该先看configs目录,再看datasets,最后才看模型定义。
拿到任何一个基于Transformer的变化检测项目源码,我建议先确认三件事:第一,模型加载的预训练权重路径是否齐全,很多项目把权重省略了,要用timm内置的来替代;第二,配置文件里的数据集根目录是不是写死成了作者机器上的路径;第三,验证脚本里的评估指标是否和论文一致。这三处是源码类项目跑通最常翻车的三个点。不要一上来就对着train.py运行,先把cuDNN、PyTorch、timm的版本对齐。
3. 核心实现:用PyTorch搭建Transformer变化检测模型与损失函数
3.1 数据读取:双时相图像对与变化标签的加载
变化检测的数据集标签是单通道的二值图,0表示未变化,255表示变化。训练时往往把两个时相和标签分别读取,做相同的随机裁剪和翻转。这里有一个常见坑:t1和t2必须用同一组增强参数,否则等于人为引入了变化。所以我的实现是把两张图拼起来后做变换,再转成模型输入。
import cv2 import numpy as np import torch from torch.utils.data import Dataset class ChangeDetectionDataset(Dataset): def __init__(self, root, split='train', patch_size=256): self.paths = self._load_pairs(root, split) self.patch_size = patch_size def _load_pairs(self, root, split): pairs = [] with open(f'{root}/{split}.txt') as f: for line in f: t1, t2, label = line.strip().split() pairs.append((t1, t2, label)) return pairs def _random_crop_pair(self, t1, t2, label): h, w = t1.shape[:2] top = np.random.randint(0, h - self.patch_size + 1) left = np.random.randint(0, w - self.patch_size + 1) t1 = t1[top:top+self.patch_size, left:left+self.patch_size] t2 = t2[top:top+self.patch_size, left:left+self.patch_size] label = label[top:top+self.patch_size, left:left+self.patch_size] return t1, t2, label def __getitem__(self, idx): t1_path, t2_path, label_path = self.paths[idx] t1 = cv2.imread(t1_path, cv2.IMREAD_COLOR) t2 = cv2.imread(t2_path, cv2.IMREAD_COLOR) label = cv2.imread(label_path, cv2.IMREAD_GRAYSCALE) t1 = cv2.cvtColor(t1, cv2.COLOR_BGR2RGB) t2 = cv2.cvtColor(t2, cv2.COLOR_BGR2RGB) t1, t2, label = self._random_crop_pair(t1, t2, label) t1 = torch.from_numpy(t1.transpose(2,0,1)).float() / 255.0 t2 = torch.from_numpy(t2.transpose(2,0,1)).float() / 255.0 label = torch.from_numpy(label.copy()).long() / 255 return t1, t2, label这段代码有几个参数要说明。patch_size是训练切块大小,256是一个非常稳妥的起点,兼顾显存和上下文信息;如果你显存只有8G,降到224也能跑,但后面会讲降低patch大小会牺牲小目标检测。读取txt列表比遍历文件夹更可控,训练集、验证集划分靠文件列表而不是目录扫描,避免混入没配对的影像。label除以255是为了把0和255变成0和1,CrossEntropyLoss要求目标从0开始。
3.2 编码器:用Swin Transformer提取分层特征
这里用timm加载Swin的backbone,去掉分类头,保留四个stage的输出。由于timm版本不同,输出格式略有差异,建议用forward_features拿到的是最后stage的特征;如果要拿多尺度,需要把每个stage的中间输出存起来。下面是比较稳的多尺度写法。
import timm import torch class SwinEncoder(torch.nn.Module): def __init__(self, model_name='swin_small_patch4_window7_224', pretrained=True): super().__init__() self.backbone = timm.create_model(model_name, pretrained=pretrained) # 去掉分类头和池化,但保留各stage输出 self.stages = torch.nn.ModuleList() self.patch_embed = self.backbone.patch_embed self.layers = self.backbone.layers self.norm = self.backbone.norm self.avgpool = None # 不再使用 def forward(self, x): outs = [] x = self.patch_embed(x) for layer in self.layers: x = layer(x) outs.append(x) return outs # 四个尺度的特征这段代码需要说明。Swin的patch_embed做的是像素到token的转换,每个stage输出不同空间分辨率的token序列。变化检测需要的多尺度特征来自outs里的第2、3、4层,第1层分辨率太高,直接拿来计算差分计算量大,一般从第2层开始用。pretrained权重来自ImageNet图像分类,对遥感影像来说只能提供纹理和边缘的初值,不要指望它直接认识屋顶和道路,这是后面训练必须调足epoch的原因之一。
如果把两个时相分别输入这个编码器,要保证是同一个module实例调用两次,这样权重才是共享的。在PyTorch里直接在forward里调用,不要写成两个独立的SwinEncoder对象。
如果你不用timm,也可以从Swin官方仓库移植权重,但没必要。timm里的Swin实现被很多人验证过,稳定性好,而且对不同版本PyTorch的兼容性更好。你要注意,Swin预训练权重的输入归一化使用的是ImageNet的mean和std,遥感影像虽然通道也是RGB,但值域分布不完全一样,很多项目直接把mean和std删掉,或者改成全0.5。我个人建议保留ImageNet的归一化,因为预训练权重是在这个分布上学的,归一化改动太大会让初始loss飘得很高,后面再训回来要花更多时间。
3.3 变化判别头:差分融合与逐像素分类
Swin输出的token序列要恢复成空间特征图。一种常见做法是用PixelShuffle或ConvTranspose逐层上采样。我的选择是:把两个时相的最后一个stage特征做绝对差分,再把多尺度差分特征加到一起,最后用一个简单的分割头。差分比拼接更直接,而且能强迫网络关注变化区域。
class ChangeHead(torch.nn.Module): def __init__(self, in_chans=768, num_classes=2): super().__init__() self.fuse = torch.nn.Sequential( torch.nn.Conv2d(in_chans, 256, kernel_size=3, padding=1), torch.nn.BatchNorm2d(256), torch.nn.ReLU(inplace=True), torch.nn.Conv2d(256, num_classes, kernel_size=1) ) def forward(self, feat_t1, feat_t2): diff = torch.abs(feat_t1 - feat_t2) out = self.fuse(diff) return out注意这里的in_chans要和你选用的Swin最后一个stage输出通道一致。如果用swin_small,最后一层是768;swin_base是1024。改模型时最容易错的就是这个数字。另外,只差最后一层可能丢失空间细节,如果显存允许,可以把第3层特征也用同样的方式融合后加进来,相当于一个简易的特征金字塔。多尺度差分还有一个常见的变体:把两个时相第三阶段特征和第四阶段特征分别差分后,再把结果上采样到同一个分辨率相加。这个比单独用最后一层能提升2-3个IoU,代价是多一层显存开销。如果模型过拟合,可以只用最后一层,先把流程跑通。
3.4 损失函数:为什么单用交叉熵会漏小目标
变化检测的标签天然是极度不平衡的:一幅512的图里,变化区域往往只有几千像素,只占几个百分点。直接用CrossEntropyLoss,模型很快就学会把所有像素都预测为不变,F1看起来还是能有零点几,因为负样本占绝对多数。
我一般用Dice Loss和Focal Loss的组合,Focal Loss解决难例数量少的问题,Dice Loss解决前景背景不平衡。参考代码:
import torch.nn.functional as F def focal_loss(logits, target, alpha=0.25, gamma=2.0): ce = F.cross_entropy(logits, target, reduction='none') pt = torch.exp(-ce) focal = alpha * (1 - pt) ** gamma * ce return focal.mean() def dice_loss(logits, target): probs = F.softmax(logits, dim=1)[:, 1] smooth = 1.0 intersection = (probs * target).sum() return 1 - (2.0 * intersection + smooth) / (probs.sum() + target.sum() + smooth)这两个函数的参数各有讲究。alpha和gamma是Focal Loss的核心,alpha用来给正样本加权重,gamma用来压低易分样本的loss贡献。在变化检测里,alpha取0.25通常比0.5好,因为变化类占比实在太小。dice_loss用的smooth是平滑项,作用只是防除零,不要调太大,否则损失会被稀释。
把这几个损失加起来训练时,我习惯让dice_loss权重为1.0,focal_loss权重为0.5。你需要做的不是照抄,而是看一眼训练日志里两个损失的数值和量级,再用加权系数把它们拉到差不多的量级,避免一个损失主导。
训练时我还会用poly学习率策略,即lr乘以(1 - iter/total_iters)^0.9。Swin这类模型在后期用固定较小的学习率也能收敛,但poly衰减在变化检测里通常比阶梯下降更稳。你也可以用CosineAnnealingLR,只是需要把T_max设成完整的epoch数,而不仅仅是第一个cycle。
4. 从数据到推理:遥感变化检测项目的训练配置与切片拼接
4.1 数据组织格式:A/B两期影像和label目录
双时相变化检测项目的一个关键是文件配对。常见数据集像LEVIR-CD、DSIFN,目录结构一般是A文件夹放t1,B文件夹放t2,label文件夹放变化标签,文件名一致或者有对应关系。如果你自己制作数据集,建议先统一成同一张表。
data/ ├── train/ │ ├── A/ │ │ ├── 0001.png │ │ └── 0002.png │ ├── B/ │ │ ├── 0001.png │ │ └── 0002.png │ └── label/ │ ├── 0001.png │ └── 0002.png在读取时,需要检查A、B、label三者尺寸是否一致。遥感影像有时候因为投影差异,同一区域的t1和t2尺寸差几个像素,这会导致后面训练时出现空白边。解决方法是预处理时把两期影像用OpenCV的resize对齐,而不是在训练时去挤。label里的值如果出现0和255以外的值,要么是标注工具的抗锯齿边缘,要么是压缩噪声,最好做一次阈值二值化:label[label>127]=255, label[label<=127]=0。
验证集划分也要注意。验证集不要只用同一个小区域的影像切块,最好按时间或空间分开。遥感变化检测里,同一景影像内的切片有很强的相关性,如果训练集和验证集来自同一幅大影像,指标会虚高。常见的做法是按地理位置划分,比如训练西北区域,验证东北区域。如果数据源不允许,至少要按文件而不是按切块划分,避免同一图像对出现在两边。
4.2 训练脚本:batch size、学习率和切片大小怎么设
下面是一个简单的训练主循环,用PyTorch单卡。为了照顾显存,输入直接取dataset返回的t1、t2和label。
import torch def train_one_epoch(model, dataloader, optimizer, device): model.train() total_loss = 0.0 for t1, t2, label in dataloader: t1 = t1.to(device) t2 = t2.to(device) label = label.to(device) feats1 = model.encoder(t1) feats2 = model.encoder(t2) logits = model.head(feats1[-1], feats2[-1]) loss = dice_loss(logits, label.float()) + 0.5 * focal_loss(logits, label) optimizer.zero_grad() loss.backward() optimizer.step() total_loss += loss.item() return total_loss / len(dataloader)注意这里在训练中直接调用两次encoder,权重共享是Python层面的同一个对象,所以没问题。如果你的数据增强里没有把t1和t2用同一随机种子裁剪,loss会一直很高且震荡,这是最常见的一个坑。
超参数我一般这样设:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| patch_size | 256 | 保留上下文,显存8G可跑 |
| batch_size | 8 | Swin Small + patch_size 256 约占用7G多 |
| learning_rate | 1e-4 | 从头微调Swin不要用2e-4以上 |
| epochs | 60-100 | 遥感数据小,需要更多epoch |
| weight_decay | 1e-4 | 防止过拟合 |
这个表里的数值不是绝对的。如果加载的是ImageNet预训练权重,初始学习率用1e-4,到40个epoch后降到1e-5,效果会稳定很多。batch_size和patch_size的乘积就是一次forward的像素总量,把它控制在50万左右,8G显存基本不会爆。
关于归一化:推理时一定要使用和训练时完全相同的像素预处理。很多项目在训练时用mean/std归一化,但推理时忘了归一化,导致logits分布偏移,阈值也不再适用。我的习惯是把归一化逻辑写成一个函数,训练和推理都调用同一个python模块来避免这个问题。
4.3 推理:切片预测、拼接和阈值选择
遥感影像通常比训练切片大得多,推理时要用滑窗切片,把每个patch的预测概率拼接回原图。这里重叠策略很关键,我通常用重叠率为25%的滑窗,每个位置被预测多次,最后取平均。这样能明显减少边缘伪影。
def sliding_window_infer(model, big_img_t1, big_img_t2, patch_size=256, overlap=64): model.eval() h, w = big_img_t1.shape[:2] stride = patch_size - overlap prob_map = np.zeros((h, w), dtype=np.float32) count_map = np.zeros((h, w), dtype=np.float32) with torch.no_grad(): for top in range(0, h - patch_size + 1, stride): for left in range(0, w - patch_size + 1, stride): p1 = big_img_t1[top:top+patch_size, left:left+patch_size] p2 = big_img_t2[top:top+patch_size, left:left+patch_size] t1 = torch.from_numpy(p1.transpose(2,0,1)).unsqueeze(0).float()/255.0 t2 = torch.from_numpy(p2.transpose(2,0,1)).unsqueeze(0).float()/255.0 logits = model(t1, t2) prob = torch.softmax(logits, dim=1)[0,1].cpu().numpy() prob_map[top:top+patch_size, left:left+patch_size] += prob count_map[top:top+patch_size, left:left+patch_size] += 1 return prob_map / np.maximum(count_map, 1)这段推理代码里,overlap是128还是64,差别很大。overlap越大,推理耗时越长,但边缘修复效果越好。我一般先用64,如果出现明显的格子状接缝就加到128。最后得到的prob_map是0到1的概率图,还需要阈值二值化。阈值选0.5是比较保守的,但遥感变化检测里如果目标是提取变化图斑,0.3到0.5之间可以接受,具体要结合验证集F1来调,不要直接拍脑袋。二值化后用连通域分析去掉面积小于阈值的碎块,通常几百平方米的噪声在遥感图上就只是几个像素,这个后处理能帮大忙。
5. 避坑手册:遥感影像变化检测的5个常见问题与排查记录
5.1 模型把季节变化当成建筑变化:伪变化来自哪里
现象:训练集F1有0.85,但在目标区域预测时,农田和树林的变化被大量标成建筑。
原因:这类项目最容易碰到的就是季节差异。两期影像如果来自不同月份,植被颜色和阴影完全不同,单靠像素差异很难区分真实变化和物候变化。模型如果只见过少数样本,根本学不到“植物颜色改变不算变化”这种语义规则。还有一个隐藏原因是数据配准不精确,同一个屋顶的边缘错位了两个像素,在差分图上就成了一个很亮的轮廓,模型会把这个当成新增变化。
解决:第一优先加大数据增强,随机调整亮度、对比度、饱和度,模拟不同季节的色调差异。第二是在训练样本里刻意保留一些“时节变化但无标签变化”的负样本区域,当作背景。第三是检查配准,如果两期影像有系统性偏移,先做配准或对齐。不要在模型层面硬扛数据问题。
5.2 验证集F1高,实际预测图斑碎了
现象:验证集IoU 0.7以上,但推理出来的变化区域是一堆细碎的斑点,没有完整图斑,很难矢量化。
原因:这往往是逐像素分类且缺少后处理导致的。可分离的小变化本来就是模型的噪声,验证集里也许和标签对上了,但推广到新场景时,孤立点就成了误检主体。另一个原因是训练时dice_loss权重设置太大,模型倾向于输出高概率的孤立区域,因为这些区域也能拉低dice loss。
解决:训练时把focal_loss的gamma调低到1.5,减少对难例的过度强调。推理后做一步形态学后处理:先做开运算去掉孤立点,再闭运算填充空洞。关键是先把验证指标和视觉结果一起看,不要只盯F1。如果碎斑还很多,可以考虑在变化检测头后面加一层更小的卷积核做空间平滑。
5.3 显存OOM:patch size和batch的妥协
现象:Swin Small + patch_size 256 + batch_size 8,在8G卡上直接报CUDA out of memory。
原因:Swin Transformer的窗口注意力虽然省显存,但两期影像要同时过编码器,等于一个batch占了双份特征空间。再加上差分头里的高分辨率feature map,内存峰值出现在backward阶段。
解决:先降batch_size到4,如果还不行,把patch_size降到224。要说明一点,优先保patch_size而不是batch_size,因为窗口注意力在patch大小变化时,感受野变化明显,小patch会让模型看不到变化区域边缘。你也可以在训练时用torch.cuda.amp做半精度混合精度训练,这个收益通常比降分辨率更大。降batch_size只是压缩了并行度,不影响感受野,所以应该放在第一步。
5.4 变化区域在边界上出现“锯齿”和错位
现象:预测mask和真实标签比,边界总有一圈误差,向外或向内扩几个像素,视觉上很脏。
原因:变化检测的标签是按多边形标注的,标注本身就有1-2个像素误差;再加上模型做的是逐像素分类,没有显式的边界约束。Swin的窗口注意力会把窗口边缘信息模糊掉,双重叠加导致边界不准。
解决:在损失函数里加上一个边界约束项——先用Laplacian算子从变化标签中提取边界,再计算预测概率在边界位置的梯度幅度,让模型在边界附近更收敛。简单的做法是在后处理时用形态学腐蚀/膨胀调整,但根治需要在训练中用边界感知损失。如果你只是要快速出结果,可以在预测概率图上用高斯模糊再做二值化,边界也会柔和一些。
5.5 小目标变化漏检:模型只关注大面积变化
现象:道路拓宽、新建独栋房屋这类小变化常常漏掉,大别墅区变化检测得很准。
原因:双时相差分后,小目标的特征在深层stage几乎被压缩没了。Swin最后一个stage的分辨率是输入的1/32,一个10x10像素的小房子在特征图上只剩1个token,信息早就被平均掉了。这是Transformer类模型普遍的问题,不是bug。
解决:把变化检测头接到Swin的第2、3个stage上,用多尺度特征做差分,而不是只取最后一层。另外,在训练时对变化区域做随机裁剪增强,让模型有机会看到更多小变化。如果数据集里大面积变化占多数,可以考虑对训练样本按变化面积进行重采样,保证小目标样本不被淹没。
6. 进阶技巧:用测试时增强和CRF把二值图变成可用图斑
6.1 测试时增强:让预测不再一锤定音
测试时增强(TTA)就是在推理时把输入做水平翻转、垂直翻转、旋转90度,然后平均多个预测概率。变化检测尤其适合TTA,因为模型在旋转后对变化区域的响应会不同。代码很简单,但要注意对t1和t2做相同的几何变换,否则等于人为造变化。
def tta_predict(model, t1, t2): probs = [] for flip in [0, 1, 2]: # 无、水平、垂直 p1 = flip_img(t1, flip) p2 = flip_img(t2, flip) logits = model(p1, p2) prob = torch.softmax(logits, dim=1)[0,1] probs.append(flip_img_reverse(prob, flip)) return torch.stack(probs).mean(dim=0)6.2 CRF后处理:修掉孤立点,保住边界
概率图直接阈值化后,往往有孤立噪点。此时用全连接CRF把像素颜色和空间距离加进来平滑,效果比单纯形态学好。遥感影像太大时,对全图做CRF内存会爆,一般按切片做。CRF参数最关键是两个:w1控制空间一致性,alpha控制颜色差异的敏感度。我常用的起始值w1=3,alpha=10,通常几次尝试就能找到适合当前影像的设置。
6.3 切片重叠推理策略
前面第4章提过重叠滑窗,这里补充一个经验:重叠比例不要用固定像素,而是用patch_size的25%。切换推理时,先小图调试,确认TTA和CRF的耗时,再上大图,否则一张几千乘几千的影像会被后处理拖死。另外,遥感变化检测项目里很多坑最后都落在数据而不是模型上。我自己的教训是:先花半天检查标签和影像对齐,再调参数;Transformer不是银弹,它只是给了你更大的感受野,省下来的时间还是要花在数据清洗上。希望这些步骤能帮你在自己的项目里少踩几个坑。
本文还有配套的精品资源,点击获取