
简介这份资源面向计算机、人工智能、电子信息等专业的学生与开发者提供一套可运行的无监督SAR图像配准Python实现适合课程设计、毕业设计、大作业或初期项目立项演示也便于零基础读者上手实战。压缩包共75个文件以37个py源码为核心辅以14个pyc编译文件、5个md说明文档、6张jpg与4张png结果图以及1个h5模型权重整体约3.58MB结构紧凑、便于查阅。项目围绕无监督配准展开包含网络结构定义、损失函数、数据生成与训练测试脚本并配有基准与配准后对比图、光流图、各类损失曲线图可直观观察配准效果与训练过程。已有143人学习读者可借此掌握空间变换、损失设计、数据增强与模型评估等关键环节快速搭建实验环境并迁移到自身课题。1. 无监督SAR图像配准从零跑通一套Python源码要跨过几道坎SAR图像配准这件事做遥感、做变化检测、做干涉测量的同行都不陌生。它要解决的核心问题是同一区域在不同时间、不同传感器、不同视角下拍到的两幅SAR影像像素坐标对不上后续做差分、做相干、做叠加全是错的。传统做法依赖人工选控制点或者基于光学的特征匹配但SAR图像有相干斑噪声、有几何畸变、有辐射差异拿光学那一套直接搬过来匹配点能有一半是错的。无监督SAR图像配准的思路就是绕开人工标注和标签数据让算法自己从两幅图里学出形变场或者匹配关系。这套Python源码加项目说明的组合适合两类人一是刚接触SAR处理、想找一个能跑通的起点的新手二是做过光学配准、想看看SAR场景下无监督方案怎么落地、参数怎么调的熟手。下面按“先搞懂它在干什么、再把环境搭起来、再跑通推理、最后知道哪里会翻车”的顺序展开每一步都落到可执行的命令和代码上。2. 无监督SAR图像配准到底在配什么原理与方案选型2.1 SAR配准和光学配准的本质差异光学图像配准的底层假设是同一地物在不同图像里灰度相似、纹理相似所以用SIFT、ORB、互信息这类方法找对应点再拟合变换矩阵。SAR图像不满足这个假设。相干斑噪声让同一地物在不同时相的灰度值可能差出几倍几何畸变让地物形状发生非线性变化叠掩和阴影让某些区域根本没有有效信息。所以SAR配准通常不走“找同名点拟合全局变换”的路线而是走“稠密形变场估计”的路线——直接算每个像素的位移量输出一个和原图同尺寸的位移场。无监督的含义在这里要拆开看。一种理解是不需要人工标注的控制点算法自己优化另一种理解是不需要成对的标签形变场做监督。这套源码走的是后者用图像相似度作为损失函数让网络自己学出把浮动图变形到参考图的位移场。常见做法是用一个U-Net结构的网络预测位移场然后用空间变换网络STN对浮动图做重采样最后用归一化互相关NCC或者互信息MI算相似度损失。整个训练过程不需要任何标注好的位移场。2.2 为什么选无监督而不是有监督有监督SAR配准需要成对的图像和对应的真实位移场。真实位移场怎么来要么用高精度GPS/INS数据反算要么用人工标注的控制点插值。前者成本极高后者在SAR图像上标注本身就很难——相干斑让地物边界模糊人工选点的一致性很差。无监督方案把这个问题绕开了只要有成对的SAR图像就能构造训练样本。代价是训练不稳定、容易收敛到平凡解比如位移场全零需要仔细设计损失函数和正则项。我一般会建议如果手头有几十对以上的同区域SAR图像且对配准精度要求不是亚像素级无监督方案值得试。如果只有两三对图或者要求像素级精确配准还是老老实实走基于互信息的传统方法或者用少量控制点做半监督。2.3 源码里常见的网络结构和损失函数这套源码大概率包含以下几个模块数据加载部分读SAR图像、做归一化、构造图像对、网络部分位移场预测网络通常是U-Net或类似结构、变换部分用位移场对浮动图做warp、损失部分NCC或MI加正则项、训练和推理脚本。下面给一个典型的位移场预测网络结构你可以对照源码看是不是这个路子。import torch import torch.nn as nn import torch.nn.functional as F class DisplacementNet(nn.Module): def __init__(self, in_ch2, base_ch32): super().__init__() # 编码器逐步下采样提取多尺度特征 self.enc1 self._block(in_ch, base_ch) self.enc2 self._block(base_ch, base_ch*2) self.enc3 self._block(base_ch*2, base_ch*4) # 解码器上采样并融合浅层特征 self.dec2 self._block(base_ch*4 base_ch*2, base_ch*2) self.dec1 self._block(base_ch*2 base_ch, base_ch) # 输出层2通道分别对应x和y方向位移 self.out nn.Conv2d(base_ch, 2, kernel_size3, padding1) def _block(self, in_ch, out_ch): return nn.Sequential( nn.Conv2d(in_ch, out_ch, 3, padding1), nn.ReLU(inplaceTrue), nn.Conv2d(out_ch, out_ch, 3, padding1), nn.ReLU(inplaceTrue) ) def forward(self, x): # x: [B, 2, H, W]通道0是参考图通道1是浮动图 e1 self.enc1(x) e2 self.enc2(F.max_pool2d(e1, 2)) e3 self.enc3(F.max_pool2d(e2, 2)) d2 F.interpolate(e3, scale_factor2, modebilinear, align_cornersFalse) d2 self.dec2(torch.cat([d2, e2], dim1)) d1 F.interpolate(d2, scale_factor2, modebilinear, align_cornersFalse) d1 self.dec1(torch.cat([d1, e1], dim1)) return self.out(d1) # 输出位移场单位是像素这段代码的关键点有三个。第一输入是两通道堆叠参考图和浮动图各占一个通道这样网络能同时看到两幅图的信息。第二编码器下采样两次感受野大概能覆盖几十个像素的位移如果你的配准任务位移量更大需要再加一层下采样或者用空洞卷积。第三输出层不加激活函数因为位移值可以是正也可以是负加了ReLU或者Sigmoid反而限制表达。损失函数部分NCC是常用选择它对线性灰度变化不敏感适合SAR图像。下面是一个局部NCC的实现def local_ncc_loss(I1, I2, win9): # I1, I2: [B, 1, H, W] # 用平均池化算局部均值和方差 pad win // 2 kernel torch.ones(1, 1, win, win, deviceI1.device) / (win * win) mu1 F.conv2d(I1, kernel, paddingpad) mu2 F.conv2d(I2, kernel, paddingpad) mu1_sq, mu2_sq mu1**2, mu2**2 mu1_mu2 mu1 * mu2 sigma1_sq F.conv2d(I1**2, kernel, paddingpad) - mu1_sq sigma2_sq F.conv2d(I2**2, kernel, paddingpad) - mu2_sq sigma12 F.conv2d(I1*I2, kernel, paddingpad) - mu1_mu2 # NCC公式加eps防止除零 ncc (sigma12 1e-5) / (torch.sqrt(sigma1_sq * sigma2_sq) 1e-5) return -ncc.mean() # 取负因为要最小化损失窗口大小win是个关键参数。win太小NCC对噪声敏感win太大局部性丢失位移场会过于平滑。SAR图像上我一般从9开始试如果配准结果边缘模糊就降到7如果噪声太大导致损失震荡就升到11或13。3. 把环境搭起来Python安装、依赖配置和源码目录梳理3.1 Python环境准备与常见安装坑这套源码是Python写的所以第一步是把Python环境弄好。如果你机器上还没有Python去官网下载3.8到3.10之间的版本不要用3.11以上因为PyTorch某些版本对3.11的支持还不稳定。安装的时候勾选“Add Python to PATH”否则后面在命令行里敲python会提示找不到。装完之后验证一下python --version pip --version如果提示“python was not found”说明PATH没配好重新安装或者手动把Python安装目录加到系统环境变量里。Windows用户特别注意Microsoft Store里那个Python是个阉割版不要用去官网下完整安装包。虚拟环境强烈建议建一个避免和系统里其他包的版本冲突python -m venv sar_reg_env # Windows sar_reg_env\Scripts\activate # Linux/Mac source sar_reg_env/bin/activate激活之后命令行前面会出现(sar_reg_env)的提示。接下来装依赖。这套源码大概率需要torch、torchvision、numpy、opencv-python、scipy、matplotlib这几个库。PyTorch的安装命令要根据你的CUDA版本选没有GPU就用CPU版# CPU版本 pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu # CUDA 11.8版本 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 其他依赖 pip install numpy opencv-python scipy matplotlib tqdm装完之后验证PyTorch能不能正常调用GPUimport torch print(torch.__version__) print(torch.cuda.is_available()) # 有GPU且驱动正常应该输出True如果torch.cuda.is_available()返回False先检查显卡驱动版本再检查装的PyTorch是不是CUDA版。CPU版永远返回False这是正常的。3.2 源码目录结构和各文件职责解压源码包之后先别急着跑花五分钟把目录结构看清楚。典型的无监督SAR配准项目会有这些文件或目录文件/目录作用train.py训练入口读配置、建模型、跑训练循环inference.py 或 test.py推理入口加载权重对单对图像做配准models/网络结构定义通常有unet.py或displacement_net.pydatasets/数据加载类负责读图、归一化、构造图像对losses/损失函数NCC、MI、正则项都在这里utils/工具函数图像读写、位移场可视化、指标计算configs/配置文件yaml或json格式存超参数checkpoints/训练权重保存目录data/放SAR图像的目录通常分train和test拿到源码后第一件事是看configs里的配置文件里面会写图像路径、batch size、学习率、训练轮数这些。把路径改成你本机实际的路径否则跑起来第一行就报FileNotFoundError。3.3 数据准备SAR图像怎么放、怎么预处理SAR图像常见格式有GeoTIFF、ENVI、PNG。源码里的数据加载类一般用opencv或者rasterio读。如果是GeoTIFF需要rasterio如果是普通PNGopencv就够了。数据目录结构通常是data/ train/ pair_001_ref.png pair_001_float.png pair_002_ref.png pair_002_float.png ... test/ pair_101_ref.png pair_101_float.png参考图和浮动图要一一对应命名规则在数据加载类里定义。SAR图像读进来之后要做归一化常见做法是减去均值除以标准差或者做对数变换后再归一化。对数变换能把SAR图像里的乘性噪声变成加性对后续配准有帮助import cv2 import numpy as np def load_sar_image(path): img cv2.imread(path, cv2.IMREAD_GRAYSCALE).astype(np.float32) # 对数变换加1防止log(0) img np.log1p(img) # 归一化到[0,1] img (img - img.min()) / (img.max() - img.min() 1e-8) return img这段预处理不是必须的但SAR图像动态范围大不做归一化直接送进网络损失函数很容易被大值主导训练不稳定。如果你的源码里没有这一步建议加上。4. 跑通训练和推理命令、参数和结果检查4.1 启动训练命令行参数怎么传训练脚本一般用argparse接收参数典型命令长这样python train.py \ --data_root ./data/train \ --epochs 100 \ --batch_size 4 \ --lr 1e-4 \ --image_size 256 \ --save_dir ./checkpoints \ --gpu 0逐个参数说明。data_root指向训练数据目录里面要有成对的参考图和浮动图。epochs是训练轮数无监督配准通常需要50到200轮才能收敛具体看数据量和图像复杂度。batch_size受显存限制256x256的图像4GB显存大概能跑4到8。lr是学习率1e-4是常用起点如果损失震荡就降到5e-5如果损失下降太慢就升到2e-4。image_size是训练时裁剪的图像块大小SAR图像通常很大整图送进去显存扛不住所以随机裁剪成小块训练。save_dir是权重保存路径gpu指定用哪块显卡没有GPU就删掉这个参数或者设为-1。训练过程中要盯着损失曲线。正常情况损失应该在前10轮快速下降然后缓慢收敛。如果损失一直不降检查数据加载是不是有问题——比如参考图和浮动图搞反了或者归一化没做导致数值范围不对。如果损失降到某个值之后开始震荡可能是学习率太大调小试试。4.2 推理脚本加载权重做单对图像配准训练完之后用推理脚本对测试图像做配准python inference.py \ --checkpoint ./checkpoints/best_model.pth \ --ref_path ./data/test/pair_101_ref.png \ --float_path ./data/test/pair_101_float.png \ --output_dir ./results \ --image_size 256推理脚本的核心逻辑是加载权重、读参考图和浮动图、预处理、送进网络得到位移场、用位移场对浮动图做warp、保存配准后的图像和位移场可视化。下面是一个简化的推理流程import torch import cv2 import numpy as np from models.displacement_net import DisplacementNet def inference(checkpoint, ref_path, float_path, output_dir): device torch.device(cuda if torch.cuda.is_available() else cpu) # 加载模型 model DisplacementNet().to(device) model.load_state_dict(torch.load(checkpoint, map_locationdevice)) model.eval() # 读图并预处理 ref cv2.imread(ref_path, cv2.IMREAD_GRAYSCALE).astype(np.float32) flo cv2.imread(float_path, cv2.IMREAD_GRAYSCALE).astype(np.float32) ref (ref - ref.min()) / (ref.max() - ref.min() 1e-8) flo (flo - flo.min()) / (flo.max() - flo.min() 1e-8) # 转tensor ref_t torch.from_numpy(ref).unsqueeze(0).unsqueeze(0).to(device) flo_t torch.from_numpy(flo).unsqueeze(0).unsqueeze(0).to(device) inp torch.cat([ref_t, flo_t], dim1) # 前向 with torch.no_grad(): disp model(inp) # [1, 2, H, W] # 用位移场warp浮动图 disp_np disp.squeeze(0).permute(1, 2, 0).cpu().numpy() h, w flo.shape grid_x, grid_y np.meshgrid(np.arange(w), np.arange(h)) map_x (grid_x disp_np[:, :, 0]).astype(np.float32) map_y (grid_y disp_np[:, :, 1]).astype(np.float32) warped cv2.remap(flo, map_x, map_y, cv2.INTER_LINEAR) # 保存结果 cv2.imwrite(f{output_dir}/warped.png, (warped * 255).astype(np.uint8)) cv2.imwrite(f{output_dir}/disp_x.png, ((disp_np[:,:,0] - disp_np[:,:,0].min()) / (disp_np[:,:,0].ptp() 1e-8) * 255).astype(np.uint8)) cv2.imwrite(f{output_dir}/disp_y.png, ((disp_np[:,:,1] - disp_np[:,:,1].min()) / (disp_np[:,:,1].ptp() 1e-8) * 255).astype(np.uint8))这段代码里cv2.remap的用法要注意map_x和map_y的坐标是浮点数表示输出图像每个像素对应输入图像的哪个位置。位移场disp_np[:,:,0]是x方向位移加到grid_x上得到采样位置。如果位移场方向定义反了warp出来的图会更歪这时候把加号改成减号试试。4.3 结果怎么判断好坏三个检查点配准结果好不好不能只看warped图顺不顺眼。我一般会做三个检查。第一把参考图和warped图叠在一起做棋盘格显示看地物边缘是不是对齐了。第二算配准前后的NCC值配准后应该明显高于配准前。第三看位移场的分布正常位移场应该是平滑的、渐变的如果出现大量噪点或者极端值说明网络没学好。def compute_ncc(img1, img2): img1 img1 - img1.mean() img2 img2 - img2.mean() return (img1 * img2).sum() / (np.sqrt((img1**2).sum()) * np.sqrt((img2**2).sum()) 1e-8) ncc_before compute_ncc(ref, flo) ncc_after compute_ncc(ref, warped) print(fNCC before: {ncc_before:.4f}, after: {ncc_after:.4f})如果配准后NCC没有提升甚至下降了说明网络输出的位移场是错的。这时候先检查训练损失有没有收敛再检查推理时的预处理和训练时是否一致。训练时做了对数变换推理时没做结果肯定不对。5. 避坑指南无监督SAR配准训练中最容易翻车的五个地方5.1 损失不降反升位移场全是噪点现象训练开始后损失从第一轮就往上走或者震荡剧烈推理出来的位移场像随机噪声。原因通常是学习率太大或者损失函数里的正则项权重太小。无监督配准的损失函数一般是NCC损失加上位移场平滑正则项如果正则项权重设成0网络会输出极端位移来强行提高NCC导致位移场完全不可用。解决方法是把学习率降到1e-5正则项权重从0.01开始试观察位移场的平滑程度。5.2 配准后图像出现明显撕裂或折叠现象warped图在某些区域出现像素拉伸或者折叠地物形状被破坏。原因是位移场不满足微分同胚约束相邻像素的位移差异太大。解决方法是加一个位移场梯度的L2正则项惩罚相邻像素位移的差异def smoothness_loss(disp): # disp: [B, 2, H, W] dx torch.abs(disp[:, :, :, 1:] - disp[:, :, :, :-1]) dy torch.abs(disp[:, :, 1:, :] - disp[:, :, :-1, :]) return dx.mean() dy.mean()这个损失加到总损失里权重一般设0.01到0.1。权重太大位移场会过于平滑配准精度下降太小起不到约束作用。5.3 训练集上表现好测试集上完全不能用现象训练时损失降得很低warped图看着也不错但换一对测试图像配准结果一塌糊涂。原因是过拟合。无监督配准虽然没有标签但网络仍然可能记住训练图像对的特定形变模式换一对图就不适用了。解决办法是增加训练数据量做数据增强随机裁剪、翻转、加噪声以及在损失函数里加权重衰减。如果数据实在少考虑用预训练的光学配准网络做迁移学习。5.4 显存不够batch size只能设1现象跑训练时提示CUDA out of memory把batch size降到1还是不够。原因是图像尺寸太大或者网络参数量太多。解决办法有三个把image_size从256降到128把网络base_ch从32降到16或者用梯度累积模拟大batch。梯度累积的做法是每算完一个batch的梯度不更新参数累积几个batch之后再更新accum_steps 4 for i, batch in enumerate(loader): loss model(batch) / accum_steps loss.backward() if (i 1) % accum_steps 0: optimizer.step() optimizer.zero_grad()这样等效于batch size放大了4倍但显存占用不变。5.5 推理结果和训练时可视化对不上现象训练时用验证集跑出来的warped图挺好用推理脚本单独跑同一对图结果不一样。原因通常是推理脚本里的预处理和训练时不一致。比如训练时做了对数变换和归一化推理时只做了归一化或者训练时图像是float32推理时读进来是uint8没转。解决办法是把预处理逻辑抽成一个函数训练和推理都调用同一个函数避免手写两套代码。6. 进阶技巧用多尺度策略提升大位移配准精度无监督SAR配准最头疼的场景是大位移——两幅图之间地物偏移超过网络感受野单尺度网络根本抓不住对应关系。我试过的一个有效方案是多尺度由粗到精先把图像降采样到1/4训练一个粗配准网络把大位移拉回来再在1/2尺度上训练精配准网络在粗配准结果基础上做微调最后在原尺度上做像素级精修。这套策略在位移超过50像素的场景下比单尺度网络NCC能提升0.1以上。具体实现上不需要训练三个独立网络可以用一个网络在不同尺度上迭代推理。下面是一个两阶段推理的示例def multi_scale_inference(model, ref, flo, scales[4, 2, 1]): disp_total np.zeros((flo.shape[0], flo.shape[1], 2), dtypenp.float32) for scale in scales: # 降采样 h, w ref.shape ref_s cv2.resize(ref, (w//scale, h//scale)) flo_s cv2.resize(flo, (w//scale, h//scale)) # 把上一尺度的位移场上采样后加到浮动图上 if scale ! scales[0]: disp_up cv2.resize(disp_total, (w//scale, h//scale)) / scale grid_x, grid_y np.meshgrid(np.arange(w//scale), np.arange(h//scale)) map_x (grid_x disp_up[:,:,0]).astype(np.float32) map_y (grid_y disp_up[:,:,1]).astype(np.float32) flo_s cv2.remap(flo_s, map_x, map_y, cv2.INTER_LINEAR) # 网络推理 inp np.stack([ref_s, flo_s], axis0)[None] inp torch.from_numpy(inp).float() with torch.no_grad(): disp_s model(inp).squeeze(0).permute(1,2,0).numpy() # 累加位移 disp_up cv2.resize(disp_s, (w, h)) * scale disp_total disp_up return disp_total这段代码的关键在于位移场的尺度变换。在1/4尺度上算出来的位移上采样到原尺寸时要乘以4因为位移是以像素为单位的尺度变了位移值也要跟着变。另外每一级推理时浮动图要先用上一级的位移场warp一下这样网络只需要学残差位移负担更小。多尺度策略的代价是推理时间变长大概是单尺度的3倍。如果对实时性要求高可以只做两级1/2和1精度损失不大。另外多尺度对训练也有帮助训练时随机选尺度做数据增强能让网络对不同尺度的位移都鲁棒。最后说一个我踩过的坑多尺度推理时如果某一级位移场出现极端值上采样之后会污染下一级的输入导致整个结果崩掉。解决办法是在每一级位移场输出后加一个clip把位移限制在合理范围内比如[-64, 64]像素。这个clip值根据你的实际位移量级来定宁可设大一点也不要设小设小了会把真实位移截断。希望帮到你。本文还有配套的精品资源点击获取