当我们使用针孔相机拍摄图像时,我们会丢失一个重要的信息,即图像的深度。或者图像中的每个点距离摄像机有多远,因为这是一个3d到2d的转换。因此,我们能否利用这些相机找到深度信息是一个重要的问题。答案是使用多个相机。我们眼睛的工作原理类似于我们使用两个摄像头(两只眼睛),这被称为立体视觉。那么让我们来看看OpenCV在这个领域提供了什么。
在讨论深度图像之前,让我们先了解多视图几何中的一些基本概念。在这一节中,我们将讨论极面几何。请看下面的图片,它显示了两个相机拍摄同一场景的基本设置。
如果我们只使用左侧摄像机,我们无法在图像中找到点 x x x对应的3D点,因为直线 O X OX OX上的每个点都投影到图像平面上的同一点。但也要考虑正确的图像。现在直线 O X OX OX上的不同点投影到右平面上的不同点 x ′ x' x′。有了这两张图像,我们就能三角测量出正确的三维点。这就是整个想法。
各点在 O X OX OX上的投影在右平面上形成一条直线(直线 l ’ l’ l’)。我们称它为对应于点 x x x的对极线,意思是,要找到右边图像上的点x,就沿着这个对极线搜索。它应该在这条线上的某个地方(这样想,要在其他图像中找到匹配点,你不需要搜索整个图像,只需要沿着对极线搜索。因此它提供了更好的性能和准确性)。这被称为外极约束。同样地,在另一张图中,所有点都有相应的对极线。平面 X O O ′ XOO' XOO′被称为对极平面。
O O O和 O ′ O' O′是相机中心。从上面给出的设置中,你可以看到右边相机 O ′ O' O′的投影在左边图像的点 e e e上,它被称为极点。极点是相机中心与图像平面的交点。同理, e ′ e' e′是左边相机的极点。在某些情况下,你将无法在图像中定位极点,它们可能在图像之外(这意味着,一个相机看不到另一个)。
所有的极线都经过它的极点。因此,为了找到极点的位置,我们可以找到许多的极线并找到它们的交点。
在这节中,我们将重点寻找偶极线和偶极。但要找到它们,我们还需要两个要素,基本矩阵( F F F)和本质矩阵( E E E)。本质矩阵包含关于平移和旋转的信息,它们描述了第二个摄像机相对于第一个摄像机在全局坐标中的位置。
但是我们更喜欢用像素坐标进行测量,对吧?基本矩阵除了包含与本质矩阵相同的信息外,还包含了两个相机的内在信息,因此我们可以将两个相机在像素坐标上联系起来。(如果我们使用校正图像,并通过除以焦距归一化点,F=E)。简单地说,基本矩阵 F F F将一个图像中的点映射到另一个图像中的一条线(极线)。这是根据两幅图像中的匹配点计算出来的。至少需要8个这样的点来找到基本矩阵(使用8点算法)。更多的点更好,然后使用RANSAC得到更鲁棒的结果。
import numpy as np
import cv2
from matplotlib import pyplot as plt
# 读入图片
img1 = cv2.imread("assets/left.jpg", 0)
img2 = cv2.imread("assets/right.jpg", 0)
# 定义SIFT算子
sift = cv2.SIFT_create()
# 找到特征点和计算特征
kp1, des1 = sift.detectAndCompute(img1,None)
kp2, des2 = sift.detectAndCompute(img2,None)
# FLANN 参数
FLANN_INDEX_KDTREE = 0
index_params = dict(algorithm = FLANN_INDEX_KDTREE, trees = 5)
search_params = dict(checks = 50)
flann = cv2.FlannBasedMatcher(index_params,search_params)
matches = flann.knnMatch(des1,des2,k=2)
# 选择匹配点中匹配度比较好的点
pts1 = []
pts2 = []
for i, (m,n) in enumerate(matches):
if m.distance < 0.76*n.distance:
pts2.append(kp2[m.trainIdx].pt)
pts1.append(kp1[m.queryIdx].pt)
# 类型转换
pts1 = np.int32(pts1)
pts2 = np.int32(pts2)
# 找到基本矩阵
F, mask = cv2.findFundamentalMat(pts1,pts2,cv2.FM_LMEDS)
# 选择内部的点
pts1 = pts1[mask.ravel()==1]
pts2 = pts2[mask.ravel()==1]
## 画极线
def drawepilines(img1, img2, lines, pts1, pts2):
r,c = img1.shape
img1_temp = cv2.cvtColor(img1, cv2.COLOR_GRAY2BGR)
img2_temp = cv2.cvtColor(img2, cv2.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_temp = cv2.line(img1_temp,(x0,y0),(x1,y1),color,1)
img1_temp = cv2.circle(img1_temp, tuple(pt1), 5, color, -1)
img2_temp = cv2.circle(img2_temp, tuple(pt2), 5, color, -1)
return img1_temp, img2_temp
# 根据右图的点找到左图的极线
lines1 = cv2.computeCorrespondEpilines(pts2.reshape(-1,1,2),2,F)
lines1 = lines1.reshape(-1,3)
img5, img6 = drawepilines(img1,img2,lines1,pts1,pts2)
# 根据左图的点找到右图的极线
lines2 = cv2.computeCorrespondEpilines(pts1.reshape(-1,1,2),1,F)
lines2 = lines2.reshape(-1,3)
img3, img4 = drawepilines(img2,img1,lines2,pts2,pts1)
res = np.hstack((img5, img3))
cv2.imshow("res", res)
# res1 = np.hstack((img5,img6))
# res2 = np.hstack((img4,img3))
# cv2.imshow("res1", res1)
# cv2.imshow("res2", res2)
cv2.waitKey(0)
cv2.destroyAllWindows()
NOTE: 这里极线的计算与选择的特征点有非常大的关系,要有足够好的特征来表明是同一个位置。
cv2.findFundamentalMat( points1, points2, method, ransacReprojThreshold, confidence, maxIters[, mask] ) -> retval, mask
cv2.findFundamentalMat( points1, points2[, method[, ransacReprojThreshold[, confidence[, mask]]]] ) -> retval, mask
cv2.findFundamentalMat( points1, points2, params[, mask] ) -> retval, mask
通过两个图像中对应的点,计算基本矩阵
- points1: 第一幅图像的 N 个点集合,点的坐标必须是浮点数,单精度或双精度都可以
- points2: 第二幅图像的 N 个点集合
- method: 计算基本矩阵的方法
- FM_7POINT: 7 个点的算法. N=7
- FM_8POINT: 8 个点的算法. N≥8
- FM_RANSAC: RANSAC 算法 N≥8
- FM_LMEDS: LMedS 算法 N≥8
- ransacReprojThreshold: RANSAC 方法的参数. 它是一个点到极线的最大距离(单位为像素),超过这个距离的点被认为是一个离群值,不用于计算最终的基本矩阵。它可以设置为1-3,这取决于点定位的精度、图像分辨率和图像噪声。
- confidence: 仅用于RANSAC和lmed方法。它指定了估计矩阵正确的理想置信水平(概率)。
- [out] mask: 可选的输出 mask
- maxIters: 最多迭代次数
对极几何是这么描述的:
[ p 2 ; 1 ] T F [ p 1 ; 1 ] = 0 [p_2;1]^TF[p_1;1]=0 [p2;1]TF[p1;1]=0
F F F 是基础矩阵, p 2 p_2 p2 和 p 1 p_1 p1 分别对应第二张和第一张图像的点;
cv2.computeCorrespondEpilines( points, whichImage, F[, lines] ) -> lines
通过一个图像中的点找到另一幅图像中的极线
- points: 输入的点集, N × 1 N×1 N×1或 1 × N 1×N 1×N,类型为 CV_32FC2或者 Point2f的vector都可以
- whichImage: 包含那些点的图像的索引(1或2)
- F: 基础矩阵,用 findFundamentalMat 或 stereoRectify 计算;
- lines: 输出的极线, a x + b y + c = 0 ax+by+c=0 ax+by+c=0, 直线方程通过 ( a , b , c ) (a,b,c) (a,b,c)描述。