需要多个视图
在相机的针孔模型中,光线从物体上反射出来,照射到胶片上形成图像。因此,沿着同一条光线的所有点都将对应于图像中的一个点。
因此,给定图像中的一个点,不可能确定其在世界上的准确位置,也就是说,我们无法从单个图像中恢复深度。
我们也无法从图像中恢复结构。这方面的一个例子是影子艺术,艺术家用手势制作美丽的影子。我们不可能只通过看阴影就对手势做出任何评价。
在本文中,我们将学习如何使用两个视图来处理这种歧义。
极地约束
假设我给你两张从不同角度拍摄的图像。我在其中一张图片中显示一个点,并要求你在另一张图片上找到它。你会怎么做?这里有一个想法:你可以在图像中的点周围画一个小补丁,然后在另一张图像上滑动,看看它最匹配的地方。
然而,一旦你看了几何图形,你就会意识到你不需要扫描整个图像,因为点必然位于一条线上,如下图所示。
直觉是,在现实世界中,点可以位于连接其投影和相机中心的线上的任何位置。所以我们可以推断,这一点在另一幅图像中的投影可以位于投影线上的任何位置。这条投影线称为外极线。所以现在我们的搜索空间缩小到这一行。这称为极线约束。
在本文中,我们将讨论如何用代数方法求解外极线。在此之前,让我们熟悉对极几何中的关键定义。
关键术语
-
基线:连接两个相机中心的线。
-
极线:投影点所在的线。极线成对出现,每一条都代表一幅图像。
-
极面:包含基线和世界上一点的平面。极面在图像平面的外极线处与图像平面相交。
-
极点:基线与图像平面相交的点。对应于不同点的所有外极线在极点相交。为什么?我们看到极面在外极线处与图像平面相交。现在,对应于不同点的每个极面都有共同的基线。由于极点是基线与图像平面相交的地方,这意味着所有外极线都将以极点为公共点。换句话说,所有的外极线都在极点处相交。此外,极点不必位于图像内,它可以位于扩展图像平面上。
相机矩阵
在本节中,我们将回顾我们将要使用的同质坐标和相机矩阵。如果你已经熟悉它们,或者你已经阅读了我以前关于相机校准的文章,你可以跳过这一节。
同质坐标
考虑点(u,v)。为了以其齐次形式表示它,我们简单地添加了另一个维度:(u,v,1)。这种表示的原因是因为平移和透视投影在齐次空间中成为线性操作;也就是说,它们可以通过矩阵乘法一次性计算出来。我们将在本文的后面部分详细讨论齐次变换矩阵。
齐次坐标的一个关键特性是它们是尺度不变的;意思是,(kx,ky,k)和(x,y,1)表示相同的点,其中k≠0和k∈ R、 要从齐次表示转换为欧几里得表示,我们只需除以最后一个坐标,如下所示:
如果你想一想,从原点到点[x,y,1]的直线也有形式k[x,y,1]。所以我们可以说,图像空间中的一点表示为均匀空间中的光线。
线的齐次表示
:考虑熟悉的方程ax+by+c=0。我们知道它表示通过点(x,y)的线的方程。现在,这个方程也可以表示为l⊺p=0,其中l为(a,b,c),l⊺ 是l的转置,p是(x,y,1)。p本质上是点(x,y)的齐次表示。
现在,l是标度不变的,因为方程l⊺如果与常数相乘,p=0不会改变。因此,我们可以说l是直线的齐次表示。
总之,给定齐次点p,方程l⊺p=0(或p⊺l=0)表示p位于l线上。记住这一点,我们将在讨论基本矩阵时重新讨论它。
外参矩阵
相机外参矩阵是将点的坐标从世界坐标系转换为相机坐标系的基矩阵的变化。它让我们从相机的角度看世界。它是旋转矩阵和平移矩阵的组合-旋转矩阵确定相机的方向,平移矩阵移动相机。方程式可以表示为:
这里的符号如下:
摄像机内参矩阵
一旦我们使用相机外参矩阵获得相机点的坐标,下一步就是将它们投影到相机的图像平面上,以形成图像。这是相机内参矩阵的工作。
相机内参矩阵在我的另一篇文章中进行了深入讨论,但总而言之,相机内参矩阵将相机坐标给定的点投影到相机的图像平面上。它基本上编码了相机胶片的属性。方程式如下:
符号为:
图像形成管道
因此,给定世界上的一个点和一个相机,我们以其齐次形式表示该点,并与外参矩阵相乘,以获得相机帧的坐标。然后我们与内参矩阵相乘,得6到其在相机像平面上的投影。最后,我们转换回欧几里得坐标,以获得点在图像中的像素位置。这是图像形成管道,如下所示:
基本矩阵
好了,我们现在讨论立体几何的基础——基本矩阵。让我们推导它,看看它做了什么。
考虑一个校准的系统,我们知道两个相机的相对位置和方向。让摄像机中心表示为Oc和Oc′。让X成为世界上的一个点。让我们将X在相机Oc的坐标表示为Xc,将在相机Oc′的坐标表示为Xc′。设Rc和Tc是基矩阵从Oc到Oc′的旋转和平移变化。这意味着给定X 在Oc的坐标,我们可以找到它们在Oc’的坐标为:
叉积矩阵
让我们绕开一小段,讨论向量叉积。两个向量a,b的叉积将是垂直于它们的向量,由a×b = [-a3b2 + a2b3, a3b1-a1b3, -a2b1 + a1b2]表示,其中a=[a1,a2,a3]和b=[b1,b2,b3]。
我们可以将其以矩阵形式表示为:
这种矩阵形式的叉积表示为[a×]b,其中[a×]是3×3矩阵,b是3×1向量。
现在,向量与自身的叉积为零。a×a=[a×]a=0。所以我们可以说[a×]是秩为2的斜对称矩阵。
回到我们的方程式:
同时乘以向量Tc,我们得到:
Tc×Tc=0。接下来,我们取两边的点积:
现在,向量𝑇 ×Xc′垂直于Xc′。因此,Xc′.(𝑇 ×Xc′)=0。
现在,Tc和RcXc都是三维向量。因此,我们可以将它们的叉积表示为矩阵形式:
最后,我们可以将方程表示为:
矩阵E称为基本矩阵,它将两个不同相机帧的点的坐标关联起来。
寻找外极线
现在,我们如何使用基本矩阵找到外极线?让我们更深入地看看这个方程式。
这里
Xc
是点
X
相对于相机帧
Oc 的坐标
。这意味着我们可以将连接X和Oc的线上的任何点表示为𝛼Xc其中𝛼 是一个常数。现在,如果我们将Xc替换为𝛼方程中的Xc,它仍然满足。
类似地,我们可以用𝛽Xc′其中𝛽 是常数,方程保持不变。
因此,我们可以说,基本矩阵方程由任意两点满足,这两点位于连接该点及其各自相机中心的投影射线上,其中点的坐标由其相机帧表示。
让x𝑐和xc′是X点在摄像机Oc和O𝑐′ 的像平面上的投影. 它们必须满足基本矩阵方程,因为它们位于投影射线上。所以我们可以写:
现在xc是3×1向量,E是3×3矩阵,所以它们的乘积将是3×1的向量。让我们用l表示:
这个方程式你应该很熟悉。如前一节所述,该方程表示齐次点xc′位于直线l上。我们可以说,l是对应于点xc的外极线,而xc′则位于该直线上。这是极线约束的数学形式。
类似地,如上式所示,l′是对应于点xc′的外极线,xc位于该线上。
因此,给定一个点在一个视图中的投影,我们将其与基本矩阵相乘,以获得另一个视图中点的投影所在的外极线。
这在实践中实现起来有点棘手。
Python示例
这是本节的代码示例:
依赖
%matplotlib widget
import matplotlib.pyplot as plt
from utils import *
from stereo_utils import *
定义相机配置
首先,我们设置一个环境,其中有一个世界点和两个相机以一个角度面对该点
# define parameters for the image plane
f = 2
img_size = (5, 5)
# Define camera 1 configuration
# rotate the camera first at an angle of 90 along the Y axis, then rotate it
# at an angle of 30 along the negative Z-axis
angles = [np.pi/2, -np.pi/6]
order = 'yz'
# translate the camera by an offset
offset1 = np.array([0, -10, 0])
# create rotation transformation matrix
R1 = create_rotation_transformation_matrix(angles, order)
R1_ = np.identity(4)
R1_[:3, :3] = R1
# create translation transformation matrix
T1_ = create_translation_matrix(offset1)
# Define camera 2 configuration and repeat the same steps
angles = [np.pi/2, np.pi/6]
order = 'yz'
offset2 = np.array([0, 10, 0])
R2 = create_rotation_transformation_matrix(angles, order)
R2_ = np.identity(4)
R2_[:3, :3] = R2
T2_ = create_translation_matrix(offset2)
绘制环境
打印整个设置,包括相机、世界点、相机的图像平面和交点。
# define a world point
point = np.array([[-6, 5, 2]])
# create and transform camera 1
xx1, yy1, Z1 = create_image_grid(f, img_size)
pt1_h = convert_grid_to_homogeneous(xx1, yy1, Z1, img_size)
pt1_h_transformed = T1_ @ R1_ @ pt1_h
xxt1, yyt1, Zt1 = convert_homogeneous_to_grid(pt1_h_transformed, img_size)
# create and transform camera 2
xx2, yy2, Z2 = create_image_grid(f, img_size)
pt2_h = convert_grid_to_homogeneous(xx2, yy2, Z2, img_size)
pt2_h_transformed = T2_ @ R2_ @ pt2_h
xxt2, yyt2, Zt2 = convert_homogeneous_to_grid(pt2_h_transformed, img_size)
# define axis and figure
fig = plt.figure(figsize=(8, 6))
ax = fig.add_subplot(111,projection='3d')
# set limits
ax.set(xlim=(-10, 5), ylim=(-15, 15), zlim=(-3, 10))
# plot both the camera centers
ax = pr.plot_basis(ax, R1, offset1, label="camera 1")
ax = pr.plot_basis(ax, R2, offset2, label="camera 2")
# plot both the image planes
ax.plot_surface(xxt1, yyt1, Zt1, alpha=0.75)
ax.plot_surface(xxt2, yyt2, Zt2, alpha=0.75)
# plot baseline
ax.plot(*make_line(offset1, offset2), color="red", alpha=0.5, label="baseline")
# plot the world point
ax.scatter(*point[0], color="black")
ax.plot(*make_line(point, offset1), color="purple", alpha=0.25)
ax.plot(*make_line(point, offset2), color="purple", alpha=0.25)
# intersection points (manually computed with trial and error)
c1_intn_world = offset1 + (point[0] - offset1) * 0.16
ax.scatter(*c1_intn_world, color="green")
c2_intn_world = offset2 + (point[0] - offset2) * 0.26
ax.scatter(*c2_intn_world, color="green")
ax.set_title("stereo geometry")
ax.set_xlabel("X-axis")
ax.set_ylabel("Y-axis")
ax.set_zlabel("Z-axis")
plt.legend()
计算摄像机上点的投影
# create a simple camera intrinsic matrix with focal length f
# and use it for both the cameras
K = compute_intrinsic_parameter_matrix(f, 0, 1, 0, 0)
# create the projection matrix and compute the projection of the world point for both the cameras
# compute projection for camera 1
E1 = np.linalg.inv(T1_ @ R1_)
E1_ = E1[:-1, :]
M1 = K @ E1_
proj_point1 = compute_world2img_projection(point.reshape(3, -1), M1, is_homogeneous=False)
# compute projection for camera 2
E2 = np.linalg.inv(T2_ @ R2_)
E2_ = E2[:-1, :]
M2 = K @ E2_
proj_point2 = compute_world2img_projection(point.reshape(3, -1), M2, is_homogeneous=False)
绘制两个相机的点投影
h, w = img_size
nrows = 1
ncols = 2
fig, axes = plt.subplots(nrows=nrows, ncols=ncols, figsize=(6, 4))
# plot projection for camera 1
ax1 = axes[0]
ax1.set(xlim = (-(h // 2), w // 2), ylim = (-(h // 2), w // 2))
ax1.set_title("Camera 1")
ax1.scatter(*proj_point1.reshape(-1))
# plot projection for camera 2
ax2 = axes[1]
ax2.set(xlim = (-(h // 2), w // 2), ylim = (-(h // 2), w // 2))
ax2.set_title("Camera 2")
ax2.scatter(*proj_point2.reshape(-1))
plt.tight_layout()
基本矩阵
# convert the world point to homogeneous coords
point_hg = to_hg_coords(point.T)
计算两台摄像机的世界点坐标
point_c1 = E1_ @ point_hg # coordinates of the point wrt camera 1
print("coordinates of the point wrt camera 1:", "\n", point_c1, "\n")
point_c2 = E2_ @ point_hg # coordinates of the point wrt camera 2
print("coordinates of the point wrt camera 2:", "\n", point_c2)
coordinates of the point wrt camera 1:
[[ 2. ]
[ 9.99038106]
[12.69615242]]
coordinates of the point wrt camera 2:
[[ 2. ]
[-1.33012702]
[ 7.69615242]]
得到从摄像机1到摄像机2的基矩阵的变化
# compute change of basis matrix from camera 1 to camera 2
Ec = (E2 @ np.linalg.inv(E1))[:-1, :]
# extract rotation and translation matrix from the change of basis matrix
Rc = Ec[:, :-1]
Tc = Ec[:, -1]
# validate the rotation and transalation change of basis matrices
is_vectors_close(point_c2.reshape(-1), Rc @ point_c1.reshape(-1) + Tc)
# compute essential matrix
Tm = get_cross_product_matrix(Tc)
essential_matrix = Tm @ Rc
# validating the essential matrix equation
np.round(point_c2.T @ essential_matrix @ point_c1)[0][0]
-0.0
# check p2.T @ E @ p1 = 0
is_vectors_close(point_c2.T @ essential_matrix @ point_c1, np.array([[0]]))
# convert the intersection points' coordinates from world system to camera system
# convert the intersection points to homogeneous coordinates
c1_intn_world_hg = to_hg_coords(np.expand_dims(c1_intn_world, axis=1))
c2_intn_world_hg = to_hg_coords(np.expand_dims(c2_intn_world, axis=1))
# compute the coordinates of the intersection points wrt the camera
c1_intn_hg = E1 @ c1_intn_world_hg
c2_intn_hg = E2 @ c2_intn_world_hg
# convert back to euclidean coordinates
c1_intn = c1_intn_hg[:-1, :]
c2_intn = c2_intn_hg[:-1, :]
# check p2_intn.T @ E @ p1_intn = 0
is_vectors_close(c2_intn.T @ essential_matrix @ c1_intn, np.array([[0]]))
绘制极线
计算并绘制齐次空间中的外极线
nrows = 1
ncols = 2
h, w = img_size
fig, axes = plt.subplots(nrows=nrows, ncols=ncols, figsize=(6, 4))
# Epipolar line in camera 1 given the point wrt camera 2
ax1 = axes[0]
ax1.set_title("camera 1")
ax1.set(xlim = (-(h // 2), w // 2), ylim = (-(h // 2), w // 2))
# compute the epipolar line in camera 1
coeffs = (point_c2.T @ essential_matrix).reshape(-1)
x, y = plot_line(coeffs, (-1, 1))
# convert c2_intn from homogeneous coordinate to pixel coordinate
u, v = to_eucld_coords(c1_intn).reshape(-1)
ax1.plot(x, y, label="epipolar line")
ax1.scatter(u, v, color="orange", label="point")
# Epipolar line in camera 2 given the point wrt camera 1
ax2 = axes[1]
ax2.set_title("camera 2")
ax2.set(xlim = (-(h // 2), w // 2), ylim = (-(h // 2), w // 2))
coeffs = (essential_matrix @ point_c1).reshape(-1)
x, y = plot_line(coeffs, (-1, 1))
u, v = to_eucld_coords(c2_intn).reshape(-1)
ax2.plot(x, y, label="epipolar line")
ax2.scatter(u, v, color="orange", label="point")
plt.tight_layout()
基本矩阵
计算基本矩阵
fundamental_matrix = np.linalg.inv(K).T @ essential_matrix @ np.linalg.inv(K)
nrows = 1
ncols = 2
fig, axes = plt.subplots(nrows=nrows, ncols=ncols, figsize=(6, 4))
# plot projection 1
ax1 = axes[0]
ax1.set(xlim = (-(h // 2), w // 2), ylim = (-(h // 2), w // 2))
ax1.set_title("Camera 1 Image")
proj_point2_hg = to_hg_coords(proj_point2)
coeffs = (proj_point2_hg.T @ fundamental_matrix).reshape(-1)
x, y = plot_line(coeffs, (-2, 2))
ax1.plot(x, y, color="orange")
ax1.scatter(*proj_point1.reshape(-1))
# plot projection 2
ax2 = axes[1]
ax2.set(xlim = (-(h // 2), w // 2), ylim = (-(h // 2), w // 2))
ax2.set_title("Camera 2 Image")
proj_point1_hg = to_hg_coords(proj_point1)
coeffs = (fundamental_matrix @ proj_point1_hg).reshape(-1)
x, y = plot_line(coeffs, (-2, 2))
ax2.plot(x, y, color="orange")
ax2.scatter(*proj_point2.reshape(-1))
plt.tight_layout()
设置环境
首先,我们为两个相机定义外部参数,使它们以一定角度相距一定距离。外部参数包括相机的位置和方向。
接下来,我们定义决定图像形成的内在参数。这里我们已经定义了图像平面的大小和焦距,这基本上是图像平面离相机中心的距离的度量。
我们还定义了世界上的一个点,以便它被两个相机捕获。在本例中,我们将找到该点投影的外极线。
两个视图中捕获的图像如下所示:
计算基本矩阵
为了找到基本矩阵,我们需要找到两个相机之间的相对几何结构,以便给定点在相机1的坐标,我们应该能够找到它在相机2的坐标。
现在,我们知道相机外参矩阵是基矩阵从世界坐标系到相机坐标系的变化。使用该信息,可以如下计算从相机1到相机2的基矩阵的变化:
从相机1到相机2的基矩阵变化=(从世界到相机2基矩阵的变化)×
⟹ 从相机1到相机2的基矩阵变化=(从世界到相机2基矩阵的变化)×
⟹ 从相机1到相机2的基矩阵的变化=(相机2的外参矩阵)×(相机1的外参矩阵的逆)
一旦我们获得了基矩阵的这种变化,我们就可以提取相机的相对方向和偏移。这个3×4矩阵的前3列将给出方向Rc,最后一列将给出偏移量Tc。
然后我们计算Tc的叉积矩阵,并将其与Rc相乘,以获得基本矩阵。
作为健全性检查,我们可以验证基本矩阵方程。
绘制极线
要找到外极线,我们首先需要找到相机图像平面上该点的投影。在这个例子中,我已经通过试错手动绘制了这些点,但我们将在后面的章节中看到更好的方法来找到它们。
一旦我们找到这些点,我们将它们与它们各自的外参矩阵相乘,得到相机帧的坐标。然后我们将它们插入基本矩阵方程,并找到相应的外极线。
例如,如果我们知道相机1中的投影点,我们将其与基本矩阵相乘,得到相机2中的外极线。然后我们在2D图像空间中绘制这条线。
为了验证相机2中的投影点位于这条线上,我们将其转换为欧几里得坐标,并将其绘制在同一空间中。我们对另一台相机重复同样的过程。
图像空间中的投影点及其对应的外极线如下所示:
平行
如果图像平面彼此平行会发生什么?在上述系统中,我们有两个相机,它们的光轴沿着Y轴彼此平行。让我们假设相机中心在X轴上,相距b。
由于相机是平行的,因此它们之间没有相对旋转。因此,Rc将是一个单位矩阵,Tc将等于[-b,0,0]。因此,我们可以将基本矩阵计算为:
让x𝑐 xc′是X点在摄像机Oc和O𝑐′的像平面上的投影. 如果我们假设两个相机的焦距都是f,我们可以写xc=[x,y,f]和xc′=[x′,y′,f]。我们可以将它们插入基本矩阵方程中,如下所示:
我们可以看到外极线具有相同的Y坐标,这意味着它们沿着X轴平行。因此,我们可以说,如果两个相机彼此平行,那么它们的外极线也将在图像空间中平行。
如下所示:
此部分的代码可在此处找到:
https://github.com/wingedrasengan927/Stereo-Geometry/blob/master/Parallel%20Image%20Planes.ipynb
如果相机沿着Y轴平行,图像中的外极线将是水平的;如果它们沿着X轴平行,则外极线将是垂直的。
基本矩阵
在现实生活中,我们很少有关于世界上点的位置的信息。然而,我们可以在图像中找到它们的位置。因此,我们需要修改基本矩阵方程,以考虑点的图像位置。
现在,给定世界上与相机帧相关的一个点,内参矩阵负责将其投影到相机的图像平面上。
我们可以将κ发送到另一侧,并将等式改写为:
因此,给定图像中的一个点,我们将其表示为齐次形式,并与内参矩阵的逆相乘,得到该点在世界上的齐次表示。
现在,我们无法从图像中确定点在世界上的准确位置,因为齐次坐标是比例不变的,并且点可以位于射线上的任何位置。
考虑两个相机帧l和r,以及在两个图像平面上投影的点Pc。这里的基本矩阵方程为:
在这个等式中,我们可以这样替换齐次图像坐标:
这被称为基本矩阵方程。
基本矩阵将同一点的坐标关联在两个不同的视图中,而基本矩阵将它们关联在两张不同的图像中。
在前面的例子中,我们可以计算基本矩阵并绘制外极线,如下所示:
如果你观察到,外极线看起来和以前完全一样,但是,这一次它们是使用基本矩阵方程直接从图像坐标中计算出来的。
从点对应关系计算基本矩阵
在现实世界中,我们很少使用校准过的系统。然而,由于基本矩阵直接关联图像点,我们仍然可以在不了解世界和相机的情况下找到它。下面是方法。
设(u,v)和(u′,v′)表示两个不同图像中的同一点。我们可以用齐次形式表示它们,并插入基本矩阵方程:
这里f1、f2、…表示基本矩阵的未知参数。上面的等式表示一个同质系统,我在这里的另一篇文章中已经深入讨论了它们。然而,我将在这里再次讨论直觉。
第一步是将等式改写为:
改写这种方法的一个原因是,我们可以在同一个等式中堆叠许多点对应关系,如下所示:
该方程可用矩阵表示法表示为
Af
⃗
= 0
,其中A是点对应矩阵和向量
f
⃗ 是平坦的基本矩阵。现在,
f
⃗ 可以通过将方程的两边除以其大小*|f* ⃗ *|*而得到单位向量.
可以通过计算*|Ax* ⃗
|
的最小值找到受约束
|x
⃗ |= 1 时
Ax
⃗
= 0
的方程的解。.
八点算法
其思想是找到图像之间的点对应关系并计算矩阵𝐴. 然后,可以通过计算𝐴⊺𝐴 并将其重塑为3 x 3矩阵。
计算f至少需要多少点?现在,f有9个未知数,所以你会说我们需要9个方程或9个点来求解它。但如果你观察到,f是尺度不变的,这意味着我们可以将f乘以任何常数,并且方程
Af
⃗
= 0
仍然满足。因此,我们可以将 f 与其值之一相除,如下所示:
现在看到有8个未知数需要解决,我们至少只需要8个点。
还有一件事我们需要解释。你看,3×3基本矩阵F的秩=2。我们这里不讨论证明,但这与叉积矩阵的秩2有关。
为此,我们对矩阵F进行奇异值分解,使其最后一个奇异值为零,然后重新组合。
八点算法的代码如下所示:
def compute_fundamental_matrix(points1, points2):
'''
Compute the fundamental matrix given the point correspondences
Parameters
------------
points1, points2 - array with shape [n, 3]
corresponding points in images represented as
homogeneous coordinates
'''
# validate points
assert points1.shape[0] == points2.shape[0], "no. of points don't match"
u1 = points1[:, 0]
v1 = points1[:, 1]
u2 = points2[:, 0]
v2 = points2[:, 1]
one = np.ones_like(u1)
# construct the matrix
# A = [u2.u1, u2.v1, u2, v2.u1, v2.v1, v2, u1, v1, 1] for all the points
# stack columns
A = np.c_[u1 * u2, v1 * u2, u2, u1 * v2, v1 * v2, v2, u1, v1, one]
# peform svd on A and find the minimum value of |Af|
U, S, V = np.linalg.svd(A, full_matrices=True)
f = V[-1, :]
F = f.reshape(3, 3) # reshape f as a matrix
# constrain F
# make rank 2 by zeroing out last singular value
U, S, V = np.linalg.svd(F, full_matrices=True)
S[-1] = 0 # zero out the last singular value
F = U @ np.diag(S) @ V
return F
标准化八点算法
现代图像具有约4000-6000像素的高分辨率。这会导致点对应关系中的大量差异,这可能会破坏算法。因此,为了解释这一点,我们在将这些点插入八点算法之前对它们进行了归一化。
这个想法是,对于每个图像,我们计算点对应的质心(平均值),并从每个图像中减去。接下来,我们对它们进行缩放,使其与质心的距离(方差)为√2,如下所示:
接下来,我们创建一个执行上述转换的矩阵,并使用它来转换点,如下面的代码所示:
def compute_fundamental_matrix_normalized(points1, points2):
'''
Normalize points by calculating the centroid, subtracting
it from the points and scaling the points such that the distance
from the origin is sqrt(2)
Parameters
------------
points1, points2 - array with shape [n, 3]
corresponding points in images represented as
homogeneous coordinates
'''
# validate points
assert points1.shape[0] == points2.shape[0], "no. of points don't match"
# compute centroid of points
c1 = np.mean(points1, axis=0)
c2 = np.mean(points2, axis=0)
# compute the scaling factor
s1 = np.sqrt(2 / np.mean(np.sum((points1 - c1) ** 2, axis=1)))
s2 = np.sqrt(2 / np.mean(np.sum((points2 - c2) ** 2, axis=1)))
# compute the normalization matrix for both the points
T1 = np.array([
[s1, 0, -s1 * c1[0]],
[0, s1, -s1 * c1[1]],
[0, 0 ,1]
])
T2 = np.array([
[s2, 0, -s2 * c2[0]],
[0, s2, -s2 * c2[1]],
[0, 0, 1]
])
# normalize the points
points1_n = T1 @ points1.T
points2_n = T2 @ points2.T
# compute the normalized fundamental matrix
F_n = compute_fundamental_matrix(points1_n.T, points2_n.T)
# de-normalize the fundamental
return T2.T @ F_n @ T1
寻找极点
我们知道极点是图像中所有外极线相交的点。数学上可以表示为:
其中l1,l2,…是外极线,e是极点。这可以用矩阵形式表示为:
现在这看起来像一个齐次方程组。因此,要找到极点,我们可以使用上一节中讨论的线性最小二乘估计找到
Le
⃗ = 0的解。
但是等等,可以从基本矩阵计算外极线。因此,我们可以将等式改写为:
现在,为了找到这个方程的解,我们简单地计算F的线性最小二乘估计,这是它的最后一个奇异值。
计算极点的代码如下所示:
def compute_epipole(F):
'''
Compute epipole using the fundamental matrix.
pass F.T as argument to compute the other epipole
'''
U, S, V = np.linalg.svd(F)
e = V[-1, :]
e = e / e[2]
return e
为了找到另一幅图像的极点,我们找到了F转置的线性最小二乘估计。
Python示例
好吧,让我们看看基本矩阵的作用。
%matplotlib widget
import matplotlib.pyplot as plt
from skimage import io
from skimage.transform import resize
from skimage.transform import warp, ProjectiveTransform
from stereo_utils import *
from skimage.color import rgb2gray, rgba2rgb
打印图像和匹配点
# load images
im1 = io.imread("data/bench/right.png")
im1 = rgb2gray(rgba2rgb(im1))
im2 = io.imread("data/bench/left.png")
im2 = rgb2gray(rgba2rgb(im2))
# load matching points
points1 = np.load("data/bench/right_points.npy")
points2 = np.load("data/bench/left_points.npy")
assert (points1.shape == points2.shape)
绘制匹配点
show_matching_result(im1, im2, points1, points2)
基本矩阵
# compute the normalized fundamental matrix
F = compute_fundamental_matrix_normalized(points1, points2)
# validate the fundamental matrix equation
p1 = points1.T[:, 0]
p2 = points2.T[:, 0]
np.round(p2.T @ F @ p1)
0.0
绘制极线
plot_epipolar_lines(im1, im2, points1, points2, show_epipole=False)
绘制极点
e1 = compute_epipole(F)
e2 = compute_epipole(F.T)
# validate epioles
np.round(e2.T @ F @ e1)
0.0
plot_epipolar_lines(im1, im2, points1, points2, show_epipole=True)
立体校正
H1, H2 = compute_matching_homographies(e2, F, im2, points1, points2)
# Transform points based on the homography matrix
new_points1 = H1 @ points1.T
new_points2 = H2 @ points2.T
new_points1 /= new_points1[2,:]
new_points2 /= new_points2[2,:]
new_points1 = new_points1.T
new_points2 = new_points2.T
# warp images based on the homography matrix
im1_warped = warp(im1, ProjectiveTransform(matrix=np.linalg.inv(H1)))
im2_warped = warp(im2, ProjectiveTransform(matrix=np.linalg.inv(H2)))
绘制新的外极线和匹配点
h, w = im1.shape
nrows = 2
ncols = 1
fig, axes = plt.subplots(nrows=nrows, ncols=ncols, figsize=(6, 8))
# plot image 1
ax1 = axes[0]
ax1.set_title("Image 1 warped")
ax1.imshow(im1_warped, cmap="gray")
# plot image 2
ax2 = axes[1]
ax2.set_title("Image 2 warped")
ax2.imshow(im2_warped, cmap="gray")
# plot the epipolar lines and points
n = new_points1.shape[0]
for i in range(n):
p1 = new_points1[i]
p2 = new_points2[i]
ax1.hlines(p2[1], 0, w, color="orange")
ax1.scatter(*p1[:2], color="blue")
ax2.hlines(p1[1], 0, w, color="orange")
ax2.scatter(*p2[:2], color="blue")
经验观察
plot_epipolar_lines(im1_warped, im2_warped, new_points1, new_points2, show_epipole=False)
我们可以看到外极线不准确
笔记本可以在这里找到:
https://github.com/wingedrasengan927/Stereo-Geometry/blob/master/Fundamental%20Matrix%20and%20Stereo%20Rectification.ipynb
首先,我们需要同一物体的两张图像。于是我打开了我的编辑器,放置了两个相机,看着同一个物体,并截屏。
接下来,我们需要两个图像之间的点对应或匹配。在这里,我手动标记了它们,但在现实世界中,我们可以使用SIFT等算法自动计算它们。
好了,现在我们可以使用归一化八点算法从点计算基本矩阵。
我们还可以使用计算的矩阵来验证基本矩阵方程。
让我们绘制两幅图像的外极线。
我们可以看到对应于一幅图像的外极线穿过另一幅图像中的点。
接下来,让我们找到并绘制外极线。
基本矩阵方程也适用于极点,因为它们也是图像平面中的点。
我们可以在这里看到,极点位于图像之外,所有外极线都在它们处相交。
立体校正
由于外极线是平行的,所以使用具有平行图像平面的图像很容易。然而,通过战略性地扭曲它们,可以使它们平行。这个过程被称为立体校正。让我们看看怎么做。
对于平行图像,外极沿水平轴位于无穷远处。所以第一步是创建变换矩阵,将点移动到无穷远。
我们需要三个变换矩阵:一个将一个点旋转到水平轴,一个将点移动到无穷远,另一个将原点平移到中心。让我们看看每一个。
将点旋转到水平轴
给定一个与X轴成角度θ的点,我们创建一个旋转矩阵,将其旋转-θ,并将其带回X轴,如下所示:
注意:如果点位于X轴的另一侧,符号将反转。
由于我们处理的是齐次坐标,我们需要考虑额外的维度。
齐次坐标的一般变换矩阵,也称为单应矩阵,如下所示:
因此,将齐次坐标旋转回X轴所需的矩阵为:
将点移到无穷远
接下来我们需要将点移到无穷远。无穷远处的点表示为(∞, 0)或(-∞, 0). 这可以用齐次坐标表示为(x,0,0),如下所示:
因此,给定一个位于X轴上的点,以其齐次形式表示为(X,0,1),我们需要一个将其转换为(X、0,0)的矩阵。
以下矩阵可完成此工作:
将原点移动到中心
默认情况下,Python假定图像的原点位于左上角,因此我们需要创建一个平移矩阵来将原点移动到中心。
应用变换后,我们可以将其移回原来的位置。
扭曲图像
因此,组合上述矩阵的总变换矩阵由下式给出:
想法是我们用上面的矩阵变换点,然后为了“撤消”效果,我们用矩阵的逆矩阵扭曲整个图像。
使用这种技术,我们可以扭曲一幅图像。现在,如何扭曲另一个图像?理查德·哈特利(Richard Hartley)在他的论文中认为,为了获得最佳结果,两个立体图像都需要对齐,这意味着变换后的点对应之间的距离应该最小。
因此,可以通过最小化变换后的点对应关系之间的平方距离之和来找到匹配单应矩阵H1:
这里我们不讨论计算H1的证明,但我已经在最后的参考资料部分将其链接起来,以供感兴趣的读者参考。
计算H1和H2的代码片段如下所示:
def compute_matching_homographies(e2, F, im2, points1, points2):
'''
Compute the matching homography matrices
'''
h, w = im2.shape
# create the homography matrix H2 that moves the epipole to infinity
# create the translation matrix to shift to the image center
T = np.array([[1, 0, -w/2], [0, 1, -h/2], [0, 0, 1]])
e2_p = T @ e2
e2_p = e2_p / e2_p[2]
e2x = e2_p[0]
e2y = e2_p[1]
# create the rotation matrix to rotate the epipole back to X axis
if e2x >= 0:
a = 1
else:
a = -1
R1 = a * e2x / np.sqrt(e2x ** 2 + e2y ** 2)
R2 = a * e2y / np.sqrt(e2x ** 2 + e2y ** 2)
R = np.array([[R1, R2, 0], [-R2, R1, 0], [0, 0, 1]])
e2_p = R @ e2_p
x = e2_p[0]
# create matrix to move the epipole to infinity
G = np.array([[1, 0, 0], [0, 1, 0], [-1/x, 0, 1]])
# create the overall transformation matrix
H2 = np.linalg.inv(T) @ G @ R @ T
# create the corresponding homography matrix for the other image
e_x = np.array([[0, -e2[2], e2[1]], [e2[2], 0, -e2[0]], [-e2[1], e2[0], 0]])
M = e_x @ F + e2.reshape(3,1) @ np.array([[1, 1, 1]])
points1_t = H2 @ M @ points1.T
points2_t = H2 @ points2.T
points1_t /= points1_t[2, :]
points2_t /= points2_t[2, :]
b = points2_t[0, :]
a = np.linalg.lstsq(points1_t.T, b, rcond=None)[0]
H_A = np.array([a, [0, 1, 0], [0, 0, 1]])
H1 = H_A @ H2 @ M
return H1, H2
好吧,让我们纠正我们示例中的图像。
一旦我们计算了单应矩阵,就可以使用它们的逆来扭曲图像,如下所示:
我们现在已经用平行的图像平面校正了图像,所以外极线只是通过点的水平线。
实证观察
如果我使用归一化八点算法计算扭曲图像的基本矩阵和外极线,结果不准确,如下所示:
我们不知道确切的原因,但我们认为算法正在崩溃。
校正后的图像现在可以用于各种下游任务,如视差估计、模板匹配等。
结论
好了,我们已经到了终点。在本文中,我们研究了处理从两个视图捕获的图像的技术。我们还看到了如何使用立体校正从两张图像中估计某些模糊度,如深度。如果我们有多个视图,我们还可以使用称为“运动结构”的技术来估计场景的结构。
参考引用
-
cs231a Notes(https://web.stanford.edu/class/cs231a/course_notes/03-epipolar-geometry_2022.pdf)
-
https://github.com/chizhang529/cs231a
-
Theory and Practice of Stereo Rectification by Richard Hartley(https://users.cecs.anu.edu.au/~hartley/Papers/joint-epipolar/journal/joint3.pdf)
☆ END ☆
如果看到这里,说明你喜欢这篇文章,请转发、点赞。微信搜索「uncle_pn」,欢迎添加小编微信「 woshicver」,每日朋友圈更新一篇高质量博文。
↓
扫描二维码添加小编↓