python计算机视觉——立体匹配与NCC算法

文章目录

      • 1. 立体匹配
        • 1.1 概述
        • 1.2 主要立体匹配算法分类
        • 1.3 立体匹配的基本步骤
      • 2. 归一化互相关(NCC)视差匹配法
        • 2.1 原理
        • 2.2 匹配流程
        • 2.3 代码实现
      • 3. 不同窗口值对匹配结果的影响
      • 4. 实验遇到的问题与解决

1. 立体匹配

1.1 概述

立体匹配是立体视觉研究中的关键部分。其目标是在两个或多个视点中匹配相应像素点,计算视差。通过建立一个能量代价函数,对其最小化来估计像素点的视差,求得深度
python计算机视觉——立体匹配与NCC算法_第1张图片
点P和Q,映射到左相机OR像面上的同一点p≡q,只要找到p和q在右相机OT像面上的对应点就可以通过三角计算估计深度找到对应点的过程,即立体匹配

为了找到对应点,需要增加约束,最常用的是极线约束

python计算机视觉——立体匹配与NCC算法_第2张图片
P和Q映射到左相机QR像面上的同一点p≡q,直线pq上的点对应点一定位于右像面的直线p’q’上,p’q’即为直线pq的极线,这就是极线约束

接下来就可以根据视差估计深度,然后通过Graph cuts算法给每一个像素点分配视差从而得到深度图,不再详细说明。

1.2 主要立体匹配算法分类

1)根据采用图像表示的基元不同,立体匹配算法分为:
A、区域立体匹配算法:可获取稠密视差图。缺点:受图像的仿射畸变和辐射畸变影响较大;像素点约束窗口的大小与形状选择比较困难,选择过大,在深度不连续处,视差图中会出现过度平滑现象;选择过小,对像素点的约束比较少,图像信息没有得到充分利用,容易产生误匹配。
B、基于特征的立体匹配算法:可获得稀疏的视差图,经差值估计可获得稠密视差图。可提取点、线、面等局部特征,也可提取多边形和图像结构等全局特征。缺点:特征提取易受遮挡、光线、重复纹理等影响较大;差值估计计算量大。
C、基于相位立体匹配算法:假定在图像对应点中,其频率范围内,其局部相位是相等的,在频率范围内进行视差估计。
2)依据采用最优化理论方法的不同,立体匹配算法可以分为:
A、局部的立体匹配算法
B、全局的立体匹配算法

还有立体匹配算法介绍的更详细内容:参考博客

1.3 立体匹配的基本步骤

立体匹配过程:

  1. 匹配代价计算: 一般是通过计算左右两图对应像素3个通道的灰度值差来决定匹配代价的,常用的就是基于像素点匹配代价计算,一般有AD,SD,TAD什么的,基于区域的匹配代价计算一般有SAD,SSD, STAD之类的。匹配代价计算会生成一个disparity space image,也就是DSI。这个DSI是一个三维的空间,也就是每一个视差,得到一张代价图。假如视差范围是0~16,则会得到17幅代价图。视差搜索范围就是MiddleBurry网站上的stereopair值,也就是说在视差范围内(比如0-16)内搜索匹配代价,得到17张匹配代价图,然后找到匹配代价最小的对应的视差值就是此像素对应的视差
  2. 代价聚合:其实就是一个滤波的过程,对每一幅代价图进行聚合,最简单的就是采用boxfilter。第一步代价计算只是得到了图像上所有孤立像素的视差值,但是这些时差值都是孤立的,引入了过多噪声,比如一片区域的视差值都是10,可是引入噪声后就会导致这一片的视差值都不一样,那么就需要一个滤波的过程,也就是我们所说的局部立体匹配方法,即采用窗口卷积达到局部滤波的目的
  3. 计算视差:常用的方法就是WTA算法(局部),对于图像中的同一个点,选出17幅代价图中匹配代价最小的那张图,该幅图对应的视差值就选取为最终的视差。或者在全局立体匹配中采用能量函数的方法,分为数据项和平滑项,数据项其实就是代价计算,平滑项就是代价聚合,只不过窗口大小是整幅图像,也可以试试如果把平滑项前面的系数lamda设为0,那么得到的结果和单纯代价计算的局部立体匹配是一样的。
  4. 视差精化:也就是对得到的视差进行优化的过程,如:左右一致性检测、区域投票等;这步其实是很多立体匹配的遮羞布,比如用遮挡处理,中值滤波,左右一致性检测等,都能使最后的是视差图提升1%左右,它是很多论文的遮羞布。但是不可否认的是,立体匹配最关键的步骤仍然是代价计算和代价聚合步骤。
    在立体匹配方法中,基于全局和局部的算法有些区别。不过基本步骤都差不多。有些时候,基于局部的算法,第一步和第二步是合并在一起进行的,基于全局的算法,会跳过第二步。

