目标
在本节中,
- 我们将学习多视图几何的基础知识
- 我们将了解什么是对极点、对极线、对极约束等。
基本概念
当我们使用针孔相机拍摄图像时,我们会丢失一个重要的信息,即图像的深度。或者图像中的每个点距离相机有多远,因为这是一个 3D 到 2D 的转换。因此,使用这些相机是否可以找到深度信息是一个重要的问题。答案是使用多个相机。我们的眼睛以类似的方式工作,我们使用两个相机(两只眼睛),这被称为立体视觉。因此,让我们看看 OpenCV 在这个领域提供了什么。
(Gary Bradsky 的Learning OpenCV在这方面有很多信息。)
在进入深度图像之前,让我们首先了解多视图几何中的一些基本概念。在本节中,我们将处理对极几何。请看下面的图像,它显示了一个基本设置,其中两个相机拍摄同一场景的图像。
image
如果我们只使用左侧相机,我们将无法找到图像中点 \(x\) 对应的 3D 点,因为直线 \(OX\) 上的每个点都投影到图像平面上的同一点。但是也考虑右侧图像。现在,直线 \(OX\) 上的不同点投影到右侧平面中的不同点(\(x'\))。因此,通过这两个图像,我们可以三角化正确的 3D 点。这就是整个想法。
\(OX\) 上不同点的投影在右侧平面上形成一条线(线 \(l'\))。我们称之为对应于点 \(x\) 的对极线。这意味着,要在右侧图像上找到点 \(x\),请沿着这条对极线搜索。它应该在这条线上的某个地方(这样想,要在另一张图像中找到匹配点,您无需搜索整个图像,只需沿着对极线搜索即可。因此,它提供了更好的性能和准确性)。这被称为对极约束。类似地,所有点在另一张图像中都会有相应的对极线。平面 \(XOO'\) 被称为对极平面。
\(O\) 和 \(O'\) 是相机中心。从上面给出的设置中,您可以看到右侧相机 \(O'\) 的投影在左侧图像的点 \(e\) 处看到。它被称为对极点。对极点是通过相机中心的直线与图像平面的交点。类似地,\(e'\) 是左侧相机的对极点。在某些情况下,您将无法在图像中找到对极点,它们可能在图像外部(这意味着,一个相机看不到另一个相机)。
所有对极线都穿过其对极点。因此,要找到对极点的位置,我们可以找到许多对极线并找到它们的交点。
因此,在本节中,我们将重点关注查找对极线和对极点。但是要找到它们,我们需要另外两种成分,基础矩阵 (F) 和本质矩阵 (E)。本质矩阵包含有关平移和旋转的信息,这些信息描述了第二个相机相对于第一个相机在全局坐标中的位置。请看下面的图像(图像由 Gary Bradsky 的 Learning OpenCV 提供)
image
但是我们更喜欢以像素坐标进行测量,对吧?除了两个相机的内部参数信息之外,基础矩阵包含与本质矩阵相同的信息,以便我们可以在像素坐标中关联两个相机。(如果我们使用校正后的图像并通过除以焦距来归一化该点,则 \(F=E\))。简而言之,基础矩阵 F 将一个图像中的点映射到另一个图像中的一条线(对极线)。这是从两个图像的匹配点计算出来的。需要至少 8 个这样的点才能找到基础矩阵(在使用 8 点算法时)。更喜欢使用更多的点,并使用 RANSAC 来获得更稳健的结果。
代码
因此,首先我们需要找到两个图像之间尽可能多的匹配项才能找到基础矩阵。为此,我们使用带有基于 FLANN 匹配器的 SIFT 描述符和比率测试。
import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
img1 =
cv.imread(
'myleft.jpg', cv.IMREAD_GRAYSCALE)
img2 =
cv.imread(
'myright.jpg', cv.IMREAD_GRAYSCALE)
sift = cv.SIFT_create()
kp1, des1 = sift.detectAndCompute(img1,None)
kp2, des2 = sift.detectAndCompute(img2,None)
FLANN_INDEX_KDTREE = 1
index_params = dict(algorithm = FLANN_INDEX_KDTREE, trees = 5)
search_params = dict(checks=50)
matches = flann.knnMatch(des1,des2,k=2)
pts1 = []
pts2 = []
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)
基于 Flann 的描述符匹配器。
定义 features2d.hpp:1294
CV_EXPORTS_W Mat imread(const String &filename, int flags=IMREAD_COLOR_BGR)
从文件加载图像。
现在我们有了两个图像的最佳匹配列表。让我们找到基础矩阵。
pts1 = np.int32(pts1)
pts2 = np.int32(pts2)
pts1 = pts1[mask.ravel()==1]
pts2 = pts2[mask.ravel()==1]
Mat findFundamentalMat(InputArray points1, InputArray points2, int method, double ransacReprojThreshold, double confidence, int maxIters, OutputArray mask=noArray())
从两个图像中的对应点计算基础矩阵。
接下来,我们找到对极线。对应于第一个图像中的点的对极线绘制在第二个图像上。因此,在此处提及正确的图像很重要。我们得到一个线数组。因此,我们定义一个新函数以在图像上绘制这些线。
def drawlines(img1,img2,lines,pts1,pts2)
''' img1 - 我们在 img2 中的点的对极线上绘制图像
lines - 相应的对极线 '''
r,c = img1.shape
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)
return img1,img2
void cvtColor(InputArray src, OutputArray dst, int code, int dstCn=0, AlgorithmHint hint=cv::ALGO_HINT_DEFAULT)
将图像从一个颜色空间转换为另一个颜色空间。
void line(InputOutputArray img, Point pt1, Point pt2, const Scalar &color, int thickness=1, int lineType=LINE_8, int shift=0)
绘制连接两点的线段。
void circle(InputOutputArray img, Point center, int radius, const Scalar &color, int thickness=1, int lineType=LINE_8, int shift=0)
Draws a circle.
现在我们找到两个图像中的对极线并绘制它们。
lines1 = lines1.reshape(-1,3)
img5,img6 = drawlines(img1,img2,lines1,pts1,pts2)
lines2 = lines2.reshape(-1,3)
img3,img4 = drawlines(img2,img1,lines2,pts2,pts1)
plt.subplot(121),plt.imshow(img5)
plt.subplot(122),plt.imshow(img3)
plt.show()
void computeCorrespondEpilines(InputArray points, int whichImage, InputArray F, OutputArray lines)
对于立体像对中的图像中的点,计算另一张图像中的相应对极线。
以下是我们得到的结果
image
您可以在左侧图像中看到所有对极线都在右侧图像外部的一个点处会聚。那个会聚点就是对极点。
为了获得更好的结果,应使用具有良好分辨率和许多非平面点的图像。
练习
- 一个重要的主题是相机的前向运动。然后,对极点将在两者中的相同位置看到,对极线从一个固定点发出。请参阅此讨论。
- 基础矩阵估计对匹配质量、异常值等很敏感。当所有选定的匹配项都位于同一平面上时,情况会变得更糟。请检查此讨论。