基于Siamese网络的SAR影像变化检测:从预处理到Web部署

基于Siamese网络的SAR影像变化检测:从预处理到Web部署 简介面向遥感与深度学习初学者的SAR图像变化检测实战项目基于Python与神经网络如CNN完成多时相SAR图像的地表变化识别可应用于地质灾害监测、城市扩展分析等场景。资源共195个文件压缩包仅3.07MB包含Python源码.py、PyTorch模型权重.pt、Vue前端组件、JavaScript逻辑、JSON配置、BMP/JPG样本图像及Markdown文档目录覆盖模型定义、数据预处理、变化检测逻辑与训练配置便于按模块研读和二次开发。包内附Ottawa地区实测图像样本与说明文档可直观理解SAR数据特点及检测流程。已有104人学习下载适合具备Python基础、希望将深度学习落地于遥感图像处理的研究者。整体包体虽小但串联了数据、算法、界面与文档是一份可运行的完整工程样例。1. 为什么把 SAR 变化检测从传统算法换成神经网络接手这个项目时压缩包里就是一对 Ottawa 地区的 Radarsat-1 时序影像一张洪水前、一张洪水后外加一个 README 备份和几个前端资源文件。用传统差值法跑出来的变化图噪声大得没法看河流两侧的细微地物变化和传感器噪声混在一起阈值怎么调都顾此失彼。换成神经网络方案之后最直观的变化是误检率明显下降——网络自动学习到的特征能区分“真实地表变化”和“噪声/配准残差”这正是这套系统的核心价值。整套方案适合两类人一是做遥感时序分析的工程师需要一个能快速复现的变化检测 baseline二是做毕设或课程设计的同学需要一个能讲清楚“数据预处理→模型训练→Web 展示”完整链路的参考实现。下面从数据处理开始完整走一遍这个系统的搭建思路。2. SAR 图像预处理的四个关键步骤2.1 多时相影像对的读取与检查压缩包里的ottawa_1.bmp和ottawa_2.bmp是同一地区不同时刻的 SAR 影像before.bmp和after.bmp是另一组样本。实际做变化检测之前先要确认两幅影像的空间范围和分辨率是否一致。SAR 影像和光学影像不同它的几何畸变来自侧视成像所以同一个地物在两幅图上的位置可能偏移几个像素这会直接影响后续逐像素比较的准确性。第一步是把影像读进来并检查 shapeimport cv2 import numpy as np img1 cv2.imread(ottawa_1.bmp, cv2.IMREAD_GRAYSCALE) img2 cv2.imread(ottawa_2.bmp, cv2.IMREAD_GRAYSCALE) print(img1 shape:, img1.shape, dtype:, img1.dtype) print(img2 shape:, img2.shape, dtype:, img2.dtype)IMREAD_GRAYSCALE直接读成单通道灰度图这是 SAR 影像的标准处理方式——SAR 本身是单极化或双极化的强度数据RGB 三个通道反而会引入无效信息。检查 shape 是否一致不一致就得先做配准或裁剪。Ottawa 这组数据是公开数据集里处理好的一般尺寸相同但实际项目里经常碰到分辨率不同、范围对不齐的情况这时候可以用cv2.matchTemplate或 ORB 特征点做配准这里不展开。2.2 相干斑噪声抑制为什么不用高斯滤波SAR 图像的固有问题是相干斑噪声speckle它源自雷达回波的相干叠加。高斯滤波会模糊边缘而边缘恰好是变化检测最关心的信息。常见做法是用 Lee 滤波或增强 Lee 滤波它会根据局部方差自适应调整滤波强度平坦区域多平滑边缘区域少平滑。from skimage.restoration import denoise_bilateral # 用双边滤波近似保持边缘的滤波效果 img1_denoised denoise_bilateral(img1, sigma_color25, sigma_spatial3) img2_denoised denoise_bilateral(img2, sigma_color25, sigma_spatial3) # 或者直接用均值滤波做简易版 # img1_denoised cv2.blur(img1, (5, 5))sigma_color控制灰度值差多少算作“不同区域”数值越大边缘保留得越模糊sigma_spatial控制空间邻域大小。SAR 影像一般sigma_color取 20~30sigma_spatial取 3 左右比较稳。如果用了 5x5 的均值滤波效果也不算错只是边缘处的变化检测结果会偏粗。工程上我一般建议先跑均值滤波看整体效果再换双边滤波对比。2.3 对数变换与差分图构造SAR 强度数据的动态范围很大直接做差值会被几个高亮目标主导。传统 SAR 变化检测里最经典的操作是对数比log-ratio# 避免除零 eps 1e-6 log_ratio np.abs(np.log((img2_denoised eps) / (img1_denoised eps))) # 归一化到 0~255 log_ratio_norm cv2.normalize(log_ratio, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)用np.log做的那个比值把乘法性的斑点噪声变成了加法性噪声正态分布假设更容易成立同时自然地压缩了动态范围。eps 1e-6是防止某些像素灰度为 0 导致除零错误这个细节在真实数据里几乎一定会碰到。cv2.normalize把结果拉伸到 0~255 方便后续处理和可视化但这步只是显示用训练神经网络时通常不做这个归一化直接用浮点数。2.4 分块策略让训练样本数量可控整幅图直接丢进神经网络不现实内存和算力都不够。标准做法是切成 patch每一对 patch 作为一条训练样本patch_size 32 stride 16 patches1 [] patches2 [] h, w img1.shape[:2] for y in range(0, h - patch_size, stride): for x in range(0, w - patch_size, stride): p1 img1_denoised[y:ypatch_size, x:xpatch_size] p2 img2_denoised[y:ypatch_size, x:xpatch_size] patches1.append(p1.reshape(-1)) patches2.append(p2.reshape(-1))patch_size32是经验值太小了感受野不够学不到区域级特征太大了边界效应明显样本量也减少。stride16是 50% 重叠既能扩大样本量又不会让相邻样本完全重复。提取出来的 patch 数量大概是((308-32)//161)^2 ≈ 19*19361对够一个简单网络训练用。如果数据不够可以增加重叠比例或对原图做翻转/旋转增强。3. 神经网络模型选型与训练实现3.1 为什么最终选 Siamese 结构的 CNN刚才把两幅图直接拼接输入网络也能训练但效果一般。原因是SAR 变化检测的本质是判断“同一位置在不同时刻是否发生了变化”这天然是一个双输入问题。Siamese 网络孪生网络的结构恰好契合这个场景——两个分支共享权重分别提取两幅影像的特征然后在高层做差异度量。共享权重意味着两幅图用同样的规则提取特征不会偏向某一时刻。也有更简单的方案构造差分图后放进普通 CNN 分类器。这种做法的缺点是差分图丢掉了原始纹理信息遇到辐射差异大的数据对时误检率高。Siamese 网络直接吃原始 patch让网络自己学“什么是变化”的度量方式鲁棒性明显好。值得注意的是这个思路和当前遥感领域主流的“时相编码 差异注意力”框架相比实现成本低但已经能覆盖大部分工程场景。3.2 网络结构定义import torch import torch.nn as nn import torch.nn.functional as F class SiameseCNN(nn.Module): def __init__(self): super().__init__() self.conv1 nn.Conv2d(1, 16, 3, padding1) self.bn1 nn.BatchNorm2d(16) self.conv2 nn.Conv2d(16, 32, 3, padding1) self.bn2 nn.BatchNorm2d(32) self.conv3 nn.Conv2d(32, 64, 3, padding1) self.bn3 nn.BatchNorm2d(64) self.pool nn.MaxPool2d(2) self.fc nn.Linear(64 * 4 * 4, 2) def forward_one(self, x): x F.relu(self.bn1(self.conv1(x))) x self.pool(x) x F.relu(self.bn2(self.conv2(x))) x self.pool(x) x F.relu(self.bn3(self.conv3(x))) x self.pool(x) return x.view(x.size(0), -1) def forward(self, x1, x2): f1 self.forward_one(x1) f2 self.forward_one(x2) diff torch.abs(f1 - f2) out self.fc(diff) return out三个卷积层逐步把通道数从 1 扩到 64每层后面跟 BatchNorm 和 ReLU过两次 MaxPool 下采样。forward_one分别提取两幅图的特征向量forward里用torch.abs(f1 - f2)计算特征差异再送进全连接层做二分类变化/未变化。用绝对差而不是拼接是因为绝对差直接表达了“特征之间的偏离程度”对变化检测更直观也有利于网络收敛。3.3 训练数据自动标注策略训练神经网络需要标签也就是“哪些 patch 是变化的”但这份数据里没有标注图。常见做法是用传统方法预先生成伪标签先用 2.3 节得到的对数比图做阈值分割或者用 K-Means 聚类分两类把聚类的输出当作弱标签。虽然不完美但有足够样本量时可以训练出一个比传统方法更鲁棒的模型。from sklearn.cluster import KMeans pixels log_ratio_norm.reshape(-1, 1).astype(np.float32) kmeans KMeans(n_clusters2, random_state0, n_init10).fit(pixels) labels kmeans.labels_.reshape(log_ratio_norm.shape) # 阈值分割的替代方案 # _, labels_simple cv2.threshold(log_ratio_norm, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU)K-Means 在这里的物理含义是把对数比图的像素强度自动分成“低差异”未变化和“高差异”变化两类。n_init10表示用 10 次随机初始化选最优结果避免陷入局部最优。Otsu 阈值法运行更快但遇到灰度分布重叠严重的数据对时分割结果不如 K-Means 稳定。3.4 训练配置与超参数说明训练时把 pair 和伪标签组装成 TensorDataset使用交叉熵损失import torch.optim as optim from torch.utils.data import DataLoader, TensorDataset # 假设 X1, X2, Y 已经是 numpy 数组 X1_t torch.FloatTensor(X1).unsqueeze(1) X2_t torch.FloatTensor(X2).unsqueeze(1) Y_t torch.LongTensor(Y) dataset TensorDataset(X1_t, X2_t, Y_t) loader DataLoader(dataset, batch_size64, shuffleTrue) model SiameseCNN() optimizer optim.Adam(model.parameters(), lr1e-4, weight_decay1e-5) criterion nn.CrossEntropyLoss() for epoch in range(80): for batch_x1, batch_x2, batch_y in loader: optimizer.zero_grad() output model(batch_x1, batch_x2) loss criterion(output, batch_y) loss.backward() optimizer.step() if (epoch 1) % 20 0: print(fEpoch {epoch1}, Loss: {loss.item():.4f})几个参数的取值逻辑lr1e-4是 CNN 微调常用的起点太大会导致伪标签中的噪声被放大太小则收敛慢。weight_decay1e-5是轻量 L2 正则防止网络在标签不准时过拟合到噪声上。batch 取 64 是因为 patch 是 32x32 的小图显存占用不大batch 再大梯度方向不稳定再小训练太慢。80 轮在 CPU 上跑大约 10~20 分钟加 BatchNorm 后不需要特别多轮次就能看到效果。4. 从概率图到变化图的后处理与 Web 集成4.1 全图推理与概率图生成训练好的模型要应用到整幅影像上仍然用滑动窗口切 patch逐 patch 预测再把预测概率拼回原图尺寸model.eval() prob_map np.zeros((h, w), dtypenp.float32) count_map np.zeros((h, w), dtypenp.float32) with torch.no_grad(): for y in range(0, h - patch_size, stride): for x in range(0, w - patch_size, stride): p1 img1_denoised[y:ypatch_size, x:xpatch_size] p2 img2_denoised[y:ypatch_size, x:xpatch_size] t1 torch.FloatTensor(p1).unsqueeze(0).unsqueeze(0) t2 torch.FloatTensor(p2).unsqueeze(0).unsqueeze(0) prob F.softmax(model(t1, t2), dim1)[0, 1].item() prob_map[y:ypatch_size, x:xpatch_size] prob count_map[y:ypatch_size, x:xpatch_size] 1 # 重叠区域取平均 prob_map prob_map / np.maximum(count_map, 1)这里用F.softmax取出“变化”类别的概率而不是直接用 argmax 取类别。概率图保留了置信度信息后续可以灵活调整阈值。重叠区域因为 stride patch_size用累加求平均的方式融合避免了拼接边界出现明显的条带效应。4.2 形态学后处理与精度评估指标原始概率图转成二值图后会有大量孤立的小噪声区域这是因为 SAR 斑点噪声即使经过滤波也残留部分高响应。常见做法是先用阈值 binarize再做形态学开运算最后用连通域面积筛选_, binary cv2.threshold(prob_map, 0.55, 1, cv2.THRESH_BINARY) binary binary.astype(np.uint8) # 开运算先腐蚀后膨胀去除细小噪声 kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)) opened cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel, iterations2) # 连通域面积过滤 num_labels, labels_im, stats, _ cv2.connectedComponentsWithStats(opened, 8) for i in range(1, num_labels): if stats[i, cv2.CC_STAT_AREA] 50: opened[labels_im i] 0阈值0.55比默认的 0.5 略高因为伪标签训练会让模型输出偏向“变化”类稍微提高阈值可以压掉部分假阳性。开运算的内核大小 3x3迭代 2 次对 Ottawa 数据来说够用如果影像分辨率更高内核要相应放大。connectedComponentsWithStats后面那个 50是面积阈值低于 50 像素的区域直接视为噪声。这个阈值需要根据影像尺寸调整——影像越大真实变化区域越大阈值也应相应增加。效果评估方面如果测试区域有 ground truth 标注图用这三项就够指标公式含义OA(TPTN)/(TPTNFPFN)总体精度Kappa(OA - pe)/(1 - pe)排除随机一致性的精度F12PR/(PR)兼顾误检和漏检在公开的 Ottawa 数据集上网络方案一般能到 94% 左右的 OA 和 0.80 以上的 Kappa。如果没有标注图就只能目视对比颜色差异和形状完整性。4.3 Web 端如何集成FastAPI 与 axios 的配合压缩包里的axios.cjs、index.d.cts说明前端展示层是通过 HTTP 调用后端接口的。整个 Web 端的逻辑是用户在页面上传两张 SAR 影像后端调已训练好的模型完成预处理、推理、后处理返回变化检测结果图和统计数据。后端用 FastAPI 暴露一个接口# app.py from fastapi import FastAPI, UploadFile, File from fastapi.responses import JSONResponse, FileResponse import numpy as np import cv2 app FastAPI() app.post(/detect) async def detect(file1: UploadFile File(...), file2: UploadFile File(...)): # 读入上传影像 data1 await file1.read() data2 await file2.read() img1_arr np.frombuffer(data1, np.uint8) img2_arr np.frombuffer(data2, np.uint8) img1 cv2.imdecode(img1_arr, cv2.IMREAD_GRAYSCALE) img2 cv2.imdecode(img2_arr, cv2.IMREAD_GRAYSCALE) # 此处调用 2~4 节中的预处理、模型推理、后处理流程 # result_map 为最终二值图 cv2.imwrite(result.png, result_map * 255) return JSONResponse(content{status: ok, result: /static/result.png})UploadFile是 FastAPI 处理文件上传的标准类型cv2.imdecode能把前端传过来的二进制流直接解码成图像数组省去了先落盘再读盘的 IO 开销。返回的 JSON 里给前端一个图片 URL前端拿到后直接更新img标签即可。前端用 axios 上传// detect.js const formData new FormData(); formData.append(file1, fileInput1.files[0]); formData.append(file2, fileInput2.files[0]); axios.post(http://127.0.0.1:8000/detect, formData, { headers: { Content-Type: multipart/form-data } }) .then(res { document.getElementById(result).src res.data.result; }) .catch(err console.error(err));FormData传文件时前端不需要手写multipart/form-data边界浏览器会自动处理。Content-Type理论上也可以不写但显式声明更保险。axios在压缩包里出现.cjs结尾的文件说明前端可能是 Electron 或 CommonJS 模块环境注意require(axios)和import axios from axios的差异即可。5. 迁移学习与模型检查的三个进阶技巧5.1 换一块数据时别从头训Ottawa 数据只有几十 MB训练一个模型很轻松但换成 Sentinel-1 的大幅影像时从头训练的时间和标注成本都会上升。常见做法是把已经训好的权重当作预训练模型在新数据上做微调# 加载已有权重 model.load_state_dict(torch.load(sar_change_det.pth)) # 只微调最后一层前几层冻结 for name, param in model.named_parameters(): if fc not in name: param.requires_grad False optimizer optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lr5e-5)冻结策略的逻辑是浅层卷积学到的是边缘、纹理这类通用特征不同数据集之间可以共享只有最后的全连接层需要适配新数据的“差异度量方式”。学习率降到5e-5因为只有少量参数更新步长太大会震荡。5.2 模型输出检查可视化特征而不是只看准确率训练完模型后把forward_one输出的特征图可视化能直观判断网络有没有学到有意义的东西def visualize_feature(model, img_patch): model.eval() x torch.FloatTensor(img_patch).unsqueeze(0).unsqueeze(0) with torch.no_grad(): feat model.conv1(x) feat_map feat[0].detach().numpy() return feat_map # feat_map shape: (16, 32, 32)挑几个通道画出来即可如果特征图里有清晰的边缘轮廓说明网络学到了结构信息如果全是噪声或全零可能是 BatchNorm 没有收敛或者数据归一化有问题。这一步排查效率比盯着 loss 曲线高得多。5.3 单类别的误检分析技巧变化检测最容易出现的问题是河流区域的大面积误检——水位变化导致后向散射系数变化算法把它识别成了地表变化。解决思路是给输出加一层地理先验掩码# 假设 water_mask 是河流区域掩码手工标注或从原图阈值得到 # 把河流区域的变化结果直接置为 0 result_final opened.copy() result_final[water_mask 1] 0更通用的做法是记录误差集中出现在哪些类型的地物上。分析时把检测结果和原始影像叠成半透明伪彩色图用 OpenCV 的addWeighted叠加快速定位误检区域的空间分布模式。这类分析做多了会对模型的盲区形成直觉判断SAR 变化检测的漏检往往集中在低后向散射区域如平静水面误检则集中在高异质区域如城区边缘。整个系统的链路到这里就闭环了数据预处理 → 神经网络训练 → 后处理生成变化图 → Web 接口展示。如果要把这套方案用到更大的数据上优先考虑把 Siamese 主干换成 ResNet 预训练版本并在模型输出后加一层 CRF 做空间平滑效果提升会非常明显。本文还有配套的精品资源点击获取