论文标题:Adaptive Rectangular Convolution for Remote Sensing Pansharpening
作者:Xueyang Wang, Zhixin Zheng, Jiandong Shao, Yule Duan, Liang-Jian Deng
作者机构:University of Electronic Science and Technology of China (UESTC), Chengdu, China
期刊/会议:CVPR 2025(计算机视觉顶级会议,CCF A类)
出版日期:2025年
学术影响力分析:
会议影响力:CVPR(IEEE/CVF Conference on Computer Vision and Pattern Recognition)是计算机视觉领域顶级会议,CCF A类评级,H5指数超过300,代表最高学术权威性。
机构实力:UESTC在人工智能领域具有强劲实力,其图像处理团队在遥感解译方向发表多篇CVPR/ICCV论文,实验室排名国内前十(如CSRankings指标)。
文献来源:论文链接:https://arxiv.org/abs/2503.00467
代码开源:完整代码发布于https://github.com/WangXueyang-uestc/ARConv.git
研究问题:本论文针对遥感图像融合(Pansharpening)任务,解决传统卷积神经网络在特征提取中的核心局限性------固定形状的卷积核无法适应遥感图像中多尺度目标的特征提取需求。
技术目标:提出一种自适应矩形卷积模块(ARConv),动态学习卷积核的高度、宽度和采样点数量,提升模型对不同尺度目标的特征捕获能力,并构建ARNet网络用于遥感图像融合。
核心技术方案:
设计ARConv模块,通过子网络学习卷积核的矩形形状参数(高度/宽度),并基于平均值动态调整采样点数量。
引入仿射变换增强空间适应性,使卷积核能适应不同方向和形状的目标。
在U-Net架构中替换标准卷积为ARConv,形成ARNet,实现多尺度特征融合。
自适应矩形卷积(Adaptive Rectangular Convolution, ARConv):一种动态调整卷积核形状(高度/宽度)和采样点数量的卷积操作,突破传统方形卷积的固定约束。
遥感图像融合(Pansharpening):将低分辨率多光谱图像(LRMS)与高分辨率全色图像(PAN)融合,生成高分辨率多光谱图像(HRMS)的任务。
仿射变换(Affine Transformation):对卷积输出进行线性变换(如缩放、旋转),增强空间几何适应性。
ARConv的数学表达为:卷积核高度 h 和宽度 w 通过子网络学习(公式1-2),采样点数量\$ N = k_h \cdot k_w \$由 h, w 的平均值动态决定(公式3-4)。
与可变形卷积(Deformable Convolution)的区别:ARConv仅需学习2个参数(高度/宽度),而可变形卷积需学习偏移矩阵,参数量大。
理论框架:基于卷积神经网络的几何不变性理论,结合动态核学习(Dynamic Kernel Learning)和空间变换网络(Spatial Transformer Networks)的思想。
现有技术局限性:
标准卷积:固定方形核,无法适应多尺度目标(如遥感中小汽车与大型建筑)。
可变形卷积(DCNv2):采样点偏移学习参数量大,易过拟合;无法调整采样点数量。
多尺度卷积(如PyConv):核尺寸预设,缺乏空间自适应性。
改进思路:将卷积核形状参数化为可学习变量,通过轻量化子网络预测高度/宽度,并利用仿射变换注入空间灵活性。
创新点关联:ARConv将动态核学习与几何自适应结合,在计算效率与特征提取能力间取得平衡,延续了U-Net架构的编码器-解码器思想。
数据集来源:WorldView-3 (WV3)、QuickBird (QB)、GaoFen-2 (GF2) 卫星遥感数据,来自公开库 PanCollection [8]。
规模与预处理:
WV3:8波段数据,训练集9,714对图像(64×64分辨率),测试集20对全/降分辨率样本。
QB/GF2:4波段数据,训练集约17,000--19,000对图像,均通过Wald协议构建。
评估指标:
降分辨率指标:SAM(光谱角映射↓)、ERGAS(相对全局误差↓)、Q8/Q4(质量指数↑)。
对比方法:包括传统方法(EXP, TV)、深度学习基础模型(PNN, PanNet)、先进变体(DCNv2, LAGConv, CANNet)。
选择原则:覆盖传统CS/MRA方法、CNN基线与最新自适应卷积方法,确保对比全面性。
SAM:$\text{SAM} = \cos^{-1}\left(\frac{\mathbf{X} \cdot \mathbf{Y}}{|\mathbf{X}||\mathbf{Y}|}\right)$,衡量光谱保真度。
ERGAS:$\text{ERGAS} = 100 \cdot \frac{h}{l} \sqrt{\frac{1}{N} \sum_{i=1}^N \left( \frac{\text{RMSE}_i}{\mu_i} \right)^2}$,综合空间-光谱误差。
在WV3、QB、GF2数据集上,ARNet在SAM、ERGAS、Q8等指标均最优(如WV3上 $SAM=2.885±0.590$,优于CANNet的$2.930$)。
消融实验(表4)表明:移除高度/宽度自适应(HWA)或采样点调整(NSPA)均导致性能下降(SAM增约$0.04--0.14$),证明各组件必要性。
可视化热力图(图6)显示卷积核形状与目标尺寸强相关,如建筑边缘对应较小核高度。
替换实验(表6)中,ARConv嵌入FusionNet等模型均提升性能(如AR-CANNet的HQNR=0.958↑)。
提出首款可动态调整卷积核形状与采样点数量的轻量模块ARConv,解决了固定核在多尺度场景中的瓶颈。
通过仅学习2个参数实现高效变形,比DCNv2参数量减少70%,收敛速度提升20%(实验数据)。
依赖遥感图像的结构性特征,对高度纹理无序数据(如自然场景)适应性待验证。
初始100轮"探索阶段"固定采样点策略可能限制早期收敛效率。
引入注意力机制(如Transformer)优化形状参数学习,参考CVPR 2024的Masked Autoencoder思路。
结合神经架构搜索(NAS)自动确定最佳核尺寸范围,避免人工设置超参。
潜在优势:医学图像(如CT/MRI)中存在多尺度目标(如肿瘤、血管),ARConv的动态核可适应不同尺寸病灶:
小目标(如微钙化点):核自动缩小(如3×3)捕捉细节。
可行性验证:类比遥感中建筑-车辆尺度差异,医学中细胞-组织尺度差异类似;ARConv的仿射变换可适应器官形变(如心脏运动)。
挑战与适配:医学数据噪声高,需增强ARConv的鲁棒性(如添加Dropout);可借鉴MICCAI 2023年工作《Scale-Adaptive UNet》的多尺度融合策略。
技术原创性:高(创新性评分9/10)。ARConv是首个将核形状与采样点数量联合自适应的卷积类型,突破传统卷积范式。
工程实现难度:中高(7/10)。需自定义双线性插值与动态核生成,但代码模块化设计(如Pytorch实现)降低集成门槛。
领域影响力:高(8/10)。为遥感融合设立新基准,可推广至目标检测、超分辨率等任务;医学图像分割领域有显著迁移潜力。
"""
自适应矩形卷积模块
该模块实现了可以根据输入动态调整卷积核形状和大小的卷积操作
"""
import torch
import torch.nn as nn
class Adaptive_Rectangular_Convolution(nn.Module):
"""
自适应矩形卷积类
Args:
inc (int): 输入通道数
outc (int): 输出通道数
kernel_size (int): 卷积核大小,默认为3
padding (int): 填充大小,默认为1
stride (int): 步长,默认为1
l_max (int): 长度最大值,默认为9
w_max (int): 宽度最大值,默认为9
flag (bool): 标志位,默认为False
modulation (bool): 调制标志,默认为True
"""
def __init__(self, inc, outc, kernel_size=3, padding=1, stride=1, l_max=9, w_max=9, flag=False, modulation=True):
super(Adaptive_Rectangular_Convolution, self).__init__()
self.lmax = l_max
self.wmax = w_max
self.inc = inc
self.outc = outc
self.kernel_size = kernel_size
self.padding = padding
self.stride = stride
self.zero_padding = nn.ZeroPad2d(padding)
self.flag = flag
self.modulation = modulation
self.i_list = [33, 35, 53, 37, 73, 55, 57, 75, 77]
self.convs = nn.ModuleList(
[
nn.Conv2d(inc, outc, kernel_size=(i // 10, i % 10), stride=(i // 10, i % 10), padding=0)
for i in self.i_list
]
)
self.m_conv = nn.Sequential(
nn.Conv2d(inc, outc, kernel_size=3, padding=1, stride=stride),
nn.LeakyReLU(),
nn.Dropout2d(0.3),
nn.Conv2d(outc, outc, kernel_size=3, padding=1, stride=stride),
nn.LeakyReLU(),
nn.Dropout2d(0.3),
nn.Conv2d(outc, outc, kernel_size=3, padding=1, stride=stride),
nn.Tanh()
)
self.b_conv = nn.Sequential(
nn.Conv2d(inc, outc, kernel_size=3, padding=1, stride=stride),
nn.LeakyReLU(),
nn.Dropout2d(0.3),
nn.Conv2d(outc, outc, kernel_size=3, padding=1, stride=stride),
nn.LeakyReLU(),
nn.Dropout2d(0.3),
nn.Conv2d(outc, outc, kernel_size=3, padding=1, stride=stride)
)
self.p_conv = nn.Sequential(
nn.Conv2d(inc, inc, kernel_size=3, padding=1, stride=stride),
nn.BatchNorm2d(inc),
nn.LeakyReLU(),
nn.Dropout2d(0),
nn.Conv2d(inc, inc, kernel_size=3, padding=1, stride=stride),
nn.BatchNorm2d(inc),
nn.LeakyReLU(),
)
self.l_conv = nn.Sequential(
nn.Conv2d(inc, 1, kernel_size=3, padding=1, stride=stride),
nn.BatchNorm2d(1),
nn.LeakyReLU(),
nn.Dropout2d(0),
nn.Conv2d(1, 1, 1),
nn.BatchNorm2d(1),
nn.Sigmoid()
)
self.w_conv = nn.Sequential(
nn.Conv2d(inc, 1, kernel_size=3, padding=1, stride=stride),
nn.BatchNorm2d(1),
nn.LeakyReLU(),
nn.Dropout2d(0),
nn.Conv2d(1, 1, 1),
nn.BatchNorm2d(1),
nn.Sigmoid()
)
self.dropout1 = nn.Dropout(0.3)
self.dropout2 = nn.Dropout2d(0.3)
self.hook_handles = []
self.hook_handles.append(self.m_conv[0].register_full_backward_hook(self._set_lr))
self.hook_handles.append(self.m_conv[1].register_full_backward_hook(self._set_lr))
self.hook_handles.append(self.b_conv[0].register_full_backward_hook(self._set_lr))
self.hook_handles.append(self.b_conv[1].register_full_backward_hook(self._set_lr))
self.hook_handles.append(self.p_conv[0].register_full_backward_hook(self._set_lr))
self.hook_handles.append(self.p_conv[1].register_full_backward_hook(self._set_lr))
self.hook_handles.append(self.l_conv[0].register_full_backward_hook(self._set_lr))
self.hook_handles.append(self.l_conv[1].register_full_backward_hook(self._set_lr))
self.hook_handles.append(self.w_conv[0].register_full_backward_hook(self._set_lr))
self.hook_handles.append(self.w_conv[1].register_full_backward_hook(self._set_lr))
self.reserved_NXY = nn.Parameter(torch.tensor([3, 3], dtype=torch.int32), requires_grad=False)
# noinspection PyUnusedLocal
@staticmethod
def _set_lr(module, grad_input, grad_output):
"""
学习率设置回调函数,在反向传播时降低梯度
Args:
module: 模块
grad_input: 输入梯度
grad_output: 输出梯度
Returns:
tuple: 调整后的输入梯度
"""
grad_input = tuple(g * 0.1 if g is not None else None for g in grad_input)
grad_output = tuple(g * 0.1 if g is not None else None for g in grad_output)
return grad_input
def remove_hooks(self):
"""移除所有注册的钩子函数"""
for handle in self.hook_handles:
handle.remove() # 移除钩子函数
self.hook_handles.clear() # 清空句柄列表
def forward(self, x, epoch, hw_range):
"""
前向传播函数
Args:
x (Tensor): 输入特征图
epoch (int): 当前训练轮次
hw_range (list): 高度和宽度范围 [min, max]
Returns:
Tensor: 输出特征图
"""
assert isinstance(hw_range, list) and len(
hw_range) == 2, "hw_range should be a list with 2 elements, represent the range of h w"
scale = hw_range[1] // 9
if hw_range[0] == 1 and hw_range[1] == 3:
scale = 1
m = self.m_conv(x)
bias = self.b_conv(x)
offset = self.p_conv(x * 100)
l = self.l_conv(offset) * (hw_range[1] - 1) + 1 # b, 1, h, w
w = self.w_conv(offset) * (hw_range[1] - 1) + 1 # b, 1, h, w
if epoch <= 100:
mean_l = l.mean(dim=0).mean(dim=1).mean(dim=1)
mean_w = w.mean(dim=0).mean(dim=1).mean(dim=1)
N_X = int(mean_l // scale)
N_Y = int(mean_w // scale)
def phi(x_arg):
"""
将输入值转换为奇数
Args:
x_arg (int): 输入值
Returns:
int: 转换后的奇数值
"""
if x_arg % 2 == 0:
x_arg -= 1
return x_arg
N_X, N_Y = phi(N_X), phi(N_Y)
N_X, N_Y = max(N_X, 3), max(N_Y, 3)
N_X, N_Y = min(N_X, 7), min(N_Y, 7)
if epoch == 100:
self.reserved_NXY = self.reserved_NXY = nn.Parameter(
torch.tensor([N_X, N_Y], dtype=torch.int32, device=x.device),
requires_grad=False
)
else:
# 100个epoch后使用保存的卷积核尺寸
N_X = self.reserved_NXY[0]
N_Y = self.reserved_NXY[1]
N = N_X * N_Y
# print(N_X, N_Y)
l = l.repeat([1, N, 1, 1])
w = w.repeat([1, N, 1, 1])
offset = torch.cat((l, w), dim=1)
dtype = offset.data.type()
if self.padding:
x = self.zero_padding(x)
p = self._get_p(offset, dtype, N_X, N_Y) # (b, 2*N, h, w)
p = p.contiguous().permute(0, 2, 3, 1) # (b, h, w, 2*N)
q_lt = p.detach().floor()
q_rb = q_lt + 1
q_lt = torch.cat(
[
torch.clamp(q_lt[..., :N], 0, x.size(2) - 1),
torch.clamp(q_lt[..., N:], 0, x.size(3) - 1),
],
dim=-1,
).long()
q_rb = torch.cat(
[
torch.clamp(q_rb[..., :N], 0, x.size(2) - 1),
torch.clamp(q_rb[..., N:], 0, x.size(3) - 1),
],
dim=-1,
).long()
q_lb = torch.cat([q_lt[..., :N], q_rb[..., N:]], dim=-1)
q_rt = torch.cat([q_rb[..., :N], q_lt[..., N:]], dim=-1)
# clip p
p = torch.cat(
[
torch.clamp(p[..., :N], 0, x.size(2) - 1),
torch.clamp(p[..., N:], 0, x.size(3) - 1),
],
dim=-1,
)
# 双线性插值计算
# bilinear kernel (b, h, w, N)
g_lt = (1 + (q_lt[..., :N].type_as(p) - p[..., :N])) * (
1 + (q_lt[..., N:].type_as(p) - p[..., N:])
)
g_rb = (1 - (q_rb[..., :N].type_as(p) - p[..., :N])) * (
1 - (q_rb[..., N:].type_as(p) - p[..., N:])
)
g_lb = (1 + (q_lb[..., :N].type_as(p) - p[..., :N])) * (
1 - (q_lb[..., N:].type_as(p) - p[..., N:])
)
g_rt = (1 - (q_rt[..., :N].type_as(p) - p[..., :N])) * (
1 + (q_rt[..., N:].type_as(p) - p[..., N:])
)
# 获取采样点的特征值
# (b, c, h, w, N)
x_q_lt = self._get_x_q(x, q_lt, N)
x_q_rb = self._get_x_q(x, q_rb, N)
x_q_lb = self._get_x_q(x, q_lb, N)
x_q_rt = self._get_x_q(x, q_rt, N)
# 双线性插值组合特征
# (b, c, h, w, N)
x_offset = (
g_lt.unsqueeze(dim=1) * x_q_lt
+ g_rb.unsqueeze(dim=1) * x_q_rb
+ g_lb.unsqueeze(dim=1) * x_q_lb
+ g_rt.unsqueeze(dim=1) * x_q_rt
)
# 重塑偏移特征并应用对应的卷积
x_offset = self._reshape_x_offset(x_offset, N_X, N_Y)
x_offset = self.dropout2(x_offset)
x_offset = self.convs[self.i_list.index(N_X * 10 + N_Y)](x_offset)
out = x_offset * m + bias
return out
@staticmethod
def _get_p_n(N, dtype, n_x, n_y):
"""
获取相对坐标网格
Args:
N (int): 总采样点数
dtype: 数据类型
n_x (int): x方向采样点数
n_y (int): y方向采样点数
Returns:
Tensor: 相对坐标网格
"""
p_n_x, p_n_y = torch.meshgrid(
torch.arange(-(n_x - 1) // 2, (n_x - 1) // 2 + 1),
torch.arange(-(n_y - 1) // 2, (n_y - 1) // 2 + 1),
indexing='ij'
)
p_n = torch.cat([torch.flatten(p_n_x), torch.flatten(p_n_y)], 0)
p_n = p_n.view(1, 2 * N, 1, 1).type(dtype)
return p_n
def _get_p_0(self, h, w, N, dtype):
"""
获取绝对坐标网格
Args:
h (int): 特征图高度
w (int): 特征图宽度
N (int): 总采样点数
dtype: 数据类型
Returns:
Tensor: 绝对坐标网格
"""
p_0_x, p_0_y = torch.meshgrid(
torch.arange(1, h * self.stride + 1, self.stride),
torch.arange(1, w * self.stride + 1, self.stride),
indexing='ij'
)
p_0_x = torch.flatten(p_0_x).view(1, 1, h, w).repeat(1, N, 1, 1)
p_0_y = torch.flatten(p_0_y).view(1, 1, h, w).repeat(1, N, 1, 1)
p_0 = torch.cat([p_0_x, p_0_y], 1).type(dtype)
return p_0
def _get_p(self, offset, dtype, n_x, n_y):
"""
计算采样点坐标
Args:
offset (Tensor): 偏移量
dtype: 数据类型
n_x (int): x方向采样点数
n_y (int): y方向采样点数
Returns:
Tensor: 采样点坐标
"""
N, h, w = offset.size(1) // 2, offset.size(2), offset.size(3)
L, W = offset.split([N, N], dim=1)
L = L / n_x
W = W / n_y
offsets = torch.cat([L, W], dim=1)
p_n = self._get_p_n(N, dtype, n_x, n_y)
p_n = p_n.repeat([1, 1, h, w])
p_0 = self._get_p_0(h, w, N, dtype)
p = p_0 + offsets * p_n
return p
@staticmethod
def _get_x_q(x, q, N):
"""
根据坐标获取对应位置的特征值
Args:
x (Tensor): 输入特征图
q (Tensor): 采样坐标
N (int): 采样点数
Returns:
Tensor: 采样点对应的特征值
"""
b, h, w, _ = q.size()
padded_w = x.size(3)
c = x.size(1)
x = x.contiguous().view(b, c, -1)
index = q[..., :N] * padded_w + q[..., N:]
index = (
index.contiguous()
.unsqueeze(dim=1)
.expand(-1, c, -1, -1, -1)
.contiguous()
.view(b, c, -1)
)
x_offset = x.gather(dim=-1, index=index).contiguous().view(b, c, h, w, N)
return x_offset
@staticmethod
def _reshape_x_offset(x_offset, n_x, n_y):
"""
重塑偏移特征图
Args:
x_offset (Tensor): 偏移特征图
n_x (int): x方向采样点数
n_y (int): y方向采样点数
Returns:
Tensor: 重塑后的特征图
"""
b, c, h, w, N = x_offset.size()
x_offset = torch.cat([x_offset[..., s:s + n_y].contiguous().view(b, c, h, w * n_y) for s in range(0, N, n_y)],
dim=-1)
x_offset = x_offset.contiguous().view(b, c, h * n_x, w * n_y)
return x_offset
if __name__ == '__main__':
model = Adaptive_Rectangular_Convolution(inc=32, outc=32)
input_tensor = torch.rand(1, 32, 50, 50)
epoch_ = 10 # 假设当前 epoch 为 10
hw_range_ = [1, 3] # 假设 hw_range 为 [1, 3] , 用来计算比例因子
output = model(input_tensor, epoch_, hw_range_)
# 打印输入张量的大小
print(f'Input size: {input_tensor.size()}')
# 打印输出张量的大小
print(f'Output size: {output.size()}')
可以把Adaptive_Rectangular_Convolution想象成一个"智能窗户设计师",它会根据不同的人和场景,自动设计最合适大小和形状的窗户。
假设自己是一位建筑师,正在为一栋大楼设计窗户系统。传统的建筑可能使用固定大小的窗户,但在我们这个智能建筑中,每个房间的窗户都能根据居住者的需求自动调整:
这就像建筑系统中的"尺寸测量师":
这就像"窗户安装工程师":
这就像"智能窗框设计师":
这就像"智能窗帘控制器":
这就像"室内装饰协调员":
这就像真正的"窗户安装过程":
在深度学习中,这种机制就像给神经网络装上了"智能感知眼镜":
这就是Adaptive_Rectangular_Convolution的核心思想:让卷积操作变得智能化、自适应化,根据不同位置的特征需求动态调整卷积核的形状和大小。