畸变校正
上篇文章说了双目视觉的数学原理,但整篇都是基于针孔相机模型来讲解的,但实际上为了更好的成像效果我们使用的都是透镜相机,二者之间的原理类似,但透镜相机会产生透视失真,也叫透视畸变。
上图为双目相机的拍摄结果,拍摄目标为相机标定使用的标定板,可以明显的观察到标定板的边缘在透视畸变的作用下是弯曲的。
极线校正
除了畸变的问题外其实还有点问题,我们通过画一些平行线可以看的更清楚,你会发现同名点(被摄点在左右图像的点对)在各自图像(或投影)坐标系中 轴方向上的坐标是不同的。
也就是说,对于左图中的一个点,想去右图中寻找同名点,需要遍历整个图像,但如果我们能够让同名点的 轴方向坐标相同,我们就只需要在右图中遍历一条线就行了,不仅时间复杂度更低,而且需要匹配的位置更少因此结果也更精准。
所谓遍历匹配同名点,就是用如 或其他尺寸的矩阵,以每一个像素为该矩阵的中心去遍历,通过某种公式计算两个 矩阵的相关性,来判断它们是不是同名点。
之所以同名点 轴方向坐标不同,是因为我们无法保证左右相机坐标系的 轴平行。
这里明确下相机坐标系,以我们的双眼类比,视觉右方为 轴正向,视觉下方为 轴正向,视觉前方为 轴正向。
如上图所示,极线为被摄点、左相机坐标系原点、右相机坐标系原点组成的极平面与成像平面的相交直线,校正方式就是为分别为左右相机虚拟出 轴平行的相机坐标系,成像平面上的点通过某种数学公式映射到虚拟像平面上。
代码实现
畸变校正和极线校正都是用 stereoRectify
这一个函数来实现,先来看一下校正后的效果。不难发现,标定板的边缘已经不再是曲线了,并且同名点 轴坐标也保持一致。
在校正前,我们需要对双目相机进行标定来获取相机的参数。这里使用 OpenCV Python 来演示,如果你习惯 OpenCV C++ 也是一样的。
标定
先来看双目标定的函数签名。
def stereoCalibrate(
objectPoints,
imagePoints1,
imagePoints2,
cameraMatrix1,
distCoeffs1,
cameraMatrix2,
distCoeffs2,
imageSize
[, R[, T[, E[, F[, flags[, criteria]]]]]]
) -> (
retval,
cameraMatrix1,
distCoeffs1,
cameraMatrix2,
distCoeffs2,
R, T, E, F
)
接下来我们一个一个的说明这些参数和返回值的意义:
参数
- objectPoints:棋盘格角点的世界坐标,这里规定每个格子边长为 1 个单位。
w, h = 9, 6 # 棋盘格两个方向的角点数量
def obj_points():
points = []
for x in range(w):
for y in range(h):
points.append([x, y, 0])
return [points]*len(imgs)
# 还有一种简洁的写法,与上面等价
def obj_ponits():
points = np.zeros((np.prod((w, h)), 3), np.float32)
points[:, :2] = np.indices((w, h)).T.reshape(-1, 2)
return [points]*len(imgs)
- imagePoints:棋盘格角点的图像坐标,需要通过角点检测来得到。
def detect_corners(img):
ok, corners = cv2.findChessboardCorners(img, self.board_size)
assert ok
cv2.cornerSubPix(img, corners, (11, 11), (-1, -1), (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_COUNT, 30, 0.01))
return corners
img_points = [detect_corners(img).reshape(-1, 2) for img in imgs]
- cameraMatrix:相机内参矩阵,也就是上篇文章提到的 矩阵,需要通过 objectPoints 和 imagePoints 来初始化,这里得到的 只是一个初始值,是不精确的还需要多次迭代计算。
K = cv2.initCameraMatrix2D(obj_ponits, img_points, img_size, 0)
-
distCoeffs:畸变参数,需要传入初始值用于后续迭代计算,也可以直接传空。
-
imageSize:图像的大小 ,例如 。
-
R、T、E、F:传出参数,不需要传。
-
flags:
- CALIB_FIX_INTRINSIC:固定传入的 cameraMatrix 和 distCoeffs,只计算 R、T、E、F。
- CALIB_USE_INTRINSIC_GUESS:以传入的 cameraMatrix 和 distCoeffs 为初始值开始迭代。
- CALIB_FIX_PRINCIPAL_POINT:迭代时固定主点。
- CALIB_FIX_FOCAL_LENGTH:迭代时固定焦距。
- CALIB_FIX_ASPECT_RATIO:固定 和 比值。
- CALIB_SAME_FOCAL_LENGTH:强制保持两个相机焦距相同。
- CALIB_ZERO_TANGENT_DIST:切向畸变保持为零。
- CALIB_RATIONAL_MODEL:使用更精确的畸变模型。
- CALIB_FIX_K*:迭代时不改变相应的畸变参数。
-
criteria:迭代的终止条件。
criteria = (
cv2.TERM_CRITERIA_EPS | # 达到精度
cv2.TERM_CRITERIA_COUNT, # 达到迭代次数
100, # 迭代次数
1e-5 # 精度
)
返回值
- retval:误差(root mean square 本文用不到)。
- cameraMatrix:计算后的 矩阵。
- distCoeffs:计算后的畸变参数。
- R:相机间的旋转矩阵。
- T:相机间的平移矩阵。
- E:本质矩阵(essential matrix 本文用不到)。
- F:基础矩阵(fundamental matrix 本文用不到)。
这里明确一下相机间的旋转和平移矩阵,这里假设 cameraMatrix1 描述左相机,cameraMatrix2 描述右相机,则函数返回的 R、T 是左相机坐标系到右相机坐标系的变换,也就是说左相机坐标进行R、T变换后得到的结果,为该点在右相机坐标系中的坐标。
校正
先来看立体校正的函数签名。
def stereoRectify(
cameraMatrix1,
distCoeffs1,
cameraMatrix2,
distCoeffs2,
imageSize,
R,
T
[, R1[, R2[, P1[, P2[, Q[, flags[, alpha[, newImageSize]]]]]]]]
) -> (
R1,
R2,
P1,
P2,
Q,
validPixROI1,
validPixROI2
)
还是一个一个的说明这些参数和返回值,上面说过的就不再重复了。
参数
- flags:
- CALIB_ZERO_DISPARITY:让两个像主点在校正后有相同的图像坐标。
- alpha:
- 0:校正后的图像被改变,没有黑色区域。
- 1:保留拍摄的所有像素,会有黑边。
返回值
- R:旋转矩阵。
- P:投影矩阵。
- Q:视差深度映射矩阵(disparity-to-depth mapping matrix 本文用不到)。
- validPixROI:有效区域,如果alpha = 0,有效区域即全部像素。
这里明确一下 矩阵的涵义。
旋转矩阵 不是世界坐标系到相机坐标系的旋转矩阵,而是真实相机坐标系到虚拟相机坐标系的旋转矩阵。
投影矩阵 则是虚拟相机坐标系到虚拟投影坐标系的投影矩阵。
End
上面的两个函数,计算出了 这四个矩阵,其中 用于畸变校正, 用于极线校正。
本文只是说了说函数的用法(具体算法是怎么运行的我其实一概不知),关于函数的参数的说明其实官方文档都有,如果本文有不专业或不准确的地方万望指出。
本文代码地址:github.com/suqinglee/C…