立体匹配是立体视觉研究中的关键部分。其目标是在两个或多个视点中匹配相应像素点,计算视差。通过建立一个能量代价函数,对其最小化来估计像素点的视差,求得深度。
点P和Q,映射到左相机OR像面上的同一点p≡q,只要找到p和q在右相机OT像面上的对应点就可以通过三角计算估计深度。找到对应点的过程,即立体匹配。
为了找到对应点,需要增加约束,最常用的是极线约束。
P和Q映射到左相机QR像面上的同一点p≡q,直线pq上的点对应点一定位于右像面的直线p’q’上,p’q’即为直线pq的极线,这就是极线约束。
接下来就可以根据视差估计深度,然后通过Graph cuts算法给每一个像素点分配视差从而得到深度图,不再详细说明。
1)根据采用图像表示的基元不同,立体匹配算法分为:
A、区域立体匹配算法:可获取稠密视差图。缺点:受图像的仿射畸变和辐射畸变影响较大;像素点约束窗口的大小与形状选择比较困难,选择过大,在深度不连续处,视差图中会出现过度平滑现象;选择过小,对像素点的约束比较少,图像信息没有得到充分利用,容易产生误匹配。
B、基于特征的立体匹配算法:可获得稀疏的视差图,经差值估计可获得稠密视差图。可提取点、线、面等局部特征,也可提取多边形和图像结构等全局特征。缺点:特征提取易受遮挡、光线、重复纹理等影响较大;差值估计计算量大。
C、基于相位立体匹配算法:假定在图像对应点中,其频率范围内,其局部相位是相等的,在频率范围内进行视差估计。
2)依据采用最优化理论方法的不同,立体匹配算法可以分为:
A、局部的立体匹配算法
B、全局的立体匹配算法
还有立体匹配算法介绍的更详细内容:参考博客
立体匹配过程:
对于原始的图像内任意一个像素点 ( 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时,表示两个匹配窗口相关程度非常高。
采集图像:通过标定好的双目相机采集图像,当然也可以用两个单目相机来组合成双目相机。
极线校正:校正的目的是使两帧图像极线处于水平方向,或者说是使两帧图像的光心处于同一水平线上。通过校正极线可以方便后续的 N C C NCC NCC操作。
1)由标定得到的内参中畸变信息中可以对图像去除畸变。
2)通过校正函数校正以后得到相机的矫正变换R和新的投影矩阵P,接下来是要对左右视图进行去畸变,并得到重映射矩阵。
特征匹配:这里便是我们利用 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=xr−xl,最终我们可以得到一个与原始图像尺寸相同的视差图 D D D
深度恢复:通过上述匹配结果得到的视差图 D D D,我们可以很简单的利用相似三角形反推出以左视图为参考系的深度图。计算原理如下图所示:
如图, T x Tx Tx为双目相机基线, f f f为相机焦距,这些可以通过相机标定步骤得到。而 x r − x l xr−xl xr−xl就是视差 d d d。
通过公式 z = f × T x d z=\frac{f×Tx}{d} z=df×Tx可以很简单地得到以左视图为参考系的深度图了。
至此,我们便完成了双目立体匹配。倘若只是用于图像识别,那么到步骤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()#清除所有窗口
# -*- 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()
此代码可以修改窗口值的大小,具体结果与分析放在下一小节
以下分别是当窗口值设定为15,20,50,100,200,500,700,1000,2000的运行结果:
总的来看,窗口值设置的越大,区域化分得越为明显,而设置的越小噪声越大,区块划分的越不明显。同时,从本次实验结果图来看,wid设置为700时,结果最为理想。
另外可以看到
瓶身光照最为强烈的地方会与瓶身其他其他地方像素值有较大区分,以及窗口大小设置越大,则会更多的提取出有光照的像素点并连结成块,区分出亮部暗部。
问题:
求解cols,rows = img.shape时出现上述报错,是图片路径不对,程序无法获取到图片。
解决办法:
右键下图目录灰色处
—>copypath,再在原本的读取图片路径代码中粘贴
注意格式改成C://Users//…
参考博客:立体匹配_数据结构与算法
参考博客:立体匹配过程
参考博客:双目立体匹配算法–归一化互相关(NCC)详解和代码实现(python)
NCC的更详细相关
延伸学习:真实场景的双目立体匹配(Stereo Matching)获取深度图详解
原理详解:双目视觉(三)立体匹配算法