论文复现:通过融合 LiDAR 和 Stereo 进行稳健且准确的深度估计

论文复现:通过融合 LiDAR 和 Stereo 进行稳健且准确的深度估计

前言

传统方法做的一种融合方法来做估计,作为当前工作的一个补充。

论文地址:https://arxiv.org/pdf/2207.06139

工作简述

22年的工作,但是采用的传统方法(非学习)

提出了去除异常值的方法。其次,提出了一种立体图像和原始激光雷达数据融合的新流程。

方法论

生成视差图并上采样

为了融合激光雷达数据和立体图像,将激光雷达的深度转换为视差(说白了就是把点云投影到左右相机上计算列坐标差),然后进行上采样。视差图上采样和深度完成是本文中的相同任务。

这部分的公式在前一篇博客有详细叙述和代码示例。

数学上简单而言是用 SLAM 14讲 里面老生常谈的坐标变换。把激光雷达的3D信息投影到2D的图像上。然后视差公式:将X的深度z′转换为像素坐标(x,y)处的视差d。

其中B和F是立体相机的基线和焦距。下图展示了投影的过程。可以简单看出这个公式是用相似三角形得到的。

当左右图像不区分时,我们将左图像统一表示为Π,即基线视差图。视差图Π可以被认为是8位图像。 Π 中的视差表示为 d

算法1:消除重叠点

虽然Π中的点非常稀疏,但是有几个不同深度的点投影到同一局部区域。(换言之点云投影的点存在遮挡的关系)

这个问题给视差图上采样带来了很大的麻烦。算法1可以在一定程度上解决这个问题——清除扫描线之间的异常值。

算法1简单总结如下: LiDAR数据的视差有一个明显的特点,就是水平方向分辨率高,垂直方向分辨率低。第一步是在水平方向上完成这些扫描线,结果如图3b所示。然后将Π的视差坐标(x,y)上下翻转,找到视差坐标 和 。如果 Π(x, y) 和 Π 以及 之间的差异大于特定值,则该差异将被消除。

完成这些之后,为了解决垂直方向的分辨率问题,该文章的作者采用了线性插值的办法,也指出其他文章的双边滤波方法。线性插值公式如下

这边空出代码区来演示算法_

首先是生成扫描线。

PYTHON
import numpy as np
import matplotlib.pyplot as plt

def generate_scanline_image(points, image_shape):
    """
    将点云投影数据转换为扫描线图像。
    
    参数:
    - points: (N, 3) 的点云投影数据,包含 [x, y, value],分别为横坐标、纵坐标和深度值/颜色值。
    - image_shape: 输出图像的形状 (height, width)。
    
    返回:
    - scanline_image: 生成的扫描线图像。
    """
    # 初始化图像
    scanline_image = np.zeros(image_shape)
    
    # 将点按 y 坐标分组(行)
    unique_rows = np.unique(points[:, 1])
    for y in unique_rows:
        # 获取当前行的所有点
        row_points = points[points[:, 1] == y]
        
        # 按 x 坐标排序
        row_points = row_points[np.argsort(row_points[:, 0])]
        
        # 提取 x 和 value 值
        x_coords = row_points[:, 0].astype(int)
        values = row_points[:, 2]
        
        # 插值填充到整行
        full_x = np.arange(x_coords.min(), x_coords.max() + 1)
        full_values = np.interp(full_x, x_coords, values)
        
        # 将插值后的值写入图像
        scanline_image[int(y), full_x] = full_values
    
    return scanline_image

# 示例用法
if __name__ == "__main__":
    # 生成示例点云投影数据 [x, y, value]
    np.random.seed(42)
    points = np.array([
        [x, y, np.sin(x / 10) * 100] for y in range(100, 200, 10) for x in np.random.randint(0, 500, 30)
    ])
    
    # 图像形状
    image_shape = (300, 500)
    
    # 生成扫描线图像
    scanline_image = generate_scanline_image(points, image_shape)
    
    # 显示图像
    plt.figure(figsize=(10, 6))
    plt.imshow(scanline_image, cmap='viridis')
    plt.title("Generated Scanline Image")
    plt.colorbar(label="Value")
    plt.show()
Copy

深度估计

第一次接触这种任务。简单叙述一下步骤。

对于单目的深度估计,利用的信息有纹理(近实远虚),透视关系(近大远小),遮挡关系等等。实际上对单张图片而言,想要很好整合这些信息主要靠神经网络的方法提取特征再合并运算。

对于多目视觉,可以依靠对极几何来运算。核心的步骤是坐标变换,匹配,生成视差图。最后视差图变换为深度图。

而本文在这个的基础上融合了激光雷达的深度信息。

窗口模型

首先补充一下窗口模型的原理和应用:

