基于马尔可夫随机场的深度估计

文章目录

  • 前言
  • 马尔科夫随机场
    • 能量函数
  • 优化算法
    • Graphcut算法
    • Belief-Propagation算法
  • 代码及实验结果
    • Graphcut算法
    • Belief-Propagation算法
    • 结果与讨论
  • 总结


前言

根据双目立体视觉的原理,当我们知道世界坐标系中一点在左右两幅图像中所成像点的位置时,我们就可以根据视差计算出物点到相机的距离,即深度。因此,在左右两幅图像中寻找对应像点是稠密三维重建的关键环节,被称为立体匹配问题。对应像点匹配的准确度越高,相应地重建精度也就越高。
对于立体匹配问题,传统的解决方案一般可分为局部匹配算法和全局匹配算法。局部匹配算法即计算极线上的像素点及其邻域与待匹配点的相似度,例如最简单的使用绝对误差和(SAD)来衡量,SAD最小的那个像点就是我们寻找的对应像点。但这种局部匹配算法会存在patch大小选择的问题:若邻域选择过小,深度估计结果易受到噪声影响;若邻域选择过大,则又会丢失细节。而全局匹配算法能够通过构建能量函数同时考虑所有像素点深度估计的损失值,然后通过优化算法寻找全局最优解,其往往能获得更好的估计结果。当然,比起局部匹配算法,全局匹配计算耗时更长,因此实时性较差。本文重点介绍全局匹配算法以及几种常用的优化算法,并对它们的结果进行了比较。


马尔科夫随机场

马尔科夫随机场(MRF)是一种无向概率图模型,在传统的立体匹配、图像分割等问题中有着广泛的应用。全局立体匹配算法往往基于MRF来构造能量函数,其简化模型如下所示:
基于马尔可夫随机场的深度估计_第1张图片
模型包含两类节点:观测节点(蓝色)和隐含节点(绿色),其对应的联合概率分布如下:
p ( { x } , { d } ) = ∏ i ϕ ( x i , d i ) ∏ ( i , j ) Φ ( x i , x j ) p(\{x\}, \{d\}) =\prod_{i} \phi(x_i, d_i)\prod_{(i, j)}\Phi(x_i, x_j) p({x},{d})=iϕ(xi,di)(i,j)Φ(xi,xj)

式中 ϕ \phi ϕ Φ \Phi Φ为势函数。在深度估计应用中,随机变量{d}通常是其对应随机变量{x}的函数。深度估计的整体思想是通过观测节点的似然函数和隐含节点的先验信息来估计隐含节点的最大后验概率。这个MAP所对应的隐含节点上的随机变量的取值,即为最终的深度估计值。以上涉及的概率论相关知识可以看这个系列文章。

能量函数

具体到立体匹配问题,我们首先需要定义能量函数,它综合了全部像素点的匹配误差,公式如下:

E = ∑ i = 1 N ( D ( x i , d i ) + ∑ j ∈ N i V ( x i , x j ) ) E = \sum_{i=1}^{N}(D(x_i, d_i) + \sum_{j\in N_i}V(x_i, x_j)) E=i=1N(D(xi,di)+jNiV(xi,xj))

式中 D ( x i , d i ) D(x_i, d_i) D(xi,di)称为数据项(Data term),即观测项; V ( x i , x j ) V(x_i, x_j) V(xi,xj)称为平滑项,即先验项(Prior term)。其中数据项在立体匹配问题中描述的是两个待匹配像素点像素值的误差;而平滑项描述的是相邻像素点深度值的差异。
这里我们有两个假设:左右图像中对应像点的灰度值因该相同,这也称为光一致性约束;深度图中相邻像素点的深度值应该相近,称为平滑约束。因此,再来看上面这个能量函数:数据项通过左右图像中对应点灰度相近来约束视差估计, 同时先验项通过实际中一点与周围点深度连续来约束视差估计。
当然上面两个假设只能作为一种宽泛的指导,在实际中并不总是满足的。例如,对于镜面反射物体(即非朗伯体),不同角度的相机拍摄到的图像中对应点的灰度值肯定是不同的,而且差异可能还会很大;相反的,对于纹理缺失区域,同一区域中的像点虽然并不是对应像点,但他们的像素值又往往相同。再有,在物体边缘区域深度肯定是不连续,这就需要对平滑约束做一定限制。另外,两个相机拍摄角度相差过大还会可能出现遮挡问题。光照不均匀物体和纹理缺失区域,以及物体边缘正是传统立体匹配问题中的一大难点。
值得注意的是,上式与前面提到的联合概率分布函数是等价的,是负对数( − l o g -log log)的关系,因此求能量函数的最小值即为求联合概率分布的最大值。

优化算法

确定了能量函数,接下来我们的工作就是求能量函数的全局最小值,其对应的隐含节点的值就是最优估计值。对于这类能量函数的优化问题,有两种常用的优化算法:Graphcut(图割)算法和Belief-Propagation(置信度传播)算法。

