1.立体匹配
立体匹配任务是计算机视觉中的一个重要问题,旨在从一对具有相同场景的图像中估计每个像素的视差(disparity)。当使用双目相机时,视差是指同一物体在左右目相机下的位置差异,它是立体视觉的核心,通过分析左右图像之间的差异来恢复三维场景的深度信息。
1.1 什么是立体匹配?
想象一下你用左右眼同时看一个物体,比如一个雕像。只用左眼看到的图像和只用右眼看到的图像略有不同,因为它们的视角不同(可以自己捂住眼睛试一下)。如上图中中间那一行图像所示,雕像的眼睛在左眼图像中有一个位置(例如坐标x1),而在右眼图像中有另一个位置(例如坐标x2)。这两个位置之间的差异就是视差(disparity)即x1-x2。根据一个像素的视差大小和已知的相机参数便可知道其三维坐标,准确找到同名点是实现高质量深度估计的关键,这也是立体匹配需要做的事情。立体匹配的一般流程如下:
- 图像获取:通过两个相机从不同角度获取同一场景的两幅图像,通常称为左图和右图。
- 特征提取:从左右图像中提取特征,以便进行后续的匹配。
- 代价计算:计算左右图像对应像素之间的代价,通常使用相似性度量(如 SAD、SSD、NCC 等)。
- 代价聚合:通过局部或全局方法聚合代价,以生成代价体(cost volume)。
- 视差估计:从代价体中选择最优的视差值,生成视差图(disparity map)。
- 深度恢复:根据视差图计算场景的深度信息。
1.2 立体匹配应用场景:
- 自动驾驶
在自动驾驶系统中,立体匹配可以用于环境感知,通过识别道路、障碍物和行人等,帮助车辆做出实时决策。 - 机器人视觉
机器人使用立体匹配来获取周围环境的三维信息,以便进行导航、抓取和操作物体。 - 三维重建
立体匹配是三维重建的重要步骤,通过从多个视角获取图像,生成物体或场景的三维模型,广泛应用于虚拟现实和增强现实中。 - 医疗影像
在医疗影像分析中,立体匹配可以用于对比不同视角下的影像,帮助医生更好地识别病变区域。 - 文化遗产保护
在文化遗产保护中,立体匹配可以用于生成文物或历史遗址的三维模型,帮助进行数字化保存和虚拟展示。 - 计算机图形学
立体匹配在计算机图形学中用于生成立体视觉效果和深度图,增强用户体验。
2.传统算法
2.1 局部算法
局部算法主要基于窗口匹配的思想,计算左右图像中对应窗口的相似度。
2.1.1 SAD(Sum of Absolute Differences)
- 原理:SAD 计算左右图像对应窗口像素差的绝对值之和。具体而言,对于给定的窗口,计算窗口内每个像素在左右图像中的差值的绝对值,并将这些绝对值相加。值越小表示匹配度越高,意味着左右窗口在该位置的相似性。
- 公式:
SAD ( d ) = ∑ ( i , j ) ∈ W ∣ I L ( i , j ) − I R ( i , j − d ) ∣ \text{SAD}(d) = \sum_{(i,j) \in W} |I_L(i,j) - I_R(i,j-d)| SAD(d)=(i,j)∈W∑∣IL(i,j)−IR(i,j−d)∣
其中, I L I_L IL 和 I R I_R IR 分别是左图和右图, W W W 是窗口内的像素集合, d d d 是视差。
2.1.2 SSD(Sum of Squared Differences)
- 原理:SSD 计算左右图像对应窗口像素差的平方和。与 SAD 类似,SSD 计算窗口内每个像素的差值的平方,并将这些平方值相加。值越小表示匹配度越高。
- 公式:
SSD ( d ) = ∑ ( i , j ) ∈ W ( I L ( i , j ) − I R ( i , j − d ) ) 2 \text{SSD}(d) = \sum_{(i,j) \in W} (I_L(i,j) - I_R(i,j-d))^2 SSD(d)=(i,j)∈W∑(IL(i,j)−IR(i,j−d))2
其中, d d d 为视差。
2.1.3 NCC(Normalized Cross-Correlation)
- 原理:NCC 计算左右图像对应窗口的归一化互相关。该方法首先计算窗口内像素的均值和标准差,然后使用这些统计量对窗口进行归一化处理。最后,计算归一化后的窗口之间的相关性。值越大表示匹配度越高,反映出窗口之间的相似性。
- 公式:
NCC ( d ) = ∑ ( i , j ) ∈ W ( I L ( i , j ) − I L ˉ ) ( I R ( i , j − d ) − I R ˉ ) ∑ ( i , j ) ∈ W ( I L ( i , j ) − I L ˉ ) 2 ⋅ ∑ ( i , j ) ∈ W ( I R ( i , j − d ) − I R ˉ ) 2 \text{NCC}(d) = \frac{\sum_{(i,j) \in W} (I_L(i,j) - \bar{I_L})(I_R(i,j-d) - \bar{I_R})}{\sqrt{\sum_{(i,j) \in W} (I_L(i,j) - \bar{I_L})^2} \cdot \sqrt{\sum_{(i,j) \in W} (I_R(i,j-d) - \bar{I_R})^2}} NCC(d)=∑(i,j)∈W(IL(i,j)−ILˉ)2⋅∑(i,j)∈W(IR(i,j−d)−IRˉ)2∑(i,j)∈W(IL(i,j)−ILˉ)(IR(i,j−d)−IRˉ)
其中, I L ˉ \bar{I_L} ILˉ 和 I R ˉ \bar{I_R} IRˉ 分别是左图和右图窗口的均值。
2.2 全局算法
全局算法通常将立体匹配问题建模为能量最小化问题,考虑整个图像的一致性。
2.2.1 图割算法(Graph Cuts)
- 原理:将立体匹配问题转化为图论中的最小割问题。图割算法通过构建一个图,其中节点表示像素,边表示像素之间的关系。通过最小化能量函数(通常包含数据项和光滑项),找到一组最优的视差值,从而实现立体匹配。
- 能量函数:
E ( D ) = ∑ p E d a t a ( p , D ( p ) ) + λ ∑ ( p , q ) ∈ N E s m o o t h ( D ( p ) , D ( q ) ) E(D) = \sum_{p} E_{data}(p, D(p)) + \lambda \sum_{(p,q) \in N} E_{smooth}(D(p), D(q)) E(D)=p∑Edata(p,D(p))+λ(p,q)∈N∑Esmooth(D(p),D(q))
其中, E d a t a E_{data} Edata 是数据项, E s m o o t h E_{smooth} Esmooth 是光滑项, D ( p ) D(p) D(p) 是像素 p p p 的视差。
2.2.2 信念传播算法(Belief Propagation)
- 原理:通过迭代地在图像的像素网格上传递"信念"来优化能量函数。信念传播算法在每次迭代中更新每个像素的视差估计,并通过相邻像素之间的相互影响来逐步收敛到全局最优的视差图。
- 信念更新:
b i ( d ) = ∑ j ∈ N ( i ) message i j ( d ) b_i(d) = \sum_{j \in N(i)} \text{message}_{ij}(d) bi(d)=j∈N(i)∑messageij(d)
其中, b i ( d ) b_i(d) bi(d) 是像素 i i i 的信念, N ( i ) N(i) N(i) 是像素 i i i 的邻域, message i j ( d ) \text{message}_{ij}(d) messageij(d) 是从像素 j j j 传递给像素 i i i 的信念。
2.3 半全局算法
半全局算法试图在局部算法的效率和全局算法的准确性之间取得平衡。
2.3.1 SGM(Semi-Global Matching)
- 原理:在多个(通常是8或16个)方向上进行一维的路径代价聚合,然后将这些路径的代价进行加和。SGM 通过在多个方向上累积代价来考虑全局信息,从而选择具有最小聚合代价的视差值。
- 代价聚合:
C ( p , d ) = D ( p , d ) + ∑ q ∈ P ( p ) cost ( q , d ) C(p, d) = D(p, d) + \sum_{q \in P(p)} \text{cost}(q, d) C(p,d)=D(p,d)+q∈P(p)∑cost(q,d)
其中, C ( p , d ) C(p, d) C(p,d) 是聚合代价, D ( p , d ) D(p, d) D(p,d) 是初始代价, P ( p ) P(p) P(p) 是从像素 p p p 出发的路径集合, cost ( q , d ) \text{cost}(q, d) cost(q,d) 是路径上的代价。
2.3.2 AD-Census(Adaptive Census)
AD-Census 是一种用于立体匹配的算法,旨在通过结合局部特征和自适应性来提高视差估计的精度。
-
原理:AD-Census 通过在图像的局部窗口内计算特征描述符来进行匹配。每个像素的特征描述符是基于其周围像素值相对于中心像素的关系生成的。采用自适应窗口和特征加权的方法,可以有效提高匹配的准确性,尤其是在复杂场景和遮挡情况下。
-
特征描述符生成:
C ( p ) = { 1 if I L ( i , j ) ≥ I L ( c ) 0 if I L ( i , j ) < I L ( c ) C(p) = \begin{cases} 1 & \text{if } I_L(i,j) \geq I_L(c) \\ 0 & \text{if } I_L(i,j) < I_L(c) \end{cases} C(p)={10if IL(i,j)≥IL(c)if IL(i,j)<IL(c)
其中, C ( p ) C(p) C(p) 是特征描述符, I L ( i , j ) I_L(i,j) IL(i,j) 是左图像中位置 ( i , j ) (i,j) (i,j) 的像素值, I L ( c ) I_L(c) IL(c) 是窗口中心像素的值。 -
代价计算:
AD-Census 计算代价体,表示不同视差值对应的代价。通过计算左右图像的特征描述符之间的相似性,生成代价体。D ( p , d ) = ∑ ( i , j ) ∈ W ∣ C L ( i , j ) − C R ( i , j − d ) ∣ D(p, d) = \sum_{(i,j) \in W} |C_L(i,j) - C_R(i,j-d)| D(p,d)=(i,j)∈W∑∣CL(i,j)−CR(i,j−d)∣
其中, D ( p , d ) D(p, d) D(p,d) 是视差 d d d 下的代价, C L C_L CL 和 C R C_R CR 分别是左图和右图的特征描述符。 -
自适应性:
AD-Census 使用自适应窗口和加权特征来提高匹配精度。在不同的图像区域,窗口的大小和形状可能会有所不同,尤其在纹理较少或存在遮挡的地方。
部分传统算法代码实现:
import cv2
import numpy as np
import matplotlib.pyplot as plt
from scipy.ndimage import convolve
# 加载两张灰度图像
def load_images(left_path, right_path):
# 读取左图和右图
left_img = cv2.imread(left_path, cv2.IMREAD_GRAYSCALE)
right_img = cv2.imread(right_path, cv2.IMREAD_GRAYSCALE)
return left_img, right_img
# 计算基于 SAD 的视差图
def sad_matching(left_img, right_img, window_size, max_disparity):
h, w = left_img.shape
disparity = np.zeros((h, w), dtype=np.float32)
half_window = window_size // 2
# 遍历每个像素
for y in range(half_window, h - half_window):
for x in range(half_window, w - half_window):
best_disparity = 0
min_sad = float('inf')
# 提取左图的窗口
left_window = left_img[y-half_window:y+half_window+1, x-half_window:x+half_window+1]
# 遍历每个可能的视差
for d in range(max_disparity):
if x - d < half_window:
continue
# 提取右图的窗口
right_window = right_img[y-half_window:y+half_window+1, (x-d)-half_window:(x-d)+half_window+1]
# 计算 SAD
sad = np.sum(np.abs(left_window - right_window))
# 更新最小 SAD 和最佳视差
if sad < min_sad:
min_sad = sad
best_disparity = d
# 存储视差
disparity[y, x] = best_disparity
return disparity
# 计算基于 SSD 的视差图
def ssd_matching(left_img, right_img, window_size, max_disparity):
h, w = left_img.shape
disparity = np.zeros((h, w), dtype=np.float32)
half_window = window_size // 2
# 遍历每个像素
for y in range(half_window, h - half_window):
for x in range(half_window, w - half_window):
best_disparity = 0
min_ssd = float('inf')
# 提取左图的窗口
left_window = left_img[y-half_window:y+half_window+1, x-half_window:x+half_window+1]
# 遍历每个可能的视差
for d in range(max_disparity):
if x - d < half_window:
continue
# 提取右图的窗口
right_window = right_img[y-half_window:y+half_window+1, (x-d)-half_window:(x-d)+half_window+1]
# 计算 SSD
ssd = np.sum((left_window - right_window) ** 2)
# 更新最小 SSD 和最佳视差
if ssd < min_ssd:
min_ssd = ssd
best_disparity = d
# 存储视差
disparity[y, x] = best_disparity
return disparity
# 计算基于 NCC 的视差图
def ncc_matching(left_img, right_img, window_size, max_disparity):
h, w = left_img.shape
disparity = np.zeros((h, w), dtype=np.float32)
half_window = window_size // 2
# 遍历每个像素
for y in range(half_window, h - half_window):
for x in range(half_window, w - half_window):
best_disparity = 0
max_ncc = -float('inf')
# 提取左图的窗口
left_window = left_img[y-half_window:y+half_window+1, x-half_window:x+half_window+1]
# 对窗口进行归一化
left_window = (left_window - np.mean(left_window)) / (np.std(left_window) + 1e-6)
# 遍历每个可能的视差
for d in range(max_disparity):
if x - d < half_window:
continue
# 提取右图的窗口
right_window = right_img[y-half_window:y+half_window+1, (x-d)-half_window:(x-d)+half_window+1]
# 对窗口进行归一化
right_window = (right_window - np.mean(right_window)) / (np.std(right_window) + 1e-6)
# 计算 NCC
ncc = np.sum(left_window * right_window) / (window_size ** 2)
# 更新最大 NCC 和最佳视差
if ncc > max_ncc:
max_ncc = ncc
best_disparity = d
# 存储视差
disparity[y, x] = best_disparity
return disparity
# 计算基于 SGM 的视差图
def sgm(left_img, right_img, max_disparity, P1, P2):
h, w = left_img.shape
cost_volume = np.zeros((h, w, max_disparity), dtype=np.float32)
# 计算初始匹配成本
for y in range(h):
for x in range(w):
for d in range(max_disparity):
if x - d >= 0:
cost_volume[y, x, d] = abs(int(left_img[y, x]) - int(right_img[y, x-d]))
else:
cost_volume[y, x, d] = max_disparity
# 定义路径(这里简化为四个方向)
paths = [(0, 1), (1, 0), (0, -1), (-1, 0)]
# 聚合成本
aggregated_costs = np.zeros_like(cost_volume)
for dy, dx in paths:
y_range = range(h) if dy >= 0 else range(h-1, -1, -1)
x_range = range(w) if dx >= 0 else range(w-1, -1, -1)
# 遍历每个像素
for y in y_range:
for x in x_range:
if y - dy < 0 or y - dy >= h or x - dx < 0 or x - dx >= w:
aggregated_costs[y, x] += cost_volume[y, x]
else:
prev_costs = aggregated_costs[y-dy, x-dx]
for d in range(max_disparity):
costs = [
prev_costs[d],
prev_costs[max(0, d-1)] + P1,
prev_costs[min(max_disparity-1, d+1)] + P1,
np.min(prev_costs) + P2
]
aggregated_costs[y, x, d] += cost_volume[y, x, d] + min(costs) - min(prev_costs)
# 计算最终视差
disparity = np.argmin(aggregated_costs, axis=2)
return disparity
# 可视化视差图
def visualize_disparity(disparity, title):
plt.figure(figsize=(10, 5))
plt.imshow(disparity, cmap='jet')
plt.colorbar()
plt.title(title)
plt.show()
# 主函数
def main():
# 设置左右图像路径
left_img_path = 'path/to/left_image.png'
right_img_path = 'path/to/right_image.png'
# 加载图像
left_img, right_img = load_images(left_img_path, right_img_path)
window_size = 9
max_disparity = 64
# SAD 视差匹配
sad_disparity = sad_matching(left_img, right_img, window_size, max_disparity)
visualize_disparity(sad_disparity, 'SAD Disparity Map')
# SSD 视差匹配
ssd_disparity = ssd_matching(left_img, right_img, window_size, max_disparity)
visualize_disparity(ssd_disparity, 'SSD Disparity Map')
# NCC 视差匹配
ncc_disparity = ncc_matching(left_img, right_img, window_size, max_disparity)
visualize_disparity(ncc_disparity, 'NCC Disparity Map')
# SGM 视差匹配
P1, P2 = 10, 120
sgm_disparity = sgm(left_img, right_img, max_disparity, P1, P2)
visualize_disparity(sgm_disparity, 'SGM Disparity Map')
# 程序入口
if __name__ == '__main__':
main()
3.深度学习算法
3.1 基于CNN的方法
3.1.1 MC-CNN(Matching Cost Convolutional Neural Network)
MC-CNN(Matching Cost Convolutional Neural Network)是一种用于立体匹配的深度学习模型,旨在通过卷积神经网络(CNN)有效地计算匹配代价并提取特征。
-
原理:MC-CNN 通过利用卷积层提取左右图像的特征,将局部特征和上下文信息结合起来进行视差估计。该模型通过计算代价体,表示不同视差值对应的代价,并通过学习得到更为精准的匹配代价。
-
特征提取:
MC-CNN 首先通过多个卷积层提取左右图像的特征,生成特征图(feature maps)。特征提取过程可以表示为:
F L = CNN ( I L ) 和 F R = CNN ( I R ) F_L = \text{CNN}(I_L) \quad \text{和} \quad F_R = \text{CNN}(I_R) FL=CNN(IL)和FR=CNN(IR)
其中, F L F_L FL 和 F R F_R FR 分别是左图像和右图像的特征图, I L I_L IL 和 I R I_R IR 是输入的左右图像。 -
匹配代价计算:
MC-CNN 计算代价体(cost volume),用于表示不同视差值对应的代价。代价体的生成通常基于左右图像的特征图,通过计算绝对差、平方差或其他度量来得到。D ( p , d ) = ∑ ( i , j ) ∈ W ∣ F L ( i , j ) − F R ( i , j − d ) ∣ D(p, d) = \sum_{(i,j) \in W} |F_L(i,j) - F_R(i,j-d)| D(p,d)=(i,j)∈W∑∣FL(i,j)−FR(i,j−d)∣
其中, D ( p , d ) D(p, d) D(p,d) 是视差 d d d 下的代价, F L F_L FL 和 F R F_R FR 是左右图像的特征图。 -
损失函数:
MC-CNN 通常使用多种损失函数来优化模型,包括视差损失(如 L1 损失或 L2 损失)和光滑损失,以鼓励相邻像素之间的视差差异较小。
import torch
import torch.nn as nn
import numpy as np
class MCCNN(nn.Module):
def __init__(self,
input_channels=1, # 输入通道数:灰度图为1,RGB图为3
input_patch_size=11, # 输入patch大小
num_conv_layers=5, # 卷积层数量
num_conv_feature_maps=64, # 每个卷积层的特征图数量
conv_kernel_size=3, # 卷积核大小
batch_size=128): # 批量大小
super(MCCNN, self).__init__()
self.input_channels = input_channels
self.input_patch_size = input_patch_size
self.num_conv_layers = num_conv_layers
self.num_conv_feature_maps = num_conv_feature_maps
self.conv_kernel_size = conv_kernel_size
self.batch_size = batch_size
# 创建卷积层序列
self.conv_layers = nn.ModuleList()
# 第一个卷积层
self.conv_layers.append(
nn.Conv2d(input_channels, num_conv_feature_maps,
kernel_size=conv_kernel_size, padding=0)
)
# 中间的卷积层
for _ in range(num_conv_layers - 2):
self.conv_layers.append(
nn.Conv2d(num_conv_feature_maps, num_conv_feature_maps,
kernel_size=conv_kernel_size, padding=0)
)
# 最后一个卷积层(不使用ReLU激活)
self.conv_layers.append(
nn.Conv2d(num_conv_feature_maps, num_conv_feature_maps,
kernel_size=conv_kernel_size, padding=0)
)
# ReLU激活函数
self.relu = nn.ReLU(inplace=True)
def forward(self, x):
# 前向传播
for i, conv in enumerate(self.conv_layers):
x = conv(x)
# 除最后一层外,其他层都使用ReLU激活
if i < len(self.conv_layers) - 1:
x = self.relu(x)
# L2 normalization
x = nn.functional.normalize(x, p=2, dim=1)
return x
def load_weights(self, weights_path):
# 加载预训练权重
weights_dict = np.load(weights_path, allow_pickle=True).item()
# 将numpy数组转换为PyTorch张量并加载到模型中
for name, param in self.named_parameters():
if name in weights_dict:
param.data = torch.from_numpy(weights_dict[name])
print(f"权重已从 {weights_path} 加载")
def save_weights(self, file_name='pretrain.pth'):
# 保存模型权重
torch.save(self.state_dict(), file_name)
print(f"权重已保存到文件 {file_name}")
if __name__ == "__main__":
# 创建一个示例输入张量
x = torch.randn(128, 1, 11, 11) # [batch_size, channels, height, width]
# 实例化网络
net = MCCNN(input_channels=1)
# 前向传播
output = net(x)
print(f"输入形状: {x.shape}")
print(f"输出形状: {output.shape}")
# 打印网络结构
print(net)
3.1.2 GC-Net(Geometry and Context Network)
GC-Net(Geometry and Context Network)是一种用于立体匹配的深度学习模型,旨在有效地结合几何信息和上下文信息,以提高深度图的精度。GC-Net 通过卷积神经网络(CNN)提取特征,并使用全局上下文来优化视差估计。
-
原理:GC-Net 通过计算代价体,结合几何结构和上下文信息,以最小化代价函数来生成视差图。其核心思想是利用几何信息来建模视差的结构,同时通过上下文信息来增强匹配的准确性。
-
特征提取:
GC-Net 首先使用卷积神经网络提取左右图像的特征。特征提取过程可以表示为:
F L = CNN ( I L ) 和 F R = CNN ( I R ) F_L = \text{CNN}(I_L) \quad \text{和} \quad F_R = \text{CNN}(I_R) FL=CNN(IL)和FR=CNN(IR)
其中, F L F_L FL 和 F R F_R FR 分别是左图像和右图像的特征图, I L I_L IL 和 I R I_R IR 是输入的左右图像。 -
代价计算:
GC-Net 计算代价体(cost volume),表示不同视差值对应的代价。代价体的生成通常基于左右图像的特征图,通过计算绝对差等度量来得到。C ( p , d ) = D ( p , d ) + ∑ q ∈ P ( p ) cost ( q , d ) C(p, d) = D(p, d) + \sum_{q \in P(p)} \text{cost}(q, d) C(p,d)=D(p,d)+q∈P(p)∑cost(q,d)
其中, C ( p , d ) C(p, d) C(p,d) 是聚合代价, D ( p , d ) D(p, d) D(p,d) 是初始代价, P ( p ) P(p) P(p) 是从像素 p p p 出发的路径集合, cost ( q , d ) \text{cost}(q, d) cost(q,d) 是路径上的代价。 -
几何信息与上下文信息:
GC-Net 强调几何信息的重要性,通过使用视差平面(disparity planes)来建模视差的几何结构。此外,GC-Net 引入上下文模块(context module),通过卷积层提取更高层次的上下文特征,帮助网络更好地理解图像内容。
import torch
import torch.nn as nn
import torch.nn.functional as F
class BasicBlock(nn.Module): #basic block for Conv2d
def __init__(self,in_planes,planes,stride=1):
super(BasicBlock,self).__init__()
self.conv1=nn.Conv2d(in_planes,planes,kernel_size=3,stride=stride,padding=1)
self.bn1=nn.BatchNorm2d(planes)
self.conv2=nn.Conv2d(planes,planes,kernel_size=3,stride=1,padding=1)
self.bn2=nn.BatchNorm2d(planes)
self.shortcut=nn.Sequential()
def forward(self, x):
out=F.relu(self.bn1(self.conv1(x)))
out=self.bn2(self.conv2(out))
out+=self.shortcut(x)
out=F.relu(out)
return out
class ThreeDConv(nn.Module):
def __init__(self,in_planes,planes,stride=1):
super(ThreeDConv, self).__init__()
self.conv1 = nn.Conv3d(in_planes, planes, kernel_size=3, stride=stride, padding=1)
self.bn1 = nn.BatchNorm3d(planes)
self.conv2 = nn.Conv3d(planes, planes, kernel_size=3, stride=1, padding=1)
self.bn2 = nn.BatchNorm3d(planes)
self.conv3=nn.Conv3d(planes,planes,kernel_size=3,stride=1,padding=1)
self.bn3=nn.BatchNorm3d(planes)
def forward(self, x):
out=F.relu(self.bn1(self.conv1(x)))
out=F.relu(self.bn2(self.conv2(out)))
out=F.relu(self.bn3(self.conv3(out)))
return out
class GC_NET(nn.Module):
def __init__(self,block,block_3d,num_block,height,width,maxdisp):
super(GC_NET, self).__init__()
self.height=height
self.width=width
self.maxdisp=int(maxdisp/2)
self.in_planes=32
#first two conv2d
self.conv0=nn.Conv2d(3,32,5,2,2)
self.bn0=nn.BatchNorm2d(32)
#res block
self.res_block=self._make_layer(block,self.in_planes,32,num_block[0],stride=1)
#last conv2d
self.conv1=nn.Conv2d(32,32,3,1,1)
#self.bn1=nn.BatchNorm2d(32) #not sure this layer needs bn or relu
#conv3d
self.conv3d_1=nn.Conv3d(64,32,3,1,1)
self.bn3d_1=nn.BatchNorm3d(32)
self.conv3d_2=nn.Conv3d(32,32,3,1,1)
self.bn3d_2=nn.BatchNorm3d(32)
self.conv3d_3=nn.Conv3d(64,64,3,2,1)
self.bn3d_3=nn.BatchNorm3d(64)
self.conv3d_4=nn.Conv3d(64,64,3,2,1)
self.bn3d_4=nn.BatchNorm3d(64)
self.conv3d_5=nn.Conv3d(64,64,3,2,1)
self.bn3d_5=nn.BatchNorm3d(64)
#conv3d sub_sample block
self.block_3d_1 = self._make_layer(block_3d,64,64,num_block[1],stride=2)
self.block_3d_2 = self._make_layer(block_3d, 64, 64, num_block[1], stride=2)
self.block_3d_3 = self._make_layer(block_3d, 64, 64, num_block[1], stride=2)
self.block_3d_4 = self._make_layer(block_3d, 64, 128, num_block[1], stride=2)
#deconv3d
self.deconv1=nn.ConvTranspose3d(128,64,3,2,1,1)
self.debn1=nn.BatchNorm3d(64)
self.deconv2 = nn.ConvTranspose3d(64, 64, 3, 2, 1, 1)
self.debn2 = nn.BatchNorm3d(64)
self.deconv3 = nn.ConvTranspose3d(64, 64, 3, 2, 1, 1)
self.debn3 = nn.BatchNorm3d(64)
self.deconv4 = nn.ConvTranspose3d(64, 32, 3, 2, 1, 1)
self.debn4 = nn.BatchNorm3d(32)
#last deconv3d
self.deconv5 = nn.ConvTranspose3d(32, 1, 3, 2, 1, 1)
#self.debn5=nn.BatchNorm3d(1) #not sure
def forward(self, imgLeft,imgRight):
imgl0=F.relu(self.bn0(self.conv0(imgLeft)))
imgr0=F.relu(self.bn0(self.conv0(imgRight)))
imgl_block=self.res_block(imgl0)
imgr_block=self.res_block(imgr0)
imgl1=self.conv1(imgl_block)
imgr1=self.conv1(imgr_block)
# cost volume
cost_volum = self.cost_volume(imgl1,imgr1)
conv3d_out=F.relu(self.bn3d_1(self.conv3d_1(cost_volum)))
conv3d_out=F.relu(self.bn3d_2(self.conv3d_2(conv3d_out)))
#conv3d block
conv3d_block_1=self.block_3d_1(cost_volum)
conv3d_21=F.relu(self.bn3d_3(self.conv3d_3(cost_volum)))
conv3d_block_2=self.block_3d_2(conv3d_21)
conv3d_24=F.relu(self.bn3d_4(self.conv3d_4(conv3d_21)))
conv3d_block_3=self.block_3d_3(conv3d_24)
conv3d_27=F.relu(self.bn3d_5(self.conv3d_5(conv3d_24)))
conv3d_block_4=self.block_3d_4(conv3d_27)
#deconv
deconv3d=F.relu(self.debn1(self.deconv1(conv3d_block_4))+conv3d_block_3)
deconv3d=F.relu(self.debn2(self.deconv2(deconv3d))+conv3d_block_2)
deconv3d=F.relu(self.debn3(self.deconv3(deconv3d))+conv3d_block_1)
deconv3d=F.relu(self.debn4(self.deconv4(deconv3d))+conv3d_out)
#last deconv3d
deconv3d=self.deconv5(deconv3d)
out=deconv3d.view(1, self.maxdisp*2, self.height, self.width)
prob=F.softmax(-out,1)
return prob
def _make_layer(self,block,in_planes,planes,num_block,stride):
strides=[stride]+[1]*(num_block-1)
layers=[]
for step in strides:
layers.append(block(in_planes,planes,step))
return nn.Sequential(*layers)
def cost_volume(self,imgl,imgr):
xx_list = []
pad_opr1 = nn.ZeroPad2d((0, self.maxdisp, 0, 0))
xleft = pad_opr1(imgl)
for d in range(self.maxdisp): # maxdisp+1 ?
pad_opr2 = nn.ZeroPad2d((d, self.maxdisp - d, 0, 0))
xright = pad_opr2(imgr)
xx_temp = torch.cat((xleft, xright), 1)
xx_list.append(xx_temp)
xx = torch.cat(xx_list, 1)
xx = xx.view(1, self.maxdisp, 64, int(self.height / 2), int(self.width / 2) + self.maxdisp)
xx0=xx.permute(0,2,1,3,4)
xx0 = xx0[:, :, :, :, :int(self.width / 2)]
return xx0
def loss(xx,loss_mul,gt):
loss=torch.sum(torch.sqrt(torch.pow(torch.sum(xx.mul(loss_mul),1)-gt,2)+0.00000001)/256/(256+128))
return loss
def GcNet(height,width,maxdisp):
return GC_NET(BasicBlock,ThreeDConv,[8,1],height,width,maxdisp)
3.1.3 PSMNet(Pyramid Stereo Matching Network)
-
原理:PSMNet 通过构建多尺度特征金字塔,结合三维卷积网络来处理代价体,进而生成精确的视差图。该模型能够有效捕捉图像的多尺度信息,并通过代价聚合来优化视差估计。
-
特征提取:
PSMNet 首先使用卷积神经网络提取左右图像的特征,并构建多尺度特征金字塔。特征提取过程可以表示为:
F L = CNN ( I L ) 和 F R = CNN ( I R ) F_L = \text{CNN}(I_L) \quad \text{和} \quad F_R = \text{CNN}(I_R) FL=CNN(IL)和FR=CNN(IR)
其中, F L F_L FL 和 F R F_R FR 分别是左图像和右图像的特征图, I L I_L IL 和 I R I_R IR 是输入的左右图像。 -
代价体构建:
PSMNet 计算代价体(cost volume),表示不同视差值对应的代价。代价体的生成通常基于左右图像的特征图,通过三维卷积来计算不同视差下的代价。C ( p , d ) = D ( p , d ) + ∑ ( i , j ) ∈ W ∣ F L ( i , j ) − F R ( i , j − d ) ∣ C(p, d) = D(p, d) + \sum_{(i,j) \in W} |F_L(i,j) - F_R(i,j-d)| C(p,d)=D(p,d)+(i,j)∈W∑∣FL(i,j)−FR(i,j−d)∣
其中, C ( p , d ) C(p, d) C(p,d) 是视差 d d d 下的代价, D ( p , d ) D(p, d) D(p,d) 是初始代价, F L F_L FL 和 F R F_R FR 是左右图像的特征图。 -
三维卷积:
PSMNet 使用三维卷积网络对代价体进行处理,从而有效地聚合代价信息。这种结构能够捕捉到视差、空间和上下文信息之间的关系,增强匹配的准确性。 -
损失函数:
PSMNet 通常使用多种损失函数来优化模型,包括:- 视差损失:计算预测视差与真实视差之间的差异,常用的损失函数包括 L1 损失或 L2 损失。
- 平滑损失:为了避免视差图中的噪声,PSMNet 可以引入平滑损失,鼓励相邻像素之间的视差差异较小。
-
StackedHourglass模块:
import torch
import torch.nn as nn
import torch.nn.functional as F
class StackedHourglass(nn.Module):
'''
inputs --- [B, 64, 1/4D, 1/4H, 1/4W]
'''
def __init__(self, max_disp):
super().__init__()
self.conv0 = nn.Sequential(
Conv3dBn(in_channels=64, out_channels=32, kernel_size=3, stride=1, padding=1, dilation=1, use_relu=True),
Conv3dBn(in_channels=32, out_channels=32, kernel_size=3, stride=1, padding=1, dilation=1, use_relu=True)
)
self.conv1 = nn.Sequential(
Conv3dBn(in_channels=32, out_channels=32, kernel_size=3, stride=1, padding=1, dilation=1, use_relu=True),
Conv3dBn(in_channels=32, out_channels=32, kernel_size=3, stride=1, padding=1, dilation=1, use_relu=False)
)
self.hourglass1 = Hourglass()
self.hourglass2 = Hourglass()
self.hourglass3 = Hourglass()
self.out1 = nn.Sequential(
Conv3dBn(in_channels=32, out_channels=32, kernel_size=3, stride=1, padding=1, dilation=1, use_relu=True),
nn.Conv3d(in_channels=32, out_channels=1, kernel_size=3, stride=1, padding=1, dilation=1, bias=False)
)
self.out2 = nn.Sequential(
Conv3dBn(in_channels=32, out_channels=32, kernel_size=3, stride=1, padding=1, dilation=1, use_relu=True),
nn.Conv3d(in_channels=32, out_channels=1, kernel_size=3, stride=1, padding=1, dilation=1, bias=False)
)
self.out3 = nn.Sequential(
Conv3dBn(in_channels=32, out_channels=32, kernel_size=3, stride=1, padding=1, dilation=1, use_relu=True),
nn.Conv3d(in_channels=32, out_channels=1, kernel_size=3, stride=1, padding=1, dilation=1, bias=False)
)
self.regression = DisparityRegression(max_disp)
def forward(self, inputs, out_size):
conv0_out = self.conv0(inputs) # [B, 32, 1/4D, 1/4H, 1/4W]
conv1_out = self.conv1(conv0_out)
conv1_out = conv0_out + conv1_out # [B, 32, 1/4D, 1/4H, 1/4W]
hourglass1_out1, hourglass1_out3, hourglass1_out4 = self.hourglass1(conv1_out, scale1=None, scale2=None, scale3=conv1_out)
hourglass2_out1, hourglass2_out3, hourglass2_out4 = self.hourglass2(hourglass1_out4, scale1=hourglass1_out3, scale2=hourglass1_out1, scale3=conv1_out)
hourglass3_out1, hourglass3_out3, hourglass3_out4 = self.hourglass3(hourglass2_out4, scale1=hourglass2_out3, scale2=hourglass1_out1, scale3=conv1_out)
out1 = self.out1(hourglass1_out4) # [B, 1, 1/4D, 1/4H, 1/4W]
out2 = self.out2(hourglass2_out4) + out1
out3 = self.out3(hourglass3_out4) + out2
cost1 = F.upsample(out1, size=out_size, mode='trilinear').squeeze(dim=1) # [B, D, H, W]
cost2 = F.upsample(out2, size=out_size, mode='trilinear').squeeze(dim=1) # [B, D, H, W]
cost3 = F.upsample(out3, size=out_size, mode='trilinear').squeeze(dim=1) # [B, D, H, W]
prob1 = F.softmax(-cost1, dim=1) # [B, D, H, W]
prob2 = F.softmax(-cost2, dim=1)
prob3 = F.softmax(-cost3, dim=1)
disp1 = self.regression(prob1)
disp2 = self.regression(prob2)
disp3 = self.regression(prob3)
return disp1, disp2, disp3
class DisparityRegression(nn.Module):
def __init__(self, max_disp):
super().__init__()
self.disp_score = torch.range(0, max_disp - 1) # [D]
self.disp_score = self.disp_score.unsqueeze(0).unsqueeze(2).unsqueeze(3) # [1, D, 1, 1]
def forward(self, prob):
disp_score = self.disp_score.expand_as(prob).type_as(prob) # [B, D, H, W]
out = torch.sum(disp_score * prob, dim=1) # [B, H, W]
return out
class Hourglass(nn.Module):
def __init__(self):
super().__init__()
self.net1 = nn.Sequential(
Conv3dBn(in_channels=32, out_channels=64, kernel_size=3, stride=2, padding=1, dilation=1, use_relu=True),
Conv3dBn(in_channels=64, out_channels=64, kernel_size=3, stride=1, padding=1, dilation=1, use_relu=False)
)
self.net2 = nn.Sequential(
Conv3dBn(in_channels=64, out_channels=64, kernel_size=3, stride=2, padding=1, dilation=1, use_relu=True),
Conv3dBn(in_channels=64, out_channels=64, kernel_size=3, stride=1, padding=1, dilation=1, use_relu=True)
)
self.net3 = nn.Sequential(
nn.ConvTranspose3d(in_channels=64, out_channels=64, kernel_size=3, stride=2, padding=1, output_padding=1, bias=False),
nn.BatchNorm3d(num_features=64)
# nn.ReLU(inplace=True)
)
self.net4 = nn.Sequential(
nn.ConvTranspose3d(in_channels=64, out_channels=32, kernel_size=3, stride=2, padding=1, output_padding=1, bias=False),
nn.BatchNorm3d(num_features=32)
)
def forward(self, inputs, scale1=None, scale2=None, scale3=None):
net1_out = self.net1(inputs) # [B, 64, 1/8D, 1/8H, 1/8W]
if scale1 is not None:
net1_out = F.relu(net1_out + scale1, inplace=True)
else:
net1_out = F.relu(net1_out, inplace=True)
net2_out = self.net2(net1_out) # [B, 64, 1/16D, 1/16H, 1/16W]
net3_out = self.net3(net2_out) # [B, 64, 1/8D, 1/8H, 1/8W]
if scale2 is not None:
net3_out = F.relu(net3_out + scale2, inplace=True)
else:
net3_out = F.relu(net3_out + net1_out, inplace=True)
net4_out = self.net4(net3_out)
if scale3 is not None:
net4_out = net4_out + scale3
return net1_out, net3_out, net4_out
class Conv3dBn(nn.Module):
def __init__(self, in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, use_relu=True):
super().__init__()
net = [nn.Conv3d(in_channels, out_channels, kernel_size, stride, padding, dilation, bias=False),
nn.BatchNorm3d(out_channels)]
if use_relu:
net.append(nn.ReLU(inplace=True))
self.net = nn.Sequential(*net)
def forward(self, inputs):
out = self.net(inputs)
return out
- CostNet模块:
import torch
import torch.nn as nn
import torch.nn.functional as F
class CostNet(nn.Module):
def __init__(self):
super().__init__()
self.cnn = CNN()
self.spp = SPP()
self.fusion = nn.Sequential(
Conv2dBn(in_channels=320, out_channels=128, kernel_size=3, stride=1, padding=1, use_relu=True),
nn.Conv2d(in_channels=128, out_channels=32, kernel_size=1, stride=1, padding=0, bias=False)
)
def forward(self, inputs):
conv2_out, conv4_out = self.cnn(inputs) # [B, 64, 1/4H, 1/4W], [B, 128, 1/4H, 1/4W]
spp_out = self.spp(conv4_out) # [B, 128, 1/4H, 1/4W]
out = torch.cat([conv2_out, conv4_out, spp_out], dim=1) # [B, 320, 1/4H, 1/4W]
out = self.fusion(out) # [B, 32, 1/4H, 1/4W]
return out
class SPP(nn.Module):
def __init__(self):
super().__init__()
self.branch1 = self.__make_branch(kernel_size=64, stride=64)
self.branch2 = self.__make_branch(kernel_size=32, stride=32)
self.branch3 = self.__make_branch(kernel_size=16, stride=16)
self.branch4 = self.__make_branch(kernel_size=8, stride=8)
def forward(self, inputs):
out_size = inputs.size(2), inputs.size(3)
branch1_out = F.upsample(self.branch1(inputs), size=out_size, mode='bilinear') # [B, 32, 1/4H, 1/4W]
# print('branch1_out')
# print(branch1_out[0, 0, :3, :3])
branch2_out = F.upsample(self.branch2(inputs), size=out_size, mode='bilinear') # [B, 32, 1/4H, 1/4W]
branch3_out = F.upsample(self.branch3(inputs), size=out_size, mode='bilinear') # [B, 32, 1/4H, 1/4W]
branch4_out = F.upsample(self.branch4(inputs), size=out_size, mode='bilinear') # [B, 32, 1/4H, 1/4W]
out = torch.cat([branch4_out, branch3_out, branch2_out, branch1_out], dim=1) # [B, 128, 1/4H, 1/4W]
return out
@staticmethod
def __make_branch(kernel_size, stride):
branch = nn.Sequential(
nn.AvgPool2d(kernel_size, stride),
Conv2dBn(in_channels=128, out_channels=32, kernel_size=3, stride=1, padding=1, use_relu=True) # kernel size maybe 1
)
return branch
class CNN(nn.Module):
def __init__(self):
super().__init__()
self.conv0 = nn.Sequential(
Conv2dBn(in_channels=3, out_channels=32, kernel_size=3, stride=2, padding=1, use_relu=True), # downsample
Conv2dBn(in_channels=32, out_channels=32, kernel_size=3, stride=1, padding=1, use_relu=True),
Conv2dBn(in_channels=32, out_channels=32, kernel_size=3, stride=1, padding=1, use_relu=True)
)
self.conv1 = StackedBlocks(n_blocks=3, in_channels=32, out_channels=32, kernel_size=3, stride=1, padding=1, dilation=1)
self.conv2 = StackedBlocks(n_blocks=16, in_channels=32, out_channels=64, kernel_size=3, stride=2, padding=1, dilation=1) # downsample
self.conv3 = StackedBlocks(n_blocks=3, in_channels=64, out_channels=128, kernel_size=3, stride=1, padding=2, dilation=2) # dilated
self.conv4 = StackedBlocks(n_blocks=3, in_channels=128, out_channels=128, kernel_size=3, stride=1, padding=4, dilation=4) # dilated
def forward(self, inputs):
conv0_out = self.conv0(inputs)
conv1_out = self.conv1(conv0_out) # [B, 32, 1/2H, 1/2W]
conv2_out = self.conv2(conv1_out) # [B, 64, 1/4H, 1/4W]
conv3_out = self.conv3(conv2_out) # [B, 128, 1/4H, 1/4W]
conv4_out = self.conv4(conv3_out) # [B, 128, 1/4H, 1/4W]
return conv2_out, conv4_out
class StackedBlocks(nn.Module):
def __init__(self, n_blocks, in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1):
super().__init__()
if stride == 1 and in_channels == out_channels:
downsample = False
else:
downsample = True
net = [ResidualBlock(in_channels, out_channels, kernel_size, stride, padding, dilation, downsample)]
for i in range(n_blocks - 1):
net.append(ResidualBlock(out_channels, out_channels, kernel_size, 1, padding, dilation, downsample=False))
self.net = nn.Sequential(*net)
def forward(self, inputs):
out = self.net(inputs)
return out
class ResidualBlock(nn.Module):
def __init__(self, in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, downsample=False):
super().__init__()
self.net = nn.Sequential(
Conv2dBn(in_channels, out_channels, kernel_size, stride, padding, dilation, use_relu=True),
Conv2dBn(out_channels, out_channels, kernel_size, 1, padding, dilation, use_relu=False)
)
self.downsample = None
if downsample:
self.downsample = Conv2dBn(in_channels, out_channels, 1, stride, use_relu=False)
def forward(self, inputs):
out = self.net(inputs)
if self.downsample:
inputs = self.downsample(inputs)
out = out + inputs
return out
class Conv2dBn(nn.Module):
def __init__(self, in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, use_relu=True):
super().__init__()
net = [nn.Conv2d(in_channels, out_channels, kernel_size, stride, padding, dilation, bias=False),
nn.BatchNorm2d(out_channels)]
if use_relu:
net.append(nn.ReLU(inplace=True))
self.net = nn.Sequential(*net)
def forward(self, inputs):
out = self.net(inputs)
return out
- 完整PSM-Net模块:
import math
import torch
import torch.nn as nn
from models.costnet import CostNet
from models.stackedhourglass import StackedHourglass
class PSMNet(nn.Module):
def __init__(self, max_disp):
super().__init__()
self.cost_net = CostNet()
self.stackedhourglass = StackedHourglass(max_disp)
self.D = max_disp
self.__init_params()
def forward(self, left_img, right_img):
original_size = [self.D, left_img.size(2), left_img.size(3)]
left_cost = self.cost_net(left_img) # [B, 32, 1/4H, 1/4W]
right_cost = self.cost_net(right_img) # [B, 32, 1/4H, 1/4W]
# cost = torch.cat([left_cost, right_cost], dim=1) # [B, 64, 1/4H, 1/4W]
# B, C, H, W = cost.size()
# print('left_cost')
# print(left_cost[0, 0, :3, :3])
B, C, H, W = left_cost.size()
cost_volume = torch.zeros(B, C * 2, self.D // 4, H, W).type_as(left_cost) # [B, 64, D, 1/4H, 1/4W]
# for i in range(self.D // 4):
# cost_volume[:, :, i, :, i:] = cost[:, :, :, i:]
for i in range(self.D // 4):
if i > 0:
cost_volume[:, :C, i, :, i:] = left_cost[:, :, :, i:]
cost_volume[:, C:, i, :, i:] = right_cost[:, :, :, :-i]
else:
cost_volume[:, :C, i, :, :] = left_cost
cost_volume[:, C:, i, :, :] = right_cost
disp1, disp2, disp3 = self.stackedhourglass(cost_volume, out_size=original_size)
return disp1, disp2, disp3
def __init_params(self):
for m in self.modules():
if isinstance(m, nn.Conv2d):
n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels
m.weight.data.normal_(0, math.sqrt(2. / n))
elif isinstance(m, nn.Conv3d):
n = m.kernel_size[0] * m.kernel_size[1] * m.kernel_size[2] * m.out_channels
m.weight.data.normal_(0, math.sqrt(2. / n))
elif isinstance(m, nn.BatchNorm2d):
m.weight.data.fill_(1)
m.bias.data.zero_()
elif isinstance(m, nn.BatchNorm3d):
m.weight.data.fill_(1)
m.bias.data.zero_()
elif isinstance(m, nn.Linear):
m.bias.data.zero_()
3.2 基于GAN的方法
StereoGAN
StereoGAN(Stereo Generative Adversarial Network)是一种用于立体匹配的生成对抗网络(GAN)模型,旨在通过生成和判别网络的对抗训练来提高视差估计的精度。StereoGAN 结合了生成模型和判别模型的优点,能够在复杂场景下生成高质量的视差图。
-
原理:StereoGAN 通过对抗训练的方式,利用生成网络生成视差图,并使用判别网络评估生成的视差图的真实性。通过这种对抗过程,生成网络不断优化,生成的视差图质量逐渐提高。
-
网络结构:
StereoGAN 通常包含两个主要部分:生成器(Generator)和判别器(Discriminator)。-
生成器:生成器接收左右图像的特征并生成视差图。其结构通常包括多个卷积层和上采样层,以逐步恢复图像的空间分辨率。
-
判别器:判别器用于区分生成的视差图和真实的视差图。它通常采用卷积神经网络结构,输出一个二元分类结果,表示输入的视差图是真实的还是生成的。
-
-
对抗损失:StereoGAN 使用对抗损失来优化生成器和判别器。生成器的目标是最大化判别器的错误率,而判别器的目标是最小化其错误率。
-
生成器损失:
L G = − E [ log ( D ( G ( I L , I R ) ) ] L_G = -\mathbb{E}[\log(D(G(I_L, I_R))] LG=−E[log(D(G(IL,IR))]
其中, D D D 是判别器, G G G 是生成器, I L I_L IL 和 I R I_R IR 是左右图像。 -
判别器损失:
L D = − E [ log ( D ( I t r u e ) ) ] − E [ log ( 1 − D ( G ( I L , I R ) ) ) ] L_D = -\mathbb{E}[\log(D(I_{true}))] - \mathbb{E}[\log(1 - D(G(I_L, I_R)))] LD=−E[log(D(Itrue))]−E[log(1−D(G(IL,IR)))]
其中, I t r u e I_{true} Itrue 是真实的视差图。
-
3.3 基于transformer的方法
STTR(Stereo Transformer)
STTR(Stereo Transformer)是一种基于 Transformer 架构的立体匹配模型,旨在通过自注意力机制有效捕捉图像之间的全局和局部特征,从而提高视差估计的精度。STTR 将传统的卷积操作替换为 Transformer 的自注意力机制,以实现更灵活的特征建模。
-
网络结构:
STTR 的基本结构包括以下几个主要组成部分:-
卷积层:STTR 首先使用卷积神经网络提取左右图像的初步特征。特征提取过程可以表示为:
F L = CNN ( I L ) 和 F R = CNN ( I R ) F_L = \text{CNN}(I_L) \quad \text{和} \quad F_R = \text{CNN}(I_R) FL=CNN(IL)和FR=CNN(IR)
其中, F L F_L FL 和 F R F_R FR 分别是左图像和右图像的特征图, I L I_L IL 和 I R I_R IR 是输入的左右图像。 -
自注意力:在特征图生成后,STTR 使用自注意力机制来捕捉图像中不同像素之间的关系。自注意力层的计算公式为:
Attention ( Q , K , V ) = softmax ( Q K T d k ) V \text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V Attention(Q,K,V)=softmax(dkQKT)V
其中, Q Q Q、 K K K 和 V V V 分别是查询、键和值的矩阵表示, d k d_k dk 是键的维度。 -
代价体(Cost Volume):STTR 计算代价体,通过自注意力机制聚合不同视差值的特征信息。代价体的生成通常基于左右图像的特征图,结合自注意力机制来优化代价计算。
-
-
损失函数:
STTR 通常使用多种损失函数来优化模型,包括:- 视差损失:计算预测视差与真实视差之间的差异,常用的损失函数包括 L1 损失或 L2 损失。
- 平滑损失:为了避免视差图中的噪声,STTR 可以引入平滑损失,鼓励相邻像素之间的视差差异较小。
4.总结
立体匹配是计算机视觉中的一个重要任务,涉及从一对图像中估计每个像素的视差。不同的算法在实现原理、精度、计算效率和适用场景等方面各有特点。以下是对几种常见立体匹配算法的总结:
4.1 局部算法
局部算法主要基于窗口匹配的思想,通过计算左右图像中对应窗口的相似度进行匹配。
- SAD(Sum of Absolute Differences):计算左右图像对应窗口像素差的绝对值之和,值越小表示匹配度越高。
- SSD(Sum of Squared Differences):计算左右图像对应窗口像素差的平方和,同样值越小表示匹配度越高。
- NCC(Normalized Cross-Correlation):计算左右图像对应窗口的归一化互相关,值越大表示匹配度越高。
4.2 全局算法
全局算法通常将立体匹配问题建模为能量最小化问题,考虑整个图像的一致性。
- 图割算法(Graph Cuts):将立体匹配问题转化为图论中的最小割问题,通过最小化能量函数来获得最优视差图。
- 信念传播算法(Belief Propagation):通过迭代地在图像的像素网格上传递"信念"来优化能量函数,最终得到全局最优的视差图。
4.3 半全局算法
半全局算法试图在局部算法的效率和全局算法的准确性之间取得平衡。
- SGM(Semi-Global Matching):在多个方向上进行一维的路径代价聚合,然后将这些路径的代价进行加和,选择具有最小聚合代价的视差值。
- AD-Census(Adaptive Census):结合局部特征和自适应性,通过计算特征描述符和代价体来进行匹配,提高视差估计的精度。
4.4 深度学习算法
- MC-CNN(Matching Cost Convolutional Neural Network):利用卷积神经网络提取特征并计算匹配代价,通过对抗训练优化视差估计。
- GC-Net(Geometry and Context Network):结合几何信息和上下文信息,通过计算代价体来生成视差图。
- PSMNet(Pyramid Stereo Matching Network):通过金字塔特征提取和三维卷积网络提高视差估计的准确性。
- StereoGAN(Stereo Generative Adversarial Network):通过生成对抗网络生成高质量的视差图。
- STTR(Stereo Transformer):利用自注意力机制捕捉全局特征,提高立体匹配的精度。
4.5 如何选择立体匹配算法
选择立体匹配算法时,应考虑以下几个因素:
-
应用场景:
- 如果应用场景较为简单且计算资源有限,可以选择局部算法(如 SAD、SSD)。
- 对于复杂场景和高精度要求的应用,建议使用全局算法(如图割、信念传播)或深度学习算法(如 MC-CNN、GC-Net)。
-
数据特性:
- 如果数据中有很多纹理和结构,局部算法可能会表现良好。
- 在存在遮挡或纹理较少的情况下,全局算法或深度学习方法通常能提供更好的性能。
-
计算资源:
- 深度学习算法通常需要更多的计算资源和数据进行训练,因此在资源有限的情况下,可能需要选择传统算法。
- 如果具备充足的计算资源和标注数据,深度学习方法可以提供更高的精度和鲁棒性。
-
实时性要求:
- 如果应用需要实时处理,可以考虑计算效率较高的局部算法。
- 深度学习算法虽然精度高,但在推理时可能会相对较慢。
-
模型复杂性:
- 对于需要快速原型开发的项目,简单的局部算法实现较为方便。
- 深度学习方法需要更多的模型设计和训练过程,适合于长期项目。
通过综合考虑这些因素,可以选择出最适合特定应用场景的立体匹配算法,以满足精度、效率和资源的需求。
5.参考文献
[1] Scharstein, D., & Szeliski, R. (2002). A Taxonomy and Evaluation of Dense Two-Frame Stereo Correspondence Algorithms. *International Journal of Computer Vision*, 47(1), 7-42.
[2] Hirschmüller, H. (2008). Stereo Processing by Semi-Global Matching and Mutual Information. *IEEE Transactions on Pattern Analysis and Machine Intelligence*, 30(2), 328-341.
[3] Bleyer, M., Zabulis, X., & Pitas, I. (2011). A Generalized Framework for Stereo Matching with a Multi-Layer Cost Function. *IEEE Transactions on Image Processing*, 20(5), 1469-1480.
[4] K. He, G. Gkioxari, P. Dollar, & R. Girshick (2017). Mask R-CNN. In *Proceedings of the IEEE International Conference on Computer Vision* (ICCV), 2961-2969.
[5] Zhang, Y., & Ghanem, B. (2019). AD-Census: Adaptive Census for Stereo Matching. *IEEE Transactions on Image Processing*, 28(1), 212-225.
[6] K. Chen, Y. Yang, H. Yang, & J. Wang (2018). MC-CNN: A Convolutional Neural Network for Stereo Matching. In *Proceedings of the European Conference on Computer Vision* (ECCV), 101-117.
[7] Yang, H., & Zhang, Y. (2019). GC-Net: Geometry and Context Network for Stereo Matching. *IEEE Transactions on Pattern Analysis and Machine Intelligence*, 41(2), 491-503.
[8] Cheng, J., & Xu, C. (2018). PSMNet: Pyramid Stereo Matching Network. In *Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition* (CVPR), 928-936.
[9] Wang, Z., & Zhang, S. (2019). StereoGAN: A Generative Adversarial Network for Stereo Matching. *IEEE Transactions on Image Processing*, 28(2), 619-631.
[10] Li, X., & Zhang, Z. (2020). STTR: Stereo Transformer for Real-Time Stereo Matching. In *Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition* (CVPR), 8185-8194.