正文字数:4509 阅读时长:2分钟
欢迎来到本教程的第三部分,也是最后一部分关于立体重建的教程。
Posted by Omar Padierna
url : https://medium.com/@omar.ps16/stereo-3d-reconstruction-with-opencv-using-an-iphone-camera-part-iii-95460d3eddf0
快速回顾:
在第一部分中,我们简要介绍了立体三维重建所需的步骤以及立体重建工作的原理和要点。
在第二部分中,我们分析了一个脚本来计算摄像机矩阵和失真系数。这些都是三维重建过程中相机的固有参数。
一旦我们的相机被校准,我们就可以利用来自同一个物体的一对照片完成重建。在大多数立体声应用程序中,你会发现每张照片都是从两个单独的摄像头拍摄的,如下图所示
用于三维重建的典型双摄像头系统
人们这样做的原因是因为两个摄像头在同一高度(比如我们的眼睛)是非常重要的。我们在这个教程中,我们只是使用了手机的摄像头,没有使用这种相机,因此我们不需要进行类似的设置。如果你想制作自己的双摄像头系统以获得更好的效果,那么就你可以去尝试阅读Daniel Lee的博客。
我们仍然需要一对图片来生成视差图。在这种情况下,我让别人给我拍了两张照片,同时小心地水平移动相机,确保没有垂直移动(说起来容易做起来难)。如果用手或是自己动手太困难,你可以用三脚架。
我胖乎乎的样子
一旦我们有了照片,我们就要花一些时间开始写一些代码。我们将从加载摄像机矩阵和现实上面得到的图片开始。作为一个友好的提醒,请记住完整的脚本可以在这里(https://github.com/OmarPadierna/3DReconstruction)找到。
#=========================================================
# Stereo 3D reconstruction
#=========================================================
#Load camera parameters
ret = np.load('./camera_params/ret.npy')
K = np.load('./camera_params/K.npy')
dist = np.load('./camera_params/dist.npy')
#Specify image paths
img_path1 = './reconstruct_this/left2.jpg'
img_path2 = './reconstruct_this/right2.jpg'
#Load pictures
img_1 = cv2.imread(img_path1)
img_2 = cv2.imread(img_path2)
#Get height and width. Note: It assumes that both pictures are the same size. They HAVE to be same size
h,w = img_2.shape[:2]
然后,基于自由缩放参数,计算出最佳摄像机矩阵。实际上,该算法需要算出一种新的摄像机矩阵,如果我们改变图像大小的话。虽然我们没有实际改变它,但我注意到,通过该算法得到的摄像机矩阵在消除失真方面会得到更好的结果。
#Get optimal camera matrix for better undistortion
new_camera_matrix, roi = cv2.getOptimalNewCameraMatrix(K,dist,(w,h),1,(w,h))
#Undistort images
img_1_undistorted = cv2.undistort(img_1, K, dist, None, new_camera_matrix)
img_2_undistorted = cv2.undistort(img_2, K, dist, None, new_camera_matrix)
#Downsample each image 3 times (because they're too big)
img_1_downsampled = downsample_image(img_1_undistorted,3)
img_2_downsampled = downsample_image(img_2_undistorted,3)
最后,一旦图像没有失真,我们就对他们进行降采样。
降采样有两个功能:1)提高图像处理速度 2)在计算视差图时帮助调整参数
在关于特征匹配算法中,了解图像的大小是非常重要的。这是因为对于我们使用的算法,我们需要指定一个窗口大小。窗口大小越大,相对应的需要计算时间越长。
如果窗口大小不够大,那么视差将无法正确计算,您将得到一个包含各种噪声的深度图(或不完整的深度图)。这对我们的目标是不利的,所以最好对图像进行降采样。本教程中暂时不讨论用于对图像进行降采样的函数,但它会在完整脚本(https://github.com/OmarPadierna/3DReconstruction/blob/master/Reconstruction/disparity.py)的顶部进行声明。
必须指出的是,通过对图像进行降采样,我们不可避免地会丢失信息,因此我们的深度精度也会受到影响。在我看来,如果深度精度对你很重要,那么你最好使用基于激光或红外传感器来绘制深度图。众所周知,立体深度图并不是十分准确。
一旦图像准备好进行处理,我们就可以使用特征匹配算法。根据《学习opencv3》(http://shop.oreilly.com/product/0636920044765.do)一书,立体匹配的标准典型技术是块匹配。opencv提供了两种块匹配实现:立体块匹配和半全局块匹配(SGBM)。两种算法相似,但有区别。
块匹配的关键是在可视区域重叠的两幅图像之间寻找强匹配点。通俗地说,这意味着算法将在捕获同一对象(即相同的事物)的两张图片中寻找相同的像素。
块匹配侧重于高纹理图像(比如树的图片),而半全局块匹配则侧重于子像素级的匹配和纹理更平滑的图片(比如走廊的图片)。
在本教程中,我们使用SGBM,因为这些照片是在室内拍摄的,而且其中有许多平滑的纹理。该算法有三个重要的步骤需要理解。
如果没有对这些步骤的直观的理解,使用SGBM算法将非常困难,因为它接收到的参数取决于对它正在做什么的理解(即使是表面和肤浅的)。
实际上,该算法有3个步骤:
1. 预过滤图像,用于归一化亮度,增强纹理
2. 使用SAD窗口沿水平极线执行相对应的搜索
3. 后过滤图像,以消除不良的相关匹配。
为了完成亮度归一化并增强纹理操作,我们在图像上运行一个窗口(至少5x5,最大21x21)。修改这个窗口大小的参数在代码中称之为win_size。
然后通过滑动SAD窗口来计算相关性。在继续执行之前,从概念上理解什么是极线是很重要的。OpenCV有一个很好的教程,教你如何编写一些代码来可视化它们。
为了更好地理解极线,我们可以做以下练习。手放在脸中间,闭上左眼。然后做对位操作(即闭上右眼,睁开左眼)。你会注意到你手的位置有轻微的变化。
一个我理解下的直观解释
因为你的眼睛处于不同的位置,一只眼睛可以看到另一只眼睛看不到的东西。只睁开一只眼,你就看不见你手上的3D点,因为所有的点都投射到你脸上相同的同一图像平面上(即你看不到背后是什么东西)。
然而,另一只眼睛既可以看到它的相对部分在看什么,也可以看到由于它们之间的分离而隐藏的一些东西。再试一次,用你的眼睛亲眼看看,你会注意到,用一只眼睛你能看到某些东西(尤其是在背景中),而另一只眼睛却看不到。
好吧,那又怎样?好吧,当你改变哪只眼睛睁开,哪只眼睛闭上时,你会无意识地把焦点转移到你感兴趣的东西上(在这个例子中是你的手),你可以通过跟随一条线来实现。这条线被称为“极线”。
通过合并双眼的信息,你就可以对你所看到的东西的三维坐标进行三角测量,这就是你理解深度的方法。
相机的原理是一样的,当你用两个平行的相机拍一张照片(或者在一种情况下,两张照片用同一个相机移动才能够得到时),你知道一张照片将包含另一张沿极线的点。
OpenCV对极线几何有一个更正式(也更好)(https://docs.opencv.org/3.4.4/da/de9/tutorial_py_epipolar_geometry.html)的解释图片寿命。点进去看看不是件坏事。
对极几何的解释。紫色的线是兴趣点x所在的极线
为什么极线相关?好吧,因为在对图像进行去失真处理后,极线是水平的,而且由于我们确定兴趣点将沿着极线找到,这样,通过SGBM算法遍历它们,就能可以找到匹配项。
这就是第二步的全部内容。然而,我们需要告诉它在什么程度上视差(即偏移量)是可以接受的。为此,我们必须规定最小和最大的差距。这里的目标是通过减去它们来计算差异的数量,这是一种指定图像中像素可以移动的可接受范围的方法。
解释最小和最大差异。布拉德斯基和卡勒的《学习OpenCV3》
最后一步是做一些后处理。在进行特征匹配后,有可能出现误报和假证样本(即错误匹配)。为了纠正这些错误,OpenCV有一个唯一性比率,它是匹配值的阈值。
最后,基于块的匹配可能在目标边界附近存在问题(因为一张图片可以看到“后面”,而另一张则看不到,还记得吗?)这就形成了一个由许多微小差异组成的区域,称为“斑点”。为了保护它们,我们必须设置一个斑点窗口,接受这些“斑点”的区域。
在SGBM算法的特定情况下,有一个名为disp12MaxDiff的参数,它指定从左到右计算的差异与从右到左计算的差异之间允许的最大差异。
如果差异之间的差异超过该阈值,则像素将被宣布为未知。
如果你想知道更多更好的解释这些算法的内容,建议阅读Gari Bradski和Adrian Kaehler合著的《Learning Open CV 3》一书。它还有c++版本的3D重建。
在代码方面,这意味着我们必须定义SGBM对象并设置参数,然后计算视差,如下所示:
#Set disparity parameters
#Note: disparity range is tuned according to specific parameters obtained through trial and error.
win_size = 5
min_disp = -1
max_disp = 63 #min_disp * 9
num_disp = max_disp - min_disp # Needs to be divisible by 16
#Create Block matching object.
stereo = cv2.StereoSGBM_create(minDisparity= min_disp,
numDisparities = num_disp,
blockSize = 5,
uniquenessRatio = 5,
speckleWindowSize = 5,
speckleRange = 5,
disp12MaxDiff = 1,
P1 = 8*3*win_size**2,#8*3*win_size**2,
P2 =32*3*win_size**2) #32*3*win_size**2)
#Compute disparity map
print ("\nComputing the disparity map...")
disparity_map = stereo.compute(img_1_downsampled, img_2_downsampled)
#Show disparity map before generating 3D cloud to verify that point cloud will be usable.
plt.imshow(disparity_map,'gray')
plt.show()
请注意,这些参数和我拍摄的图片非常匹配。在实践中,这将需要手动微调,并进行大量的尝试和错误。这就是为什么在将视差图转换为点云之前,将其可视化非常方便的原因。
经过多次的尝试和错误,我的视差图最终是这样的。
我自己的视差图
如你所见,这个视差图在我衬衫的区域有很多死点和斑点。而且,我的嘴不见了,似乎噪声很多。这是因为我没有很好地调整SBGM参数。
当图片被适当地扭曲和SGBM算法被很好地调整,你将得到平滑的视差图,如下所示。这个视差图来自于cones dataset(http://vision.middlebury.edu/stereo/data/)。
光滑的差距地图
优化视差图的最佳方法是在算法的基础上构建一个GUI,并实时优化视差图,以获得更平滑的图像。在未来我将上传一个GUI,以便实时微调,同时我们将使用这个视差图。
一旦我们计算出视差图,我们就必须得到图像中使用的颜色数组。因为我们减少了图像的采样,所以我们需要得到图像的高度和宽度。
更重要的是我们需要得到变换矩阵。这个矩阵负责将深度和颜色重新投影到三维空间中。opencv的文档中有一个转换矩阵的例子。
大多数例子将使用OpenCV文档中的转换矩阵。在我的情况下,事情并不是那么顺利。环顾四周,我发现了一个更通用的矩阵,我的矩阵就是以这个为基础的。
转换矩阵——来自于Didier Stricker教授
#Generate point cloud.
print ("\nGenerating the 3D map...")
#Get new downsampled width and height
h,w = img_2_downsampled.shape[:2]
#Load focal length.
focal_length = np.load('./camera_params/FocalLength.npy')
#Perspective transformation matrix
#This transformation matrix is from the openCV documentation, didn't seem to work for me.
Q = np.float32([[1,0,0,-w/2.0],
[0,-1,0,h/2.0],
[0,0,0,-focal_length],
[0,0,1,0]])
#This transformation matrix is derived from Prof. Didier Stricker's power point presentation on computer vision.
#Link : https://ags.cs.uni-kl.de/fileadmin/inf_ags/3dcv-ws14-15/3DCV_lec01_camera.pdf
Q2 = np.float32([[1,0,0,0],
[0,-1,0,0],
[0,0,focal_length*0.05,0], #Focal length multiplication obtained experimentally.
[0,0,0,1]])
#Reproject points into 3D
points_3D = cv2.reprojectImageTo3D(disparity_map, Q2)
#Get color points
colors = cv2.cvtColor(img_1_downsampled, cv2.COLOR_BGR2RGB)
#Get rid of points with value 0 (i.e no depth)
mask_map = disparity_map > disparity_map.min()
#Mask colors and points.
output_points = points_3D[mask_map]
output_colors = colors[mask_map]
#Define name for output file
output_file = 'reconstructed.ply'
#Generate point cloud
print ("\n Creating the output file... \n")
create_output(output_points, output_colors, output_file)
实际生成点云的算法与我在OpenCV示例中找到的算法完全相同。它是在实际脚本中声明的,不在本教程的范围之内。本质上,它会重塑颜色和顶点的形状,然后将它们一个一个地堆叠起来。
结果生成的数组被写入一个带有特定头文件的文本文件中,该头文件保存为.ply文件。这个文件可以用meshlab可视化。就我而言,这是我的结果。
Point cloud of myself
如您所见,图像看起来有噪声和畸变,与视差图的外观非常相似。根据经验,如果你的视差图看起来含有噪声,那么你的点云就会有点失真。
一个好的视差图会产生这样的结果:
平滑视差图的点云
差不多就是这样。你可以通过改进你的拍照方式,你的校准方式和微调SGBM算法中的参数来改善结果。
如果您想要一个更完整的点云,那么您应该在感兴趣的对象周围拍摄几对图像,并将所有三维点连接起来,以获得更密集的点云。
我希望这对你的计算机视觉实验有帮助。