Graphcut算法

Graphcut算法的基本思想是通过能量函数构建图模型并确定连接容量,将其转化为max-flow/min-cut问题,然后可以使用例如Ford-Fulkerson算法等通过迭代找到最大流,即能量函数全局最小值。具体代码如下所示(Matlab实现)。

Belief-Propagation算法

Belief-Propagation算法从概率图模型出发,依次寻找每个隐含节点对应边缘概率最大时的值。需要指出的是,即使全部变量都使它们对应的边缘概率最大,也并不能保证最终联合概率取到最大值。因此BP算法无法保证收敛到全局最优,它只是一种近似算法(个人理解)。当然,具体的关于其敛散性的分析博主也不想去搞,感兴趣的同学可以自行了解。
另外,最原始的置信传播算法并不适用于存在回路的概率图模型,这里需要用到改进的算法:Loopy Belief-Propagation。它对具有特定形式平滑项的能量函数又采用了一些技巧,以进一步减少计算量,具体原理可参考这篇文章。

代码及实验结果

这一部分分别使用Matlab和Python实现了Graphcut算法和Loopy Belief-Propagation算法。实验中所用双目图像如下:
基于马尔可夫随机场的深度估计_第2张图片

Graphcut算法

这里使用matlab实现了Graphcut算法,其中用到开源的GC优化工具:GCMex,代码如下:

clc
clear
addpath('./GCMex')

img1= double(imread('left.png'));
img2 = double(imread('right.png'));

disp = stereoGC(img1, img2, 50, 4);
imshow(disp)

function result = stereoGC(img_l, img_r, D_MAX, adjacency_num)
    [H, W, C] = size(img_l);
    d = 0 : D_MAX;
    N = H * W;
    unary = ones(D_MAX+1, N);
    [I, J] = meshgrid(1:D_MAX+1, 1:D_MAX+1);
    labelcost = min(25, (I - J).*(I - J));
    labelcost = labelcost./ 25;

    for i = 1 : D_MAX+1
        I = zeros(H,W,C);
        I(:, 1:W-d(i),:) = img_l(:, d(i)+1:W, :);
        D = sqrt(sum((img_r-I).^2, 3));
        D = reshape(D, 1, N);
        unary(i,:) = D;
    end
    if adjacency_num == 8
        unary = unary./ max(max(unary)) * 8;
    else
        unary = unary./ max(max(unary)) * 4;
    end
    [~,segclass] = min(unary,[],1);

    loctmp = ones(H, W);
    top = find(imtranslate(loctmp, [0, 1], 'FillValues', 0) ~= 0);
    bottom = find(imtranslate(loctmp, [0, -1], 'FillValues', 0) ~= 0);
    left = find(imtranslate(loctmp, [1, 0], 'FillValues', 0) ~= 0);
    right = find(imtranslate(loctmp, [-1, 0], 'FillValues', 0) ~= 0);
    if adjacency_num == 8
        lefttop = find(imtranslate(loctmp, [1, 1], 'FillValues', 0) ~= 0);
        righttop = find(imtranslate(loctmp, [-1, 1], 'FillValues', 0) ~= 0);
        leftbottom = find(imtranslate(loctmp, [1, -1], 'FillValues', 0) ~=0);
        rightbottom = find(imtranslate(loctmp, [-1, -1], 'FillValues', 0) ~= 0);
    end
    m = [right;left;top;bottom];
    n = [left;right;bottom;top];
    if adjacency_num == 8
        m = [right;left;top;bottom;righttop;lefttop;rightbottom;leftbottom];
        n = [left;right;bottom;top;leftbottom;rightbottom;lefttop;righttop];
    end
    pairwise = sparse(m, n, 1);
    [labels, ~, ~] = GCMex(segclass-1, single(unary), pairwise, single(labelcost), 1);
    dp = labels;
    dp = dp / max(dp);
    result = reshape(dp, H, W);
end

实验结果如下:
基于马尔可夫随机场的深度估计_第3张图片
在图模型的构建过程中,这里分别尝试了4邻域和8邻域两种模式。4邻域即在平滑项中考虑像素点上下左右4个相邻节点的平滑代价,而8邻域又额外考虑了左上,右上,左下,右下4个节点。可以发现,8邻域模式下获取的深度图更加平滑,整体估计效果更好,当然,计算耗时也更长。另外,关于如何平衡数据项和先验项的权重,这里对于初始值的设置博主有个建议,就是首先将二者的取值都归一化到(0, 1)区间,然后对于4邻域模式,其一个先验项中有4对像素点的平滑代价,因此数据项应该扩大4倍,对于8邻域模式,数据项就需要扩大8倍了。当然这只是粗调策略,实际中想要获得最优解还需要调参侠们进一步微调。

Belief-Propagation算法

这里使用Python实现了Belief-Propagation算法,代码如下:

import numpy as np
import cv2 as cv

