目标
在本节中,
- 我们将学习多视图几何的基础知识。
- 我们将了解什么是极点、极线、极线约束等。
基本概念
当我们使用针孔相机拍摄图像时,会丢失一个重要的信息,即图像的深度。或者说图像中每个点到相机的距离是多少,因为这是一个三维到二维的转换。因此,一个重要的问题是如何使用这些相机找到深度信息。答案是使用多个相机。我们的眼睛以类似的方式工作,我们使用两个相机(两只眼睛),这被称为立体视觉。那么让我们看看OpenCV在这个领域提供了什么。
(Gary Bradsky 的《学习OpenCV》一书对此领域有很多信息。)
在深入研究深度图像之前,让我们首先了解多视图几何中的一些基本概念。在本节中,我们将讨论极线几何。请看下图,它显示了一个基本的设置,其中两个相机拍摄同一场景的图像。
图像
如果我们只使用左相机,我们就无法找到与图像中点\(x\)对应的三维点,因为线\(OX\)上的每个点都投影到图像平面上相同的点。但是也考虑右图像。现在,线\(OX\)上的不同点投影到右平面上的不同点(\(x'\))。因此,通过这两幅图像,我们可以对正确的三维点进行三角测量。这就是整个思想。
线\(OX\)上不同点的投影在右平面上形成一条线(线\(l'\))。我们称它为点\(x\)对应的**极线**。这意味着,要在右图像上找到点\(x\),沿着这条极线搜索。它应该位于这条线上(这样想,要在另一幅图像中找到匹配点,不需要搜索整幅图像,只需沿着极线搜索即可。因此,它提供了更好的性能和精度)。这称为**极线约束**。类似地,所有点在另一幅图像中都将有其对应的极线。平面\(XOO'\)称为**极平面**。
\(O\)和\(O'\)是相机中心。从上面给出的设置中,您可以看到右相机\(O'\)的投影在左图像上的点\(e\)处可见。这称为**极点**。极点是穿过相机中心和图像平面的线的交点。类似地,\(e'\)是左相机的极点。在某些情况下,您将无法在图像中找到极点,它们可能位于图像之外(这意味着一个相机看不到另一个相机)。
所有极线都经过其极点。因此,为了找到极点的位置,我们可以找到许多极线并找到它们的交点。
所以在本节中,我们专注于寻找极线和极点。但是为了找到它们,我们需要另外两个要素,**基础矩阵 (F)** 和**本质矩阵 (E)**。本质矩阵包含关于平移和旋转的信息,这些信息描述了第二个相机相对于第一个相机在全局坐标系中的位置。请看下图(图片来自:Gary Bradsky 的《学习OpenCV》)
图像
但是我们更希望用像素坐标进行测量,对吧?基础矩阵包含与本质矩阵相同的信息,此外还包含关于两个相机内参数的信息,以便我们可以在像素坐标中关联这两个相机。(如果我们使用校正图像并通过焦距除以点进行归一化,则\(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)
返回 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)
绘制一个圆。
现在我们找到两幅图像中的极线并绘制它们。
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)
对于立体图像对中的一幅图像中的点,计算另一幅图像中对应的极线。
以下是我们得到的结果
图像
您可以在左图像中看到所有极线都在图像右侧的一个点之外汇聚。该交点是极点。
为了获得更好的结果,应该使用具有良好分辨率和许多非平面点的图像。
练习
- 一个重要的主题是相机的向前运动。然后,极点将出现在两个图像的相同位置,极线从一个固定点出现。参见此讨论。
- 基本矩阵估计对匹配质量、异常值等很敏感。当所有选择的匹配点都位于同一平面上时,情况会变得更糟。查看此讨论。