2. 归一化互相关(NCC)视差匹配法

2.1 原理

对于原始的图像内任意一个像素点 ( p x , p y ) (px,py) (px,py)构建一个 n × n n×n n×n的邻域作为匹配窗口。然后对于目标相素位置 ( p x + d , p y ) (px+d,py) (px+d,py)同样构建一个 n × n n×n n×n大小的匹配窗口,对两个窗口进行相似度度量,注意这里的dd dd有一个取值范围。对于两幅图像来说,在进行 N C C NCC NCC计算之前要对图像处理,也就是将两帧图像校正到水平位置,即光心处于同一水平线上,此时极线是水平的,否则匹配过程只能在倾斜的极线方向上完成,这将消耗更多的计算资源。

N C C NCC NCC计算公式如下:
N C C ( p , d ) = ∑ ( x , y ) ∈ W p ( I 1 ( x , y ) − I 1 ‾ ( P x , P y ) ) ⋅ ( I 1 ( x + d , y ) − I 2 ‾ ( P x + d , P y ) ) ∑ ( x , y ) ∈ W p ( I 1 ( x , y ) − I 1 ‾ ( P x , P y ) ) 2 ⋅ ∑ ( x , y ) ∈ W p ( I 2 ( x + d , y ) − I 2 ‾ ( P x + d , P y ) ) 2 NCC(p,d)=\frac{\sum_{(x,y)\in W_{p}}(I_{1}(x,y)-\overline{I_{1}}(P_{x},P_{y}))\cdot (I_{1}(x+d,y)-\overline{I_{2}}(P_{x}+d,P_{y}))}{\sum_{(x,y)\in W_{p}}(I_{1}(x,y)-\overline{I_{1}}(P_{x},P_{y}))^2\cdot \sum_{(x,y)\in W_{p}}(I_{2}(x+d,y)-\overline{I_{2}}(P_{x}+d,P_{y}))^2} NCC(p,d)=(x,y)Wp(I1(x,y)I1(Px,Py))2(x,y)Wp(I2(x+d,y)I2(Px+d,Py))2(x,y)Wp(I1(x,y)I1(Px,Py))(I1(x+d,y)I2(Px+d,Py))

其中 N C C ( p , d ) NCC(p,d) NCC(p,d)得到的值的范围将在[−1,1]之间。
W p Wp Wp为之前提到的匹配窗口
I 1 ( x , y ) I_{1}(x,y) I1(x,y)原始图像的像素值
I 1 ‾ ( p x , p y ) \overline{I_{1}}(p_x,p_y) I1(px,py)原始窗口内像素的均值
I 2 ( x + d , y ) I_{2}(x+d,y) I2(x+d,y)原始图像在目标图像上对应点位置在 x x x方向上偏移 d d d后的像素值
I 2 ‾ ( p x + d , p y ) \overline{I_{2}}(p_x+d, p_y) I2(px+d,py)目标图像匹配窗口像素均值

N C C = − 1 NCC=−1 NCC=1,则表示两个匹配窗口完全不相关,相反,若 N C C = 1 NCC=1 NCC=1时,表示两个匹配窗口相关程度非常高。

