实验要求:
1. 从理论角度,分析以窗口代价计算视差的原理
2. 实现NCC 视差匹配方法,即给定左右两张视图,根据NCC计算视差图
3. 分析不同窗口值对匹配结果的影响,重点考查那些点(或者哪些类型的点)在不同窗口大小下的匹配精度影响
一、归一化相关性(normalization cross-correlation,因此简称NCC)
NCC,顾名思义,就是用于归一化待匹配目标之间的相关程度,注意这里比较的是原始像素。通过在待匹配像素位置p(px,py)构建3*3邻域匹配窗口,与目标像素位置p'(px+d,py)同样构建邻域匹配窗口的方式建立目标函数来对匹配窗口进行度量相关性,注意这里构建相关窗口的前提是两帧图像之间已经校正到水平位置,即光心处于同一水平线上,此时极线是水平的,否则匹配过程只能在倾斜的极线方向上完成,这将消耗更多的计算资源。相关程度的度量方式由如下式子定义:
上式中的变量需要解释一下:其中p点表示图像I1待匹配像素坐标(px,py),d表示在图像I2被查询像素位置在水平方向上与px的距离。如下图所示:
左边为图像I1,右边为图像I2。图像I1,蓝色方框表示待匹配像素坐标(px,py),图像I2蓝色方框表示坐标位置为(px,py),红色方框表示坐标位置(px+d,py)。(由于画图水平有限,只能文字和图片双重说明来完成了~)
Wp表示以待匹配像素坐标为中心的匹配窗口,通常为3*3匹配窗口。
没有上划线的I1表示匹配窗口中某个像素位置的像素值,带上划线的I1表示匹配窗口所有像素的均值。I2同理。
上述公式表示度量两个匹配窗口之间的相关性,通过归一化将匹配结果限制在 [-1,1]的范围内,可以非常方便得到判断匹配窗口相关程度:
若NCC = -1,则表示两个匹配窗口完全不相关,相反,若NCC = 1时,表示两个匹配窗口相关程度非常高。
我们很自然的可以想到,如果同一个相机连续拍摄两张图像(注意,此时相机没有旋转也没有位移,此外光照没有明显变化,因为基于原始像素的匹配方法通常对上述条件是不具备不变性的),其中有一个位置是重复出现在两帧图像中的。比如桌子上的一个可乐瓶。那么我们就可以对这个可乐瓶的位置做一下匹配。直观的看,第一帧中可乐瓶上某一个点,它所构成邻域窗口按理说应该是与第二帧相同的,就算不完全相同,也应该是具有非常高相关性的。基于这种感性的理解,于是才有前辈提出上述的NCC匹配方法。(纯属个人理解)
双目立体匹配,这一部分是说明NCC如何用于双目匹配。
假设有校正过的两帧图像I1,、I2,由上述NCC计算流程的描述可知,对图像I1一个待匹配像素构建3*3匹配窗口,在图像I2极线上对每一个像素构建匹配窗口与待匹配像素匹配窗口计算相关性,相关性最高的视为最优匹配。很明显,这是一个一对多的过程。如果图像尺寸是640*480,则每一个像素的匹配过程是是1对640,两帧图像完全匹配需要计算640*480*640 = 196608000,即一亿九千多万次~ 尽管计算机计算速度非常快,但也着实是非常消耗计算资源的。由于NCC匹配流程是通过在同一行中查找最优匹配,因此它可以并行处理,这大概也算是一种弥补吧~
双目立体匹配流程如下:
1. 采集图像:通过标定好的双目相机采集图像,当然也可以用两个单目相机来组合成双目相机。(标定方法下次再说)
2. 极线校正:校正的目的是使两帧图像极线处于水平方向,或者说是使两帧图像的光心处于同一水平线上。通过校正极线可以方便后续的NCC操作。
3. 特征匹配:这里便是我们利用NCC做匹配的步骤啦,匹配方法如上所述,右视图中与左视图待测像素同一水平线上相关性最高的即为最优匹配。完成匹配后,我们需要记录其视差d,即待测像素水平方向xl与匹配像素水平方向xr之间的差值d = xr - xl,最终我们可以得到一个与原始图像尺寸相同的视差图D。
4. 深度恢复:通过上述匹配结果得到的视差图D,我们可以很简单的利用相似三角形反推出以左视图为参考系的深度图。计算原理如下图所示:
如图,Tx为双目相机基线,f为相机焦距,这些可以通过相机标定步骤得到。而xr - xl就是视差d。
通过公式 z = f * Tx / d可以很简单地得到以左视图为参考系的深度图了。
代码运行:
import numpy as np
import cv2
import math
im1 = 'im2.ppm'
im2 = 'im6.ppm'
img1 = cv2.imread(im1, cv2.CV_8UC1)
img2 = cv2.imread(im2, cv2.CV_8UC1)
rows, cols = img1.shape
print(img1.shape)
# 用3*3卷积核做均值滤波
def NCC(img1, img2, avg_img1, avg_img2, disparity, NCC_value, deeps, threshold, max_d, min_rows, max_rows):
# 设立阈值
ncc_value = threshold
if min_rows == 0:
min_rows += 1
for i in range(3, max_rows - 3):
for j in range(3, cols - 3):
if j < cols - max_d - 3:
max_d1 = max_d
else:
max_d1 = cols - j - 3
for d in range(4, max_d1): # 减一防止越界
ncc1 = 0
ncc2 = 0
ncc3 = 0
for m in range(i - 3, i + 4):
for n in range(j - 3, j + 4):
ncc1 += (img2[m, n] - avg_img2[i, j]) * (img1[m, n + d] - avg_img1[i, j + d])
ncc2 += (img2[m, n] - avg_img2[i, j]) * (img2[m, n] - avg_img2[i, j])
ncc3 += (img1[m, n + d] - avg_img1[i, j + d]) * (img1[m, n + d] - avg_img1[i, j + d])
ncc_b = math.sqrt(ncc2 * ncc3)
ncc_p_d = 0
if ncc_b != 0:
ncc_p_d = ncc1 / (ncc_b)
if ncc_p_d > ncc_value:
ncc_value = ncc_p_d
disparity[i, j] = d
NCC_value[i, j] = ncc_p_d
ncc_value = threshold
print("iter{0}".format(i))
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))
img1 = img1.astype(np.float32)
img2 = img2.astype(np.float32)
avg_img1 = avg_img1.astype(np.float32)
NCC(img1, img2, avg_img1, avg_img2, disparity, NCC_value, deeps, 0.6, 64, 0, 150)
disp = cv2.normalize(disparity, disparity, alpha=0, beta=255, norm_type=cv2.NORM_MINMAX,
dtype=cv2.CV_8U)
cv2.imshow("depth", disp)
cv2.waitKey(0) # 等待按键按下
cv2.destroyAllWindows() # 清除所有窗口
print(NCC_value)
实验数据集:
图片下载网址为:http://vision.middlebury.edu/stereo/data/scenes2003/
运行结果:
wid=15时:
wid=7时:
wid=3时:
实验结论:
对于NCC来说,运行比较耗时间,分子上是两个patch里边的像素分别相乘相加,而分母是每个patch里边的像素值平方然后求和在相乘开方,即使右眼的亮度加了,但是分子和分母的值都会同时增大,而且增大的值差不多相同,增大的值相除接近为1,所以得到的结果和右眼强度没有加10的结果基本上一样,没有多大的变化。所以如果左眼图和右眼图受光照强度干扰比较大的情况下,NCC会比较好。通过调节窗口wid来提高运行速率,当wid窗口越大、左右图像视差越大时,运行耗时就会减少,运行结果图片越清晰,并且会保持较好的匹配精度。