本文中我们准备探索允许3D重建在实践中工作的核心概念。具体来说,我们将了解多视图或对极几何的基础知识。 让我们来看一下现代针孔相机的功能。当一幅图像被拍摄时,它的信息呈现在二维平面上,在二维平面上,每个点(或像素)与相机镜头之间的距离是不存在的。但这正是我们为了使用2D图像执行3D重建所需的信息。我们的解决方案的关键在于使用第二个摄像头拍摄同一物体的照片,并比较每个图像来提取深度信息。我们的眼睛在类似的情况下执行着同样的任务。不过,OpenCV包含的工具可以帮助我们在非人类针孔相机拍摄的图像中看到深度。 我们先分析下面这张图,该图说明了两个摄像机拍摄同一场景照片的场景中多视图几何的一些基本概念。 通过检查从左侧摄像机的中心点发出的上图所示的OX线,我们可以看到该沿线有3个点,我们需要知道它们与O点的距离。如前所述我们并不能够仅从左侧相机中提取此信息。然而,对于右侧相机,我们可以看到X的3个点可以沿其图像平面上的一条直线投影(直线用:l '表示)。这条线被称为核线(epiline); 必须出现沿着OX线的任何点X的线。重要的是,当我们在右侧相机的图像中搜索X的匹配点时,我们已经知道它将沿着该核线出现,因此大大减少了我们的搜索工作量。此外,我们可以假设在左侧图像中出现的每个点将始终具有在右侧图像中找到的伴随的核线。这被称为极线约束。 需要注意的另一个重要方面是核点(epipole)。核点是连接到每个摄像机中心点的线在核面内的交点。在上面的图中,0和0 '分别表示在两端。点e和e'是对方相机的中心点出现在对应图像中的点。通过将点X(我们试图提取其深度值的点)包含在核点线上,我们可以导出这个平面表示为X00'的平面。该平面称为极线平面。 我们还可以在上图中看到,右侧图像中的X点的投影与相对的相机(e')的核点相交。这显示了所有的极线将如何通过图像中找到的核点(相对照相机的真实中心点将出现的图像点)。这一点的重要性在于我们能够通过找到多个核线相交的图像点来精确计算出核点在我们的图像中的位置。我们还应该注意,在很多情况下,在相对图像的帧中可能没有捕获一个或两个相机中心点,这意味着该极点将落在其2D像素矩阵之外。尽管如此,我们仍然可以通过分析核线精确地计算出虚构的核点位置,从而达到提取深度信息的最终目标。 由于我们现在已经涵盖了对极几何的基础知识,我们现在可以解决另外两个元素,以便找到核点和核线。 基本矩阵包含有关平移和旋转的信息,它描述了相机相对于其对应物的位置。下图展示了基本矩阵。 但是对于我们的解决方案,我们希望根据相机像素坐标进行计算。这是基本矩阵发挥作用的地方,因为它包含与实际矩阵相同的信息,但也包括关于两个相机的内在信息,以便我们可以用这样的术语将两者联系起来。虽然基本矩阵中的信息来源于我们相机的真实世界定位,但实际矩阵必须自己计算。基本矩阵允许我们做的是将一个图像中的单个点映射到另一个图像中的相应的极线,为我们提供一个起点,以便之后找到两个摄像机中心点和极平面之间的核线。 但是我们如何计算实际矩阵?我们可以使用OpenCV从每个图像中的一组已知匹配点推导出这个矩阵。这基本上是一个校准过程,建议从图像中找到最少8个匹配点,并用于达到所需的准确度。为此,我们使用OpenCV分析两个图像并提取其最佳匹配像素坐标。在下面的Python代码中,我们使用SIFT描述符和基于FLANN的matcher和ratio文本提取这些点。 import cv2import numpy as npfrom matplotlib import pyplot as pltimg1 = cv2.imread('myleft.jpg',0) #queryimage # left imageimg2 = cv2.imread('myright.jpg',0) #trainimage # right imagesift = cv2.SIFT()# find the keypoints and descriptors with SIFTkp1, des1 = sift.detectAndCompute(img1,None)kp2, des2 = sift.detectAndCompute(img2,None)# FLANN parametersFLANN_INDEX_KDTREE = 0index_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)good = []pts1 = []pts2 = []for i,(m,n) in enumerate(matches): if m.distance < 0.8*n.distance:="" good.append(m)="" pts2.append(kp2[m.trainidx].pt)=""> 这将为我们提供两个图像中最佳像素匹配的列表,然后可以将其发送以计算实际矩阵,我们将在下面使用OpenCV的功能进行计算。Python代码如下: pts1 = np.int32(pts1)pts2 = np.int32(pts2)F, mask = cv2.findFundamentalMat(pts1,pts2,cv2.FM_LMEDS)# We select only inlier pointspts1 = pts1[mask.ravel()==1]pts2 = pts2[mask.ravel()==1] 让我们花点时间创建一个Python函数,它将在我们的图像上绘制线条,这些线条稍后将用于可视化核线。 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 = cv2.cvtColor(img1,cv2.COLOR_GRAY2BGR) img2 = 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 = cv2.line(img1, (x0,y0), (x1,y1), color,1) img1 = cv2.circle(img1,tuple(pt1),5,color,-1) img2 = cv2.circle(img2,tuple(pt2),5,color,-1) return img1,img2 现在我们继续计算与相对图像中的点相对应的核线。在这个阶段,我们还需要注意我们正在使用哪个相机,因为我们将在一个图像中找到点,并在其对应物上绘制核线。下面的代码将生成一个行数组,我们可以随后将其发送到绘图函数。Python代码如下: # Find epilines corresponding to points in right image (second image) and# drawing its lines on left imagelines1 = cv2.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 imagelines2 = cv2.computeCorrespondEpilines(pts1.reshape(-1,1,2), 1,F)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() 结果显示两幅带有核线的图像,它们代表了来自相反图像的点数组。我们可以注意到,每一组线的集合点都在摄像机视图之外。这个虚构的点是核点(即在三维空间中,相对摄像机的中心点)。 最后,我们应该认识到在尝试进行3D重建时相机分辨率的重要性。这是由于在我们的过程中,第一步我们必须使用一种算法来识别每幅图像中非常匹配的像素,这样我们就可以产生一个精确的基本矩阵。分辨率越低,我们处理的信息就越少,因此极大地阻碍了保持精度的能力。尽管如此,我们仍在用立体图像进行全面三维重建的,我们的最后一步是使用第二个相应的极线计算第一个图像上的点的深度。 |
|