窗口模型(也称为局部匹配或块匹配)是一种用于计算立体图像中对应点的技术。其基本原理是通过在一个小的窗口内(通常是一个正方形或矩形区域)搜索最佳匹配点。这种方法的核心思想是:

  • 选定一个参考点(在一个图像中),并在另一个图像中寻找一个相似的窗口区域

  • 计算窗口区域内的像素差异或相似度,寻找最佳匹配。

  • 在图像的多个位置重复这一过程,得到深度图(或视差图)。

步骤

  1. 局部窗口(Local Window)匹配 局部窗口匹配是指在图像的某一小区域内进行匹配。该区域通常是一个方形或矩形窗口,窗口的大小可以根据实际需要调整。窗口内的像素值通常由亮度、颜色、纹理等信息组成,算法通过计算两个图像窗口之间的差异(例如SSD、SAD、NCC等相似度度量)来确定最佳匹配。

  2. 滑动窗口(Sliding Window) 滑动窗口是指将窗口从图像的一个位置滑动到另一个位置,逐步遍历整个图像。在每个位置,通过计算窗口内的像素差异来判断两个视图是否匹配。随着窗口的滑动,算法逐渐完成整个图像的匹配过程。

  3. 视差图生成 通过对每个像素点及其对应窗口的匹配,算法可以计算每个像素的视差值。视差图(Disparity Map)是反映图像深度信息的图像,视差越大,意味着该点距离相机越近,反之则越远。

这篇文章采用的窗口是倾斜支撑窗,与平行于图像平面的窗口差距如下图所示。简而言之,是根据视察自适应调整窗口大小的一种窗口。

其引用的思想是双目视觉当中的一篇经典作品:倾斜支持窗 —— Patchmatch stereo-stereo matching with slanted support windows

下面是该文章的一个解析参考:

理论恒叨】【立体匹配系列】经典PatchMatch: (1)Slanted support windows倾斜支持窗模型

换言之,对于传统的矩形窗口,假设的是所有点在双目相机上的深度相同(也就是被摄物体的平面和极线纠正过后的相机平面相平行) 自然,投影的某一条线段也是平行且相等的。

当然这个假设虽然很适合直接用计算机的固定矩形卷积窗口,但是过于理想化。

如何保证窗口匹配的过程当中能最大限度地匹配这两张图片呢。答案是让窗口平面贴合物体的深度平面。于是这个倾斜支持窗口应运而生:从图中我们可以看出来,Slanted support windows应该是随着表面的朝向而动态变化的

窗口用(x,y,d)来描述。

平面 由点 (x, y, d) 和该点的随机单位法向量 确定。平面 可表示为

其中,d表示如下:

其中(x,y,d)是视差平面的点,(x,y)是视差图中的像素坐标。 a、b、c是平面 xi 的三个参数。这些参数通过以下三个公式获得。

那么问题是如何找到那个平面,无边界平面集F里用暴力穷举法搜索最小值显然不可取。可以看看原算法是怎么解决的——

PatchMatch的基本思想是:在自然图像中,一个有一定大小的像素块内所有像素都可以用同一个平面来近似。这也构成了PMS的基本思想。即图像可看作多个像素块,而每个像素块可有一个近似的视差平面,算法的目标就是要找到图像的所有视差平面。

  1. 随机初始化

    随机初始化是PMS寻找视差平面的第一步,即为每个像素随机一个视差平面。PMS期望能够通过该步骤能够在每一个平面区域中至少为1个像素随机到正确平面

  2. 迭代传播

把随机的所有视差平面中的少数正确的视差平面传播至同一视差平面内的其他像素。算法执行多次迭代,每次迭代执行四个步骤:(1)空间传播(Spatial Propagation)(2)视图传播(View Propagation)(3)时序传播(Temporal Propagation)(4)平面优化(Plane Refinement)。

在这些范围内可以缩小我们需要找到的平面范围并计算出视差图。

那么结合了激光点云的深度信息,我们自然可以更简单一些——随机初始化的过程直接取值,迭代传播的过程在雷达的扫描线当中进行并进一步缩小范围。

匹配成本

简洁易懂的全是公式——

最小匹配成本

本节用于确定使匹配成本最小化的平面参数。该算法可分为三个步骤:初始化、视差传播和平面细化。该算法每次迭代都会执行一次视差传播和平面细化。在本文中,算法执行了三次迭代。在下面的小节中,可以介绍该算法的一些细节——

1) 初始化:在窗口模型部分已经确定了平面模型。那么每个像素需要分配一个平面,但是很多平面参数还没有确定。因此每个像素可以分配一个随机单位向量, , 。如果窗口模型是前平行窗口,则单位向量为0。如果上采样视差图中p'(x,y)具有视差d',则将视差d'分配给d。如果不存在这样的值,则将整个图像的最大和最小视差之间的随机初始值分配给d。