2.2 匹配流程

  1. 采集图像:通过标定好的双目相机采集图像,当然也可以用两个单目相机来组合成双目相机。

  2. 极线校正:校正的目的是使两帧图像极线处于水平方向,或者说是使两帧图像的光心处于同一水平线上。通过校正极线可以方便后续的 N C C NCC NCC操作。
    1)由标定得到的内参中畸变信息中可以对图像去除畸变
    2)通过校正函数校正以后得到相机的矫正变换R和新的投影矩阵P,接下来是要对左右视图进行去畸变,并得到重映射矩阵

  3. 特征匹配:这里便是我们利用 N C C NCC NCC做匹配的步骤啦,匹配方法如上所述,右视图中与左视图待测像素同一水平线上相关性最高的即为最优匹配。完成匹配后,我们需要记录其视差 d d d,即待测像素水平方向 x l xl xl与匹配像素水平方向 x r xr xr之间的差值 d = x r − x l d=xr−xl d=xrxl,最终我们可以得到一个与原始图像尺寸相同的视差图 D D D

  4. 深度恢复:通过上述匹配结果得到的视差图 D D D,我们可以很简单的利用相似三角形反推出以左视图为参考系的深度图。计算原理如下图所示:
    python计算机视觉——立体匹配与NCC算法_第3张图片

如图, T x Tx Tx为双目相机基线, f f f为相机焦距,这些可以通过相机标定步骤得到。而 x r − x l xr−xl xrxl就是视差 d d d
通过公式 z = f × T x d z=\frac{f×Tx}{d} z=df×Tx可以很简单地得到以左视图为参考系的深度图了。
至此,我们便完成了双目立体匹配。倘若只是用于图像识别,那么到步骤3时已经可以结束了。

2.3 代码实现

NCCfaster.py

import numpy as np
import cv2
from PCV.localdescriptors import sift

im1 = 'C://Users//Garfield//PycharmProjects//untitled//NCC-master//im2.ppm'
im2 = 'C://Users//Garfield//PycharmProjects//untitled//NCC-master//im6.ppm'

img1 = cv2.imread(im1, cv2.CV_8UC1)
img2 = cv2.imread(im2, cv2.CV_8UC1)
rows, cols = img1.shape
print(img1.shape)

def translaton(image, shape):
    step = round((shape[0]-1)/2)
    print(step)
    shifted = []
    for i in range(0, step+1):
        for j in range(0, step+1):
            if i==0 and j==0:
                M1 = np.float32([[1, 0, i], [0, 1, j]])
                shifted.append(cv2.warpAffine(image, M1, (image.shape[1], image.shape[0])))
            elif i==0 and j!=0:
                M1 = np.float32([[1, 0, i], [0, 1, j]])
                M2 = np.float32([[1, 0, i], [0, 1, -j]])
                shifted.append(cv2.warpAffine(image, M1, (image.shape[1], image.shape[0])))
                shifted.append(cv2.warpAffine(image, M2, (image.shape[1], image.shape[0])))
            elif i!=0 and j==0:
                M1 = np.float32([[1, 0, i], [0, 1, j]])
                M2 = np.float32([[1, 0, -i], [0, 1, j]])
                shifted.append(cv2.warpAffine(image, M1, (image.shape[1], image.shape[0])))
                shifted.append(cv2.warpAffine(image, M2, (image.shape[1], image.shape[0])))
            else:
                M1 = np.float32([[1, 0, i], [0, 1, j]])
                M2 = np.float32([[1, 0, -i], [0, 1, -j]])
                M3 = np.float32([[1, 0, -i], [0, 1, j]])
                M4 = np.float32([[1, 0, i], [0, 1, -j]])
                shifted .append(cv2.warpAffine(image, M1, (image.shape[1], image.shape[0])))
                shifted.append(cv2.warpAffine(image, M2, (image.shape[1], image.shape[0])))
                shifted.append(cv2.warpAffine(image, M3, (image.shape[1], image.shape[0])))
                shifted.append(cv2.warpAffine(image, M4, (image.shape[1], image.shape[0])))

    print(len(shifted))
    return np.array(shifted)

#I(x,y)-avg(I(x,y))
def img_sub_avg(img_shifted, avg_img):
    len, height, width = img1_shifted.shape
    tmp_ncc1 = np.zeros([len, height, width])
    for i in range(len):
        tmp_ncc1[i] = img_shifted[i] - avg_img
    print(tmp_ncc1)
    return tmp_ncc1

