OpenCV下的单目标定,双目标定与立体校正(calibrateCamera, stereoCalibrate and stereoRectify)
文章目录
- 1. 杂话
- 2. 单目标定
- 2.1 先看代码
- 2.2 一点解释
- 2.3 calibrateCamera参数
- 3. 双目标定
- 3.1 先看代码
- 3.2 stereoCalibrate参数
- 4. 立体校正
- 4.1 先看代码
- 4.2 一点解释
- 4.3 stereoRectify参数
- 4.4 initUndistortRectifyMap参数
- 4.5 remap参数
- 5. 绘制极线
- 5.1 先看代码
- 5.2 一点解释
- 5.3 校正结果
1. 杂话
大伙儿应该都用过OpenCV和相机吧,所以今天咱们就来说说怎么使用两个相机拍摄的照片和OpenCV来进行标定和立体校正。相机标定的理论解释其实有很多啦,我就随便找两个写得不错的帖子给大家参考一下哈:
fengye2two的帖子-标定
卍卐没想到的帖子-标定
瞻邈-立体校正
总而言之,我就不那个班门弄斧关公面前耍大刀了,我就简单说说代码层面的实现。
其中,标定部分的代码部分参考了:Temuge Batpurev’s Blog
绘制极线部分的代码参考了:逆光525的帖子-绘制极线
对了,我使用的数据和完整的代码在这里:
Repo : Calibrate-and-Rectify
2. 单目标定
2.1 先看代码
chessboard_size = (9, 6)
frame_size = (640, 480)
# 设置棋盘格点的世界坐标
objp = np.zeros((chessboard_size[0]*chessboard_size[1], 3), np.float32)
objp[:,:2] = np.mgrid[0:chessboard_size[0], 0:chessboard_size[1]].T.reshape(-1, 2)
square_size = 1
objp *= square_size
# 用于存储世界坐标和图像坐标
objpoints = [] # 3d points in real world space
imgpoints_main = [] # 2d points in image plane
images = glob.glob('demo/left*.jpg')
images = sorted(images)
print(f"Found {len(images)} images for calibration")
for idx, image_file in enumerate(images):
img = cv2.imread(image_file)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 寻找棋盘格角点
ret, corners = cv2.findChessboardCorners(gray, chessboard_size, None)
if ret == True:
objpoints.append(objp)
# 亚像素级角点精确化
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
corners2 = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria)
imgpoints_main.append(corners2)
else:
print(f"Chessboard corners not found in image: {image_file}")
# 相机标定
ret_main, mtx_main, dist_main, rvecs_main, tvecs_main = cv2.calibrateCamera(objpoints, imgpoints_main, frame_size, None, None)
mtx_main, roiL = cv2.getOptimalNewCameraMatrix(mtx_main, dist_main,frame_size, 0)
print(ret_main)
imgpoints_side = [] # 2d points in image plane.
images = glob.glob('demo/right*.jpg')
images = sorted(images)
print(f"Found {len(images)} images for calibration")
for idx, image_file in enumerate(images):
img = cv2.imread(image_file)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 寻找棋盘格角点
ret, corners = cv2.findChessboardCorners(gray, chessboard_size, None)
if ret == True:
# 亚像素级角点精确化
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
corners2 = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria)
imgpoints_side.append(corners2)
else:
print(f"Chessboard corners not found in image: {image_file}")
# 相机标定
ret_side, mtx_side, dist_side, rvecs_side, tvecs_side = cv2.calibrateCamera(objpoints, imgpoints_side, frame_size, None, None)
mtx_side, roiL = cv2.getOptimalNewCameraMatrix(mtx_side, dist_side,frame_size, 0)
print(ret_side)
2.2 一点解释
首先是这个objp,这玩意说白了就是世界坐标系的坐标,但是这个世界坐标系不是咱们可以自己定义嘛对吧。方便起见,我就直接假设标定板的平面就是x-y平面,所以上面所有的焦点的z坐标不久都是0了嘛对吧哈哈哈。
然后就是calibrateCamera这个函数,具体的输入参数是都写在上面了,需要注意的是,一般来说返回的误差也就是rmse在0.5以下会比较好,如果很大的话,那么你的标定图像就需要好好调整一下了,比如光照好一点,加入更多的角度之类的。
2.3 calibrateCamera参数
# 单目标定 calibrateCamera
# ret, mtx, dist, rvecs, tvecs =
# cv2.calibrateCamera(objpoints, imgpoints, imageSize, cameraMatrix, distCoeffs)
# 输入参数:
# objpoints: 物体点坐标的列表。这些是 3D 世界坐标,通常是棋盘格的角点坐标。
# imgpoints: 图像点坐标的列表。这些是 2D 图像坐标,通常是从图像中检测到的棋盘格角点的坐标。
# imageSize: 图像的大小,格式为 (width, height)。
# cameraMatrix (可选): 初始的相机内参矩阵。如果传入 None,则函数会计算一个初始值。
# distCoeffs (可选): 初始的畸变系数。如果传入 None,则函数会计算一个初始值。
# 输出参数:
# ret: 平均重投影误差(root mean square error, RMSE),表示校准结果的精度。
# mtx: 相机内参矩阵(camera matrix)。
# dist: 畸变系数(distortion coefficients)。
# rvecs: 旋转向量列表,表示每个视角的旋转。
# tvecs: 平移向量列表,表示每个视角的平移。
3. 双目标定
3.1 先看代码
flags = cv2.CALIB_FIX_INTRINSIC
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_COUNT, 100, 1e-6)
ret, mtx_main, dist_main, mtx_side, dist_side, R, T, E, F = cv2.stereoCalibrate(
objpoints, imgpoints_main, imgpoints_side,
mtx_main, dist_main, mtx_side, dist_side,
frame_size, criteria=criteria, flags=flags)
3.2 stereoCalibrate参数
# 双目标定函数
# ret, cameraMatrix1, distCoeffs1, cameraMatrix2, distCoeffs2, R, T, E, F =
# cv2.stereoCalibrate(objectPoints, imagePoints1, imagePoints2,
# cameraMatrix1, distCoeffs1, cameraMatrix2, distCoeffs2, imageSize,
# criteria=criteria, flags=flags)
# 输入参数:
# objectPoints: 物体点坐标的列表,类似 calibrateCamera 函数中的 objpoints。
# imagePoints1: 左相机的图像点坐标列表。
# imagePoints2: 右相机的图像点坐标列表。
# cameraMatrix1: 左相机的初始内参矩阵。
# distCoeffs1: 左相机的初始畸变系数。
# cameraMatrix2: 右相机的初始内参矩阵。
# distCoeffs2: 右相机的初始畸变系数。
# imageSize: 图像的大小,格式为 (width, height)。
# criteria (可选): 终止条件,用于优化算法的迭代过程。
# cv2.TERM_CRITERIA_MAX_ITER:当达到最大迭代次数时停止。
# cv2.TERM_CRITERIA_EPS:当参数变化小于设定的精度时停止。
# 可以组合使用,例如cv2.TERM_CRITERIA_MAX_ITER + cv2.TERM_CRITERIA_EPS,表示当满足任意一个条件时停止。
# 最大迭代次数:一个整数,指定最大迭代次数。例如,100表示最多迭代100次。
# 精度阈值:一个浮点数,指定参数变化小于该值时停止迭代。例如,1e-5表示当参数变化小于0.00001时停止迭代。
# flags (可选): 标志位,用于指定某些参数的固定或自由度。
# cv2.CALIB_FIX_INTRINSIC:在标定过程中保持两个相机的内参数矩阵不变。这意味着在双目标定过程中不会重新估计每个相机的内参数矩阵(包括焦距、光轴中心等),而是使用单个相机标定结果中得到的内参数。
# cv2.CALIB_USE_INTRINSIC_GUESS:使用传入的内参数作为初始猜测值,并在标定过程中对其进行优化。这对提高标定精度很有帮助,特别是在内参数已经较准确的情况下。
# cv2.CALIB_FIX_PRINCIPAL_POINT:保持主点(光轴中心)固定不变。
# cv2.CALIB_FIX_FOCAL_LENGTH:保持焦距不变。
# cv2.CALIB_FIX_ASPECT_RATIO:保持焦距的长宽比不变。
# cv2.CALIB_ZERO_TANGENT_DIST:假设切向畸变参数为零并保持不变。
# cv2.CALIB_RATIONAL_MODEL:使标定函数使用一个带有6个畸变系数的合理模型。
# cv2.CALIB_SAME_FOCAL_LENGTH:假设两个摄像头具有相同的焦距。
# 输出参数:
# ret: 平均重投影误差。
# cameraMatrix1: 校准后的左相机内参矩阵。
# distCoeffs1: 校准后的左相机畸变系数。
# cameraMatrix2: 校准后的右相机内参矩阵。
# distCoeffs2: 校准后的右相机畸变系数。
# R: 旋转矩阵,将右相机坐标系转换到左相机坐标系。
# T: 平移向量,将右相机坐标系转换到左相机坐标系。
# E: 基础矩阵。
# F: 本质矩阵。
4. 立体校正
4.1 先看代码
# 立体校正
# 立体校正
R1, R2, P1, P2, Q, roi1, roi2 = cv2.stereoRectify(mtx_main, dist_main, mtx_side, dist_side, frame_size, R, T)
# 对测试图像进行校正
def rectify_image(img, mtx, dist, R, P):
h, w = img.shape[:2]
mapx, mapy = cv2.initUndistortRectifyMap(mtx, dist, R, P, (w, h), cv2.CV_32FC1)
return cv2.remap(img, mapx, mapy, cv2.INTER_LINEAR)
# 读取测试图片
img_main = cv2.imread('demo/left03.jpg')
img_side = cv2.imread('demo/right03.jpg')
# 校正图像
rectified_main = rectify_image(img_main, mtx_main, dist_main, R1, P1)
rectified_side = rectify_image(img_side, mtx_side, dist_side, R2, P2)
# 保存校正后的图像
cv2.imwrite('rectified_main.png', rectified_main)
cv2.imwrite('rectified_side.png', rectified_side)
4.2 一点解释
其实也没啥好解释的吧,可能就是需要说一下这个initUndistortRectifyMap和这个remap函数是啥子。简单来说哈,第一个函数就是根据相机自己的内参数和两个相机之间的外参数,生成一个映射来消除图片的畸变(为啥要消除可以看上面的理论贴)。第二个函数就是一个映射过程,根据上一个函数生成的映射执行这个映射过程,得到矫正之后的图片。具体的参数我在下面也解释一下吧。
4.3 stereoRectify参数
# R1, R2, P1, P2, Q, roi1, roi2 =
# cv2.stereoRectify(cameraMatrix1, distCoeffs1, cameraMatrix2, distCoeffs2, imageSize,
# R, T, flags=cv2.CALIB_ZERO_DISPARITY, alpha=0, newImageSize=(0, 0))
# 输入参数:
# cameraMatrix1: 左相机内参矩阵。
# distCoeffs1: 左相机畸变系数。
# cameraMatrix2: 右相机内参矩阵。
# distCoeffs2: 右相机畸变系数。
# imageSize: 图像的大小,格式为 (width, height)。
# R: 旋转矩阵,将右相机坐标系转换到左相机坐标系。
# T: 平移向量,将右相机坐标系转换到左相机坐标系。
# flags (可选): 校正类型标志。
# alpha (可选): 自由参数,范围为 [0, 1],决定图像边缘区域的裁剪程度。
# newImageSize (可选): 新图像的大小。
# 输出参数:
# R1: 左相机的校正变换(旋转矩阵)。
# R2: 右相机的校正变换(旋转矩阵)。
# P1: 左相机的新投影矩阵。
# P2: 右相机的新投影矩阵。
# Q: 视差-深度映射矩阵。
# roi1: 左相机图像的有效区域。
# roi2: 右相机图像的有效区域。
4.4 initUndistortRectifyMap参数
# 计算畸变和矫正的映射 initUndistortRectifyMap
# map1, map2 =
# cv2.initUndistortRectifyMap(cameraMatrix, distCoeffs, R, newCameraMatrix, size, m1type)
# 输入参数:
# cameraMatrix: 相机内参矩阵。
# distCoeffs: 相机畸变系数。
# R: 校正变换(旋转矩阵)。
# newCameraMatrix: 新的相机矩阵。
# size: 图像的大小,格式为 (width, height)。
# m1type: 输出映射的类型,可以是 cv2.CV_32FC1 或 cv2.CV_16SC2 等。
# 输出参数:
# map1: 第一张映射表,用于 remap 函数。
# map2: 第二张映射表,用于 remap 函数。
4.5 remap参数
# 重映射函数 remap
# dst = cv2.remap(src, map1, map2, interpolation, borderMode, borderValue)
# 输入参数:
# src: 输入图像。
# map1: 第一张映射表。
# map2: 第二张映射表。
# interpolation: 插值方法,如 cv2.INTER_LINEAR 或 cv2.INTER_CUBIC。
# borderMode (可选): 边界模式,定义如何处理图像边界,如 cv2.BORDER_CONSTANT 或 cv2.BORDER_REPLICATE。
# borderValue (可选): 边界值,如果使用 cv2.BORDER_CONSTANT 时使用。
# 输出参数:
# dst: 校正后的图像。
5. 绘制极线
5.1 先看代码
plt.figure(figsize=(20, 20))
for i in range(0,1): # 以第一对图片为例
im_L=Image.fromarray(rectified_main) # numpy 转 image类
im_R=Image.fromarray(rectified_side) # numpy 转 image 类
width = im_L.size[0]*2
height = im_L.size[1]
img_compare = Image.new('RGBA',(width, height))
img_compare.paste(im_L,box=(0,0))
img_compare.paste(im_R,box=(640,0))
#在已经极线对齐的图片上均匀画线
for i in range(1,20):
len=480/20
plt.axhline(y=i*len, color='r', linestyle='-')
plt.imshow(img_compare)
plt.savefig('epipolar_lines.png', bbox_inches='tight', pad_inches=0)
plt.show()
5.2 一点解释
注意哈,因为有点怕麻烦,我这里不是去算了那个极线,然后画的线。我是直接画了水平线作为极线,然后手动去看在同一个水平线上的点是不是对应的。当然了对于标定图片,你也可以直接使用角点作为对应点,画一下极线来看一看。但是如果不是标定图片,可能就需要用SIFT关键点匹配了,这一点OpenCV官方有:官方教程
5.3 校正结果
OK,基本符合预期哦~