2) 视差传播:每个像素都被分配了一个平面,然后需要找到每个像素上聚合成本最小的平面。由于相邻像素很可能具有相似的平面,因此可以通过扩展相邻像素的平面来找到聚合成本相对较低的平面。传播的方式有很多种,比如从左到右、从上到下、从左上到右下等等。本文使用的方法是在PMS中提出的。如果传播次数为奇数,则从左上到右下的传播方向描述为图6的右部分。如果传播次数为偶数,则传播方向为从右下到上-left 为图 6 的左侧部分。如果图像中的像素 C(p, Ψp) > C(p, Ψp)right,则需要进行 C(p, Ψp) = C(p, Ψp)right 。然后如果C(p, Ψp) > C(p, Ψp)down,则需要在奇数次传播中的相邻像素之间执行C(p, Ψp) = C(p, Ψp)down。像素p在偶数传播过程中需要与 和 进行比较 。

3) 平面细化: 也与PMS相同。。

文章的实验效果,(a)(g)立体图像的左图。 (b)(h) 深度完成的基本事实。 (c)(i) OURS(BI*) 的结果。 (d) (j) PMS 结果。 (e)(k) OURS(BF*)的结果。 (f)(l)是当α、β、γ分别设置为0.1、0.1、0.8,窗口大小为10~35时OURS(BI*)的结果。此时LiDAR数据缺失的鲁棒性领域得到增强。

值得关注的是天空那一块估计的并不好(类似高斯噪声的数据,可以尝试卷积清除)。此外对于一开始的清楚异常点,也可以试试可变大小的卷积清除图像上的异常投影。

这边同样预留出代码区:

PYTHON
import cv2
import numpy as np

def patchmatch_stereo(left_img, right_img, patch_size=5, max_disparity=64, iterations=5):
    """
    实现一个简单版本的 PatchMatch Stereo 算法
    
    :param left_img: 左图像 (灰度图像)
    :param right_img: 右图像 (灰度图像)
    :param patch_size: 匹配窗口的大小
    :param max_disparity: 最大视差范围
    :param iterations: PatchMatch 的迭代次数
    :return: 视差图
    """

    # 获取图像的高度和宽度
    H, W = left_img.shape
    disparity_map = np.zeros_like(left_img, dtype=np.float32)
    
    # 初始化视差图(随机初始化视差值)
    for y in range(H):
        for x in range(W):
            disparity_map[y, x] = np.random.randint(0, max_disparity)
    
    # 匹配操作
    for it in range(iterations):
        # 对每个像素进行 PatchMatch
        for y in range(H):
            for x in range(W):
                best_disparity = disparity_map[y, x]
                best_cost = float('inf')
                
                # 遍历视差范围
                for d in range(-max_disparity, max_disparity + 1):
                    if 0 <= x + d < W:
                        # 获取左图和右图对应窗口的块
                        left_patch = left_img[y:y+patch_size, x:x+patch_size]
                        right_patch = right_img[y:y+patch_size, (x + d): (x + d + patch_size)]
                        
                        # 计算块之间的差异(可以使用其他度量,比如 SSIM 或其他)
                        cost = np.sum((left_patch - right_patch) ** 2)
                        
                        if cost < best_cost:
                            best_cost = cost
                            best_disparity = d
                
                disparity_map[y, x] = best_disparity
        
        # 传播优化:更新邻域像素的视差
        for y in range(H):
            for x in range(W):
                # 可以进行视差传播(例如水平、垂直方向传播)
                # 基于已匹配的邻域值做优化
                # 简单的传播方法可以使用简单平均
                if x > 0:
                    disparity_map[y, x] = (disparity_map[y, x] + disparity_map[y, x-1]) / 2
                if y > 0:
                    disparity_map[y, x] = (disparity_map[y, x] + disparity_map[y-1, x]) / 2

    return disparity_map

# 载入图像
left_img = cv2.imread('left_image.png', cv2.IMREAD_GRAYSCALE)
right_img = cv2.imread('right_image.png', cv2.IMREAD_GRAYSCALE)

# 确保图像大小一致
assert left_img.shape == right_img.shape, "左右图像大小不一致"

# 运行 PatchMatch Stereo
disparity_map = patchmatch_stereo(left_img, right_img)

# 显示视差图
cv2.imshow('Disparity Map', disparity_map / np.max(disparity_map))  # 归一化显示
cv2.waitKey(0)
cv2.destroyAllWindows()
Copy

相关文章

  • Radiate 数据集融合前置:图像和激光雷达的校正,配准,融合