def NCC(img1_sub_avg,img2_sub_avg, threshold, max_d):
    #设立阈值
    len, height, width = img1_sub_avg.shape
    thershould_shifted = np.zeros([len, height, width])
    ncc_max = np.zeros([height, width])
    ncc_d = np.zeros([height, width])
    for j in range(3, max_d):
        tmp_ncc1 = np.zeros([height, width])
        tmp_ncc2 = np.zeros([height, width])
        tmp_ncc3 = np.zeros([height, width])
        for k in range(len):
            M1 = np.float32([[1, 0, -j - 1], [0, 1, 0]])
            thershould_shifted[k] = cv2.warpAffine(img1_sub_avg[k], M1, (img1_sub_avg.shape[2], img1_sub_avg.shape[1]))
        for i in range(len):
            tmp_ncc1 += (img2_sub_avg[i])*(thershould_shifted[i])
            tmp_ncc2 += pow(img2_sub_avg[i], 2)
            tmp_ncc3 += pow(thershould_shifted[i], 2)

        tmp_ncc2 = tmp_ncc2*tmp_ncc3
        tmp_ncc2 = np.sqrt(tmp_ncc2)
        tmp_ncc4 = tmp_ncc1/tmp_ncc2
        for m in range(height):
            for n in range(width):
                if tmp_ncc4[m, n] > ncc_max[m ,n] and tmp_ncc4[m, n] > threshold:
                    ncc_max[m, n] = tmp_ncc4[m, n]
                    ncc_d[m , n] = j
    for i in ncc_d:
        print(i)
    return ncc_max, ncc_d

if __name__ == "__main__":
    disparity = np.zeros([rows, cols])
    NCC_value = np.zeros([rows, cols])
    deeps = np.zeros([rows, cols])
    # 用3*3卷积核做均值滤波
    avg_img1 = cv2.blur(img1, (7, 7))
    avg_img2 = cv2.blur(img2, (7, 7))
    fimg1 = img1.astype(np.float32)
    fimg2 = img2.astype(np.float32)
    avg_img1 = avg_img1.astype(np.float32)
    avg_img2  = avg_img2.astype(np.float32)
    img1_shifted = translaton(fimg1, [7, 7])
    img2_shifted = translaton(fimg2, [7, 7])
    img1_sub_avg = img_sub_avg(img1_shifted, avg_img1)
    img2_sub_avg = img_sub_avg(img2_shifted, avg_img2)
    ncc_max, ncc_d = NCC(img1_sub_avg,img2_sub_avg, threshold = 0.5, max_d = 64)
    print(img1_shifted.shape)
    disp = cv2.normalize(ncc_d, ncc_d, alpha=0, beta=255, norm_type=cv2.NORM_MINMAX,
                          dtype=cv2.CV_8U)
    cv2.imshow("left", img1)
    cv2.imshow("right", img2)
    cv2.imshow("depth", disp)
    cv2.waitKey(0)  # 等待按键按下
    cv2.destroyAllWindows()#清除所有窗口

原始图像:
python计算机视觉——立体匹配与NCC算法_第4张图片

运行结果:

python计算机视觉——立体匹配与NCC算法_第5张图片
代码2:

# -*- coding: utf-8 -*-
import scipy.misc
from PIL import Image
from pylab import *
import cv2
from numpy import *
from numpy.ma import array
from scipy.ndimage import filters
def plane_sweep_ncc(im_l,im_r,start,steps,wid):
    """ 使用归一化的互相关计算视差图像 """
    m,n = im_l.shape
    # 保存不同求和值的数组
    mean_l = zeros((m,n))
    mean_r = zeros((m,n))
    s = zeros((m,n))
    s_l = zeros((m,n))
    s_r = zeros((m,n))
    # 保存深度平面的数组
    dmaps = zeros((m,n,steps))
    # 计算图像块的平均值
    filters.uniform_filter(im_l,wid,mean_l)
    filters.uniform_filter(im_r,wid,mean_r)
    # 归一化图像
    norm_l = im_l - mean_l
    norm_r = im_r - mean_r
    # 尝试不同的视差
    for displ in range(steps):
        # 将左边图像移动到右边,计算加和
        filters.uniform_filter(np.roll(norm_l, -displ - start) * norm_r, wid, s) # 和归一化
        filters.uniform_filter(np.roll(norm_l, -displ - start) * np.roll(norm_l, -displ - start), wid, s_l)
        filters.uniform_filter(norm_r*norm_r,wid,s_r) # 和反归一化
        # 保存 ncc 的分数
        dmaps[:,:,displ] = s / sqrt(s_l * s_r)
        # 为每个像素选取最佳深度
    return np.argmax(dmaps, axis=2)

