核级几何和立体图像深度图
一、核级几何
本小节中我们主要学习多视角几何学基础以及什么是对极、对极线、对极约束等。
1. 基础概念
当我们使用针孔相机拍摄图像时,我们会丢失一个重要信息,即图像的深度。 或者图像中的每个点距离相机有多远,因为它是 3D 到 2D 的转换。 因此,我们是否可以使用这些相机找到深度信息是一个重要的问题。 答案是使用不止一台相机。 我们的眼睛以类似的方式工作,我们使用两个相机(两只眼睛),这被称为立体视觉。 那么让我们看看OpenCV在这个领域提供了什么。
(Gary Bradsky 的《Learning OpenCV》在这个领域有很多资料。)
在进入深度图像之前,让我们首先了解多视图几何中的一些基本概念。 在本节中,我们将处理对极几何。 请参见下图,该图显示了使用两个摄像头拍摄同一场景图像的基本设置。
如果我们只使用左侧相机,我们将无法在图像中找到与点 x 对应的 3D 点,因为 OX 线上的每个点都投影到图像平面上的同一点。但也要考虑正确的图像。现在 OX 线上的不同点投影到右平面中的不同点 (x')。因此,通过这两张图像,我们可以对正确的 3D 点进行三角测量。这是整个想法。
OX 上不同点的投影在右平面上形成一条线(线 l')。我们称它为对应于点 x 的外延线。这意味着,要找到正确图像上的点 x,沿着这条线搜索。它应该在这条线上的某个地方(这样想,要在其他图像中找到匹配点,您无需搜索整个图像,只需沿着 epiline 搜索即可。因此它提供了更好的性能和准确性)。这称为对极约束。同样,所有点在另一幅图像中都有其对应的外延线。平面 XOO' 称为对极平面。
O 和 O' 是相机中心。从上面给出的设置中,您可以看到右侧相机 O' 的投影在左侧图像上的点 e 处看到。它被称为极点。 Epipole 是通过相机中心和图像平面的线的交点。类似地,e' 是左相机的极点。在某些情况下,您将无法在图像中找到极点,它们可能在图像之外(这意味着,一台摄像机看不到另一台摄像机)。
所有的epilines都通过它的epipole。所以要找到极点的位置,我们可以找到许多极线并找到它们的交点。
因此,在本节中,我们专注于寻找极线和极线。但要找到它们,我们还需要另外两种成分,基本矩阵 (F) 和基本矩阵 (E)。基本矩阵包含有关平移和旋转的信息,这些信息描述了第二个摄像机相对于第一个摄像机在全局坐标中的位置。请参见下图(图片提供:Gary Bradsky 的 Learning OpenCV):
但我们更喜欢在像素坐标中进行测量,对吧? Fundamental Matrix 包含与 Essential Matrix 相同的信息以及有关两个相机的内在函数的信息,因此我们可以在像素坐标中关联两个相机。 (如果我们使用校正后的图像并通过除以焦距来归一化该点,F=E)。 简而言之,基本矩阵 F 将一幅图像中的一个点映射到另一幅图像中的一条线(epiline)。 这是根据两个图像的匹配点计算得出的。 至少需要 8 个这样的点才能找到基本矩阵(使用 8 点算法时)。 更多的点是首选,并使用 RANSAC 来获得更稳健的结果。
2. 代码
所以首先我们需要在两幅图像之间找到尽可能多的匹配来找到基本矩阵。 为此,我们使用 SIFT 描述符和基于 FLANN 的匹配器和比率测试。
import cv2 as cv
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
%matplotlib inline
复制代码
def cv_show(name,img):
cv.imshow(name,img)
cv.waitKey(0)
cv.destroyAllWindows()
复制代码
def compare(images):
res = np.hstack(images)
cv_show('Compare', res)
复制代码
img1 = cv.imread('left.jpg',0) #queryimage # left image
img2 = cv.imread('right.jpg',0) #trainimage # right image
sift = cv.SIFT_create()
# find the keypoints and descriptors with SIFT
kp1, des1 = sift.detectAndCompute(img1,None)
kp2, des2 = sift.detectAndCompute(img2,None)
# FLANN parameters
FLANN_INDEX_KDTREE = 1
index_params = dict(algorithm = FLANN_INDEX_KDTREE, trees = 5)
search_params = dict(checks=50)
flann = cv.FlannBasedMatcher(index_params,search_params)
matches = flann.knnMatch(des1,des2,k=2)
pts1 = []
pts2 = []
# ratio test as per Lowe's paper
for i,(m,n) in enumerate(matches):
if m.distance < 0.8*n.distance:
pts2.append(kp2[m.trainIdx].pt)
pts1.append(kp1[m.queryIdx].pt)
复制代码
现在我们得到了两个图像的最佳匹配列表。 让我们找到基本矩阵。
pts1 = np.int32(pts1)
pts2 = np.int32(pts2)
F, mask = cv.findFundamentalMat(pts1,pts2,cv.FM_LMEDS)
# We select only inlier points
pts1 = pts1[mask.ravel()==1]
pts2 = pts2[mask.ravel()==1]
复制代码
接下来我们找到外延线。 在第二张图像上绘制对应于第一张图像中的点的 Epilines。 所以在这里提到正确的图像很重要。 我们得到一系列行。 所以我们定义了一个新函数来在图像上绘制这些线条。
def drawlines(img1,img2,lines,pts1,pts2):
''' img1 - image on which we draw the epilines for the points in img2
lines - corresponding epilines '''
r,c = img1.shape
img1 = cv.cvtColor(img1,cv.COLOR_GRAY2BGR)
img2 = cv.cvtColor(img2,cv.COLOR_GRAY2BGR)
for r,pt1,pt2 in zip(lines,pts1,pts2):
color = tuple(np.random.randint(0,255,3).tolist())
x0,y0 = map(int, [0, -r[2]/r[1] ])
x1,y1 = map(int, [c, -(r[2]+r[0]*c)/r[1] ])
img1 = cv.line(img1, (x0,y0), (x1,y1), color,1)
img1 = cv.circle(img1,tuple(pt1),5,color,-1)
img2 = cv.circle(img2,tuple(pt2),5,color,-1)
return img1,img2
复制代码
现在我们在两个图像中找到外延线并绘制它们。
# Find epilines corresponding to points in right image (second image) and
# drawing its lines on left image
lines1 = cv.computeCorrespondEpilines(pts2.reshape(-1,1,2), 2,F)
lines1 = lines1.reshape(-1,3)
img5,img6 = drawlines(img1,img2,lines1,pts1,pts2)
# Find epilines corresponding to points in left image (first image) and
# drawing its lines on right image
lines2 = cv.computeCorrespondEpilines(pts1.reshape(-1,1,2), 1,F)
lines2 = lines2.reshape(-1,3)
img3,img4 = drawlines(img2,img1,lines2,pts2,pts1)
compare([img5,img3])
# fig = plt.figure(figsize = (20,8))
# plt.subplot(121),plt.xticks([]),plt.yticks([])
# plt.imshow(img5)
# plt.subplot(122),plt.xticks([]),plt.yticks([])
# plt.imshow(img3)
# plt.show()
复制代码
您可以在左侧图像中看到所有外延线都在右侧图像外部的一点处会聚。 那个交汇点就是极点。
为了获得更好的结果,应使用具有良好分辨率和许多非平面点的图像。
二、立体图像深度图
本小节我们主要学习从立体图像中创建一个深度图。
1. 基础概念
在上一节课中,我们看到了极线约束和其他相关术语等基本概念。 我们还看到,如果我们有两个相同场景的图像,我们可以直观地从中获取深度信息。 下面是一张图片和一些简单的数学公式来证明这种直觉。 (图片示意):
上图包含等效三角形。 写出它们的等效方程将产生以下结果:
x 和 x' 是对应于场景点 3D 的图像平面中的点与它们的相机中心之间的距离。 B 是两个相机之间的距离(我们知道),f 是相机的焦距(已经知道)。 所以简而言之,上面的等式表示场景中一个点的深度与相应图像点与其相机中心的距离差成反比。 因此,利用这些信息,我们可以推导出图像中所有像素的深度。
所以它找到了两个图像之间的对应匹配。 我们已经看到了epiline约束如何使这个操作更快更准确。 一旦找到匹配项,它就会找到差异。 让我们看看如何使用 OpenCV 做到这一点。
2. 代码
imgL = cv.imread('tsukuba_l.png',0)
imgR = cv.imread('tsukuba_r.png',0)
stereo = cv.StereoBM_create(numDisparities=16, blockSize=15)
disparity = stereo.compute(imgL,imgR)
# compare([imgL,imgR,disparity])
fig = plt.figure(figsize = (20,8))
plt.subplot(131),plt.xticks([]),plt.yticks([])
plt.imshow(imgL,cmap='gray')
plt.subplot(132),plt.xticks([]),plt.yticks([])
plt.imshow(imgR,cmap='gray')
plt.subplot(133),plt.xticks([]),plt.yticks([])
plt.imshow(disparity,cmap='gray')
plt.show()
复制代码
下图包含原始图像(左)及其视差图(右)。 如您所见,结果被高度噪声污染。 通过调整 numDisparities 和 blockSize 的值,可以获得更好的结果。
当您熟悉 StereoBM 时,有一些参数,您可能需要对参数进行微调以获得更好、更流畅的结果。参数:
- texture_threshold:过滤掉没有足够纹理进行可靠匹配的区域
- Speckle range and size:基于块的匹配器通常会在对象边界附近产生“散斑”,其中匹配窗口在一侧捕捉前景,在另一侧捕捉背景。在这个场景中,匹配器似乎也在桌子上的投影纹理中发现了小的虚假匹配。为了消除这些伪影,我们使用由 speckle_size 和 speckle_range 参数控制的散斑滤波器对视差图像进行后处理。 speckle_size 是视差斑点被视为“散斑”的像素数。 speckle_range 控制值差异的接近程度必须被视为同一 blob 的一部分。
- 视差disparities数:将窗口滑过多少像素。它越大,可见深度的范围越大,但需要更多的计算。
- min_disparity:开始搜索的左侧像素的 x 位置的偏移量。
- uniqueness_ratio:另一个后过滤步骤。如果最佳匹配视差没有比搜索范围内的所有其他视差足够好,则过滤掉该像素。如果 texture_threshold 和散斑过滤仍然允许虚假匹配,您可以尝试调整它。
- prefilter_size 和 prefilter_cap:预过滤阶段,标准化图像亮度并增强纹理,为块匹配做准备。通常你不需要调整这些。
3. 补充资料
4. 练习
#!/usr/bin/env python
'''
Simple example of stereo image matching and point cloud generation.
Resulting .ply file cam be easily viewed using MeshLab ( http://meshlab.sourceforge.net/ )
'''
# Python 2/3 compatibility
from __future__ import print_function
import numpy as np
import cv2 as cv
ply_header = '''ply
format ascii 1.0
element vertex %(vert_num)d
property float x
property float y
property float z
property uchar red
property uchar green
property uchar blue
end_header
'''
def write_ply(fn, verts, colors):
verts = verts.reshape(-1, 3)
colors = colors.reshape(-1, 3)
verts = np.hstack([verts, colors])
with open(fn, 'wb') as f:
f.write((ply_header % dict(vert_num=len(verts))).encode('utf-8'))
np.savetxt(f, verts, fmt='%f %f %f %d %d %d ')
def main():
print('loading images...')
imgL = cv.pyrDown(cv.imread(cv.samples.findFile('aloeL.jpg'))) # downscale images for faster processing
imgR = cv.pyrDown(cv.imread(cv.samples.findFile('aloeR.jpg')))
# disparity range is tuned for 'aloe' image pair
window_size = 3
min_disp = 16
num_disp = 112-min_disp
stereo = cv.StereoSGBM_create(minDisparity = min_disp,
numDisparities = num_disp,
blockSize = 16,
P1 = 8*3*window_size**2,
P2 = 32*3*window_size**2,
disp12MaxDiff = 1,
uniquenessRatio = 10,
speckleWindowSize = 100,
speckleRange = 32
)
print('computing disparity...')
disp = stereo.compute(imgL, imgR).astype(np.float32) / 16.0
print('generating 3d point cloud...',)
h, w = imgL.shape[:2]
f = 0.8*w # guess for focal length
Q = np.float32([[1, 0, 0, -0.5*w],
[0,-1, 0, 0.5*h], # turn points 180 deg around x-axis,
[0, 0, 0, -f], # so that y-axis looks up
[0, 0, 1, 0]])
points = cv.reprojectImageTo3D(disp, Q)
colors = cv.cvtColor(imgL, cv.COLOR_BGR2RGB)
mask = disp > disp.min()
out_points = points[mask]
out_colors = colors[mask]
out_fn = 'out.ply'
write_ply(out_fn, out_points, out_colors)
print('%s saved' % out_fn)
cv.imshow('left', imgL)
# cv.imshow('right', imgR)
cv.imshow('disparity', (disp-min_disp)/num_disp)
cv.waitKey()
print('Done')
if __name__ == '__main__':
print(__doc__)
main()
cv.destroyAllWindows()
复制代码
Simple example of stereo image matching and point cloud generation.
Resulting .ply file cam be easily viewed using MeshLab ( http://meshlab.sourceforge.net/ )
loading images...
computing disparity...
generating 3d point cloud...
out.ply saved
Done
复制代码