def stereoLBP(img_l, img_r, k=50, s=0.05, eta=1, iterations=20):
    H, W, C = img_l.shape
    D = np.zeros((H, W, k))
    for i in range(k):
        tmp = np.zeros((H, W, C))
        tmp[:, :W - i, :] = img_l[:, i:, :]
        D[:, :, i] = np.sum((img_r - tmp) ** 2, -1) ** 0.5
    D = D / np.max(D)

    AFFINE_DIR = {'up': np.array([[1, 0, 0], [0, 1, -1]], dtype=np.float32),
                  'down': np.array([[1, 0, 0], [0, 1, 1]], dtype=np.float32),
                  'left': np.array([[1, 0, -1], [0, 1, 0]], dtype=np.float32),
                  'right': np.array([[1, 0, 1], [0, 1, 0]], dtype=np.float32)}
    m = {'up':      np.zeros((H, W, k)),
         'down':    np.zeros((H, W, k)),
         'left':    np.zeros((H, W, k)),
         'right':   np.zeros((H, W, k))}
    h = {'up':      np.zeros((H, W, k)),
         'down':    np.zeros((H, W, k)),
         'left':    np.zeros((H, W, k)),
         'right':   np.zeros((H, W, k))}
    for _ in range(iterations):
        h_tot = D + m['up'] + m['down'] + m['left'] + m['right']
        h['up'] = cv.warpAffine(h_tot-m['down'], AFFINE_DIR['down'], dsize=(W, H))
        h['down'] = cv.warpAffine(h_tot-m['up'], AFFINE_DIR['up'], dsize=(W, H))
        h['left'] = cv.warpAffine(h_tot-m['right'], AFFINE_DIR['right'], dsize=(W, H))
        h['right'] = cv.warpAffine(h_tot-m['left'], AFFINE_DIR['left'], dsize=(W, H))
        for x in {'up', 'down', 'left', 'right'}:
            m[x] = h[x]
            for i in range(1, k):
                m[x][:, :, i] = np.minimum(m[x][:, :, i], m[x][:, :, i-1] +  s)
            for i in reversed(range(0, k-1)):
                m[x][:, :, i] = np.minimum(m[x][:, :, i], m[x][:, :, i+1] + s)

        for x in {'up', 'down', 'left', 'right'}:
            tmp = h[x].min(axis=-1, keepdims=True) + eta
            m[x] = np.minimum(m[x], tmp)
    B = np.copy(D)
    for x in {'up', 'down', 'left', 'right'}:
        B = B + m[x]
    tmp =  np.argmin(B, -1)
    res = tmp / np.max(tmp)
    return res

if __name__ == '__main__':
    img1 = cv.imread('left.png')
    img2 = cv.imread('right.png')
    disp = stereoLBP(img1, img2)
    cv.imshow('Disparity Image', disp)
    cv.waitKey(0)

实验结果如下:
基于马尔可夫随机场的深度估计_第4张图片
实验发现,随着迭代次数的增加,获取的视差图不断变化,上面这个结果是迭代了20次的。虽然那个文章中说做一次迭代即可获取最优值,但博主在实验中并没有感受到收敛。这具体是理论层面的问题,还是我算法实现的问题,目前尚不清楚,也不打算深究了。
这个代码是博主自己手写的,在实现层面肯定还有很大优化空间,由于其中涉及了很多矩阵操作并且优化过程能够并行,因此可以使用GPU加速计算(Cupy库),这里就不展示了。至于这个Belief-Propagation算法和上面的Graphcut算法相比到底谁的时间复杂度更高,谁的实际耗时更长,这里不太好比较,感兴趣的同学可以自己研究。

结果与讨论

以上两种优化算法得到的结果不太一样,一方面是数据项和先验项设置的问题,另一方面是优化算法本身的问题。为了获得更好的深度估计结果,除了改进优化算法,我们还需要在能量函数的设计上做文章。比如光一致性约束和平滑约束的力度对于不同像素点应该都是不同的,研究者也提出了很多相关的自适应策略,如根据相邻像素点的像素值来调整平滑约束的力度:对于灰度差异较大的相邻像素点,认为其深度值差异会更大,因此减弱对应平滑约束的力度,这样能够更好地保护物体边缘。


总结

本文主要讲了传统的基于MRF的全局立体匹配算法,重点介绍了其基础理论和实现流程。因为涉及到很多零零散散的理论知识,想要说明白同时避免喧宾夺主还是有一定难度,因此更多的博主还是直接贴上了相关博文的链接,感兴趣的同学可以自行学习。
总体来看,传统方法理论性较强,门槛儿较高,但鲁棒性差,适用场景少,在实际中效果往往并不理想,因此本文仅作为对基础理论的梳理。目前深度估计的主流方法还是使用CNN提取特征,然后在特征空间计算视差,但其也存在三维卷积计算量过大的问题,还需研究者们继续努力。

你可能感兴趣的:(CV,算法,机器学习,人工智能,计算机视觉)