def plane_sweep_gauss(im_l,im_r,start,steps,wid):
 """ 使用带有高斯加权周边的归一化互相关计算视差图像 """
 m,n = im_l.shape
 # 保存不同加和的数组
 mean_l = zeros((m,n))
 mean_r = zeros((m,n))
 s = zeros((m,n))
 s_l = zeros((m,n))
 s_r = zeros((m,n))
 # 保存深度平面的数组
 dmaps = zeros((m,n,steps))
 # 计算平均值
 filters.gaussian_filter(im_l,wid,0,mean_l)
 filters.gaussian_filter(im_r,wid,0,mean_r)
 # 归一化图像
 norm_l = im_l - mean_l
 norm_r = im_r - mean_r
 # 尝试不同的视差
 for displ in range(steps):
     # 将左边图像移动到右边,计算加和
     filters.gaussian_filter(np.roll(norm_l, -displ - start) * norm_r, wid, 0, s) # 和归一化
     filters.gaussian_filter(np.roll(norm_l, -displ - start) * np.roll(norm_l, -displ - start), wid, 0, s_l)
     filters.gaussian_filter(norm_r*norm_r,wid,0,s_r) # 和反归一化
     # 保存 ncc 的分数
     dmaps[:,:,displ] = s / np.sqrt(s_l * s_r)
 # 为每个像素选取最佳深度
 return np.argmax(dmaps, axis=2)

im_l = array(Image.open(r'C://Users//Garfield//Desktop//towelmatch//jidian//1.jpg').convert('L'), 'f')
im_r = array(Image.open(r'C://Users//Garfield//Desktop//towelmatch//jidian//2.jpg').convert('L'),'f')
# 开始偏移,并设置步长
steps = 12
start = 4
# ncc 的宽度
wid = 2000
res = plane_sweep_ncc(im_l,im_r,start,steps,wid)

imsave('C://Users//Garfield//PycharmProjects//untitled//NCC-master//depth2000.png',res)
show()

此代码可以修改窗口值的大小,具体结果与分析放在下一小节

3. 不同窗口值对匹配结果的影响

原图:
python计算机视觉——立体匹配与NCC算法_第6张图片

以下分别是当窗口值设定为15,20,50,100,200,500,700,1000,2000的运行结果:
python计算机视觉——立体匹配与NCC算法_第7张图片
python计算机视觉——立体匹配与NCC算法_第8张图片

总的来看,窗口值设置的越大,区域化分得越为明显,而设置的越小噪声越大,区块划分的越不明显。同时,从本次实验结果图来看,wid设置为700时,结果最为理想。
另外可以看到
python计算机视觉——立体匹配与NCC算法_第9张图片
瓶身光照最为强烈的地方会与瓶身其他其他地方像素值有较大区分,以及窗口大小设置越大,则会更多的提取出有光照的像素点并连结成块,区分出亮部暗部。

4. 实验遇到的问题与解决

问题:

  • ‘NoneType’ object has no attribute ‘shape’

求解cols,rows = img.shape时出现上述报错,是图片路径不对,程序无法获取到图片。

解决办法:
右键下图目录灰色处
python计算机视觉——立体匹配与NCC算法_第10张图片
—>copypath,再在原本的读取图片路径代码中粘贴
注意格式改成C://Users//…
参考博客:立体匹配_数据结构与算法
参考博客:立体匹配过程
参考博客:双目立体匹配算法–归一化互相关(NCC)详解和代码实现(python)
NCC的更详细相关
延伸学习:真实场景的双目立体匹配(Stereo Matching)获取深度图详解
原理详解:双目视觉(三)立体匹配算法

你可能感兴趣的:(python计算机视觉——立体匹配与NCC算法)