双目相机标定
数字图像处理分享双目相机标定双目相机标定张凯博2024-11-242025-02-07前言
相机标定是计算机视觉的基础。由于物理原因,相机采集到的图像存在一定的畸变需要进行标定,从而矫正图像。为后期计算视差图、识别等做准备。
生成标定板
在标定前需要标定板帮助我们进行标定。对于一般标定,无需昂贵的高精度标定板,下面推荐一个生成标定板的网站。
引用站外地址
标定板生成网站
https://github.com/KB-talk/picx-images-hosting/raw/master/双目相机标定/image.3yehbset9j.webp
相机校正
相机校正包括单目校正和双目校正两个步骤,其中单目校正主要计算出相机的内参,来对镜头进行去畸变以及深度的推算。双目校正则是计算出左右相机的外参,知道外参后可以将左右相机分别旋转一定角度,以至左右相机的同名点在同一平面且同一水平线上。
实验
本实验完整代码已上传到Github中,下面内容为实验过程及代码说明。
实验器材
本次使用双目相机进行试验。此双目相机由一颗可见光相机和一颗红外相机组成。
对于三维重建,使用双彩色相机进行图像采集更为合适。此款相机更适合对于复杂环境或特殊用途,如活体人脸识别等。但对于此双目相机标定试验仍可以进行试验。
环境搭建
本实验使用OpenCV进行,需要进行相应的环境搭建。
使用Anaconda创建解释器环境。并使用pip进行安装。
1pip install opencv-python
拍摄左右相机图片
使用双目相机同时拍摄标定板,并将图像存放到对应的文件夹中,并进行编号。
新建名为左右相机拍摄.py的文件,在文件中添加如下代码。
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859import cv2import osdef create_directory_if_not_exists(directory): if not os.path.exists(directory): os.makedirs(directory)def open_and_display_cameras(): # 创建存储照片的文件夹 left_folder = 'left_photos' right_folder = 'right_photos' create_directory_if_not_exists(left_folder) create_directory_if_not_exists(right_folder) # 打开两个摄像头 left_camera = cv2.VideoCapture(0) # 左摄像头 right_camera = cv2.VideoCapture(1) # 右摄像头 if not left_camera.isOpened() or not right_camera.isOpened(): print("无法打开摄像头") return photo_count = 0 while True: # 读取帧 ret_left, frame_left = left_camera.read() ret_right, frame_right = right_camera.read() if not ret_left or not ret_right: print("无法获取帧") break # 显示帧 cv2.imshow('left_camera', frame_left) cv2.imshow('right_camera', frame_right) # 按下空格键拍照 key = cv2.waitKey(1) & 0xFF if key == ord(' '): left_photo_path = os.path.join(left_folder, f'l{photo_count}.jpg') right_photo_path = os.path.join(right_folder, f'r{photo_count}.jpg') cv2.imwrite(left_photo_path, frame_left) cv2.imwrite(right_photo_path, frame_right) print(f"照片已保存: {left_photo_path}, {right_photo_path}") photo_count += 1 # 按下 'q' 键退出循环 if key == ord('q'): break # 释放摄像头资源 left_camera.release() right_camera.release() cv2.destroyAllWindows()if __name__ == "__main__": open_and_display_cameras()
双目摄像头矫正
获取角点
从拍摄的棋盘图像中提取角点。
在获取角点时,我们使用cv2.findChessboardCorners方法来检测角点,并使用cv2.cornerSubPix方法提高角点的精度。
chessboard_size = (9, 14)为棋盘内角点的数量,请根据实际棋盘大小进行修改。
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849import cv2import numpy as npimport glob# 棋盘格的尺寸 (宽度, 高度)chessboard_size = (9, 14)# 棋盘格方格的实际大小 (单位: 米)square_size = 0.010# 对象点,例如 (0,0,0), (1,0,0), (2,0,0) ....,(8,5,0)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# 存储对象点和图像点的列表objpoints = [] # 3d point in real world spaceimgpoints_l = [] # 2d points in image plane for left cameraimgpoints_r = [] # 2d points in image plane for right camera# 图像路径left_images = glob.glob('left_photos/*.jpg')right_images = glob.glob('right_photos/*.jpg')if len(left_images) != len(right_images): raise ValueError("左右图像数量不匹配")for left_img, right_img in zip(sorted(left_images), sorted(right_images)): img_l = cv2.imread(left_img) img_r = cv2.imread(right_img) gray_l = cv2.cvtColor(img_l, cv2.COLOR_BGR2GRAY) gray_r = cv2.cvtColor(img_r, cv2.COLOR_BGR2GRAY) # 查找棋盘格角点 ret_l, corners_l = cv2.findChessboardCorners(gray_l, chessboard_size, None) ret_r, corners_r = cv2.findChessboardCorners(gray_r, chessboard_size, None) if ret_l and ret_r: objpoints.append(objp) imgpoints_l.append(corners_l) imgpoints_r.append(corners_r) # 绘制并显示角点 cv2.drawChessboardCorners(img_l, chessboard_size, corners_l, ret_l) cv2.drawChessboardCorners(img_r, chessboard_size, corners_r, ret_r) cv2.imshow('Left Image', img_l) cv2.imshow('Right Image', img_r) cv2.waitKey(500)cv2.destroyAllWindows()
进行单目标定
通过提取到的角点,使用cv2.calibrateCamera对左右相机分别进行标定,得到每个相机的内参(相机矩阵),畸变系数,旋转向量和平移向量。
内参(Camera Matrix):每个相机都有一个 3x3 的内参矩阵(mtx_l 和 mtx_r)。
畸变系数(Distortion Coefficients):每个相机的畸变参数(dist_l 和 dist_r),通常包含 5 个参数。
旋转向量和平移向量:描述相机坐标系与世界坐标系之间的关系。
123456# 校准左相机ret_l, mtx_l, dist_l, rvecs_l, tvecs_l = cv2.calibrateCamera(objpoints, imgpoints_l, gray_l.shape[::-1], None, None)# 校准右相机ret_r, mtx_r, dist_r, rvecs_r, tvecs_r = cv2.calibrateCamera(objpoints, imgpoints_r, gray_r.shape[::-1], None, None)
进行双目标定
使用左右相机拍摄的棋盘图像,计算出两相机之间的旋转和平移关系。
旋转矩阵 (R):描述两个相机之间的旋转关系。
平移向量 (T):描述两个相机之间的平移关系。
本质矩阵 (E):两个相机的内参矩阵已知时,给出两相机坐标系之间的关系。
基础矩阵 (F):计算两个相机图像平面之间的关系。
123456789101112# 立体校正flags = 0flags |= cv2.CALIB_FIX_INTRINSICcriteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)ret, mtx_l, dist_l, mtx_r, dist_r, R, T, E, F = cv2.stereoCalibrate( objpoints, imgpoints_l, imgpoints_r, mtx_l, dist_l, mtx_r, dist_r, gray_l.shape[::-1], criteria=criteria, flags=flags)# 计算校正映射R1, R2, P1, P2, Q, roi1, roi2 = cv2.stereoRectify(mtx_l, dist_l, mtx_r, dist_r, gray_l.shape[::-1], R, T)
立体校正
在双目标定后,我们可以进行立体校正(Stereo Rectification),将左右相机的图像映射到相同的平面,方便后续的视差计算。
123456789101112131415161718# 计算重映射矩阵map1_l, map2_l = cv2.initUndistortRectifyMap(mtx_l, dist_l, R1, P1, gray_l.shape[::-1], cv2.CV_32FC1)map1_r, map2_r = cv2.initUndistortRectifyMap(mtx_r, dist_r, R2, P2, gray_r.shape[::-1], cv2.CV_32FC1)# 应用校正img_l = cv2.imread('left_photos/l0.jpg')img_r = cv2.imread('right_photos/r0.jpg')dst_l = cv2.remap(img_l, map1_l, map2_l, cv2.INTER_LINEAR)dst_r = cv2.remap(img_r, map1_r, map2_r, cv2.INTER_LINEAR)# 显示校正后的图像cv2.imshow('Left Image', img_l)cv2.imshow('Right Image', img_r)cv2.imshow('Corrected Left Image', dst_l)cv2.imshow('Corrected Right Image', dst_r)cv2.waitKey(0)cv2.destroyAllWindows()
矫正图像
使用等线来观察矫正效果。
12345678910111213141516171819202122def stack_images(img1, img2): """水平拼接图像,方便对比""" height = max(img1.shape[0], img2.shape[0]) width = img1.shape[1] + img2.shape[1] stacked_image = np.zeros((height, width, 3), dtype=np.uint8) # 将左图和右图分别放置 stacked_image[:img1.shape[0], :img1.shape[1], :] = img1 stacked_image[:img2.shape[0], img1.shape[1]:, :] = img2 return stacked_image# 拼接校正后的图像comparison_image = stack_images(dst_l, dst_r)# 绘制水平线,便于对比for i in range(0, comparison_image.shape[0], 50): # 每隔50像素画一条线 cv2.line(comparison_image, (0, i), (comparison_image.shape[1], i), (0, 255, 0), 1)# 显示拼接图像cv2.imshow('Stereo Rectification Comparison', comparison_image)cv2.waitKey(0)cv2.destroyAllWindows()
完整代码
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127import cv2import numpy as npimport glob# 棋盘格的尺寸 (宽度, 高度)chessboard_size = (9, 14)# 棋盘格方格的实际大小 (单位: 米)square_size = 0.010# 对象点,例如 (0,0,0), (1,0,0), (2,0,0) ....,(8,5,0)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# 存储对象点和图像点的列表objpoints = [] # 3d point in real world spaceimgpoints_l = [] # 2d points in image plane for left cameraimgpoints_r = [] # 2d points in image plane for right camera# 图像路径left_images = glob.glob('left_photos/*.jpg')right_images = glob.glob('right_photos/*.jpg')if len(left_images) != len(right_images): raise ValueError("左右图像数量不匹配")for left_img, right_img in zip(sorted(left_images), sorted(right_images)): img_l = cv2.imread(left_img) img_r = cv2.imread(right_img) gray_l = cv2.cvtColor(img_l, cv2.COLOR_BGR2GRAY) gray_r = cv2.cvtColor(img_r, cv2.COLOR_BGR2GRAY) # 查找棋盘格角点 ret_l, corners_l = cv2.findChessboardCorners(gray_l, chessboard_size, None) ret_r, corners_r = cv2.findChessboardCorners(gray_r, chessboard_size, None) if ret_l and ret_r: objpoints.append(objp) imgpoints_l.append(corners_l) imgpoints_r.append(corners_r) # 绘制并显示角点 cv2.drawChessboardCorners(img_l, chessboard_size, corners_l, ret_l) cv2.drawChessboardCorners(img_r, chessboard_size, corners_r, ret_r) cv2.imshow('Left Image', img_l) cv2.imshow('Right Image', img_r) cv2.waitKey(500)cv2.destroyAllWindows()# 校准左相机ret_l, mtx_l, dist_l, rvecs_l, tvecs_l = cv2.calibrateCamera(objpoints, imgpoints_l, gray_l.shape[::-1], None, None)# 校准右相机ret_r, mtx_r, dist_r, rvecs_r, tvecs_r = cv2.calibrateCamera(objpoints, imgpoints_r, gray_r.shape[::-1], None, None)# 立体校正flags = 0flags |= cv2.CALIB_FIX_INTRINSICcriteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)ret, mtx_l, dist_l, mtx_r, dist_r, R, T, E, F = cv2.stereoCalibrate( objpoints, imgpoints_l, imgpoints_r, mtx_l, dist_l, mtx_r, dist_r, gray_l.shape[::-1], criteria=criteria, flags=flags)# 计算校正映射R1, R2, P1, P2, Q, roi1, roi2 = cv2.stereoRectify(mtx_l, dist_l, mtx_r, dist_r, gray_l.shape[::-1], R, T)# 输出校正参数print('输出校正参数')print("R1:\n", R1)print("R2:\n", R2)print("P1:\n", P1)print("P2:\n", P2)print("Q:\n", Q)print("roi1:", roi1)print("roi2:", roi2)# 计算重映射矩阵map1_l, map2_l = cv2.initUndistortRectifyMap(mtx_l, dist_l, R1, P1, gray_l.shape[::-1], cv2.CV_32FC1)map1_r, map2_r = cv2.initUndistortRectifyMap(mtx_r, dist_r, R2, P2, gray_r.shape[::-1], cv2.CV_32FC1)print('输出重映射矩阵')print("map1_l:\n", map1_l)print("map2_l:\n", map2_l)print("map1_r:\n", map1_r)print("map2_r:\n", map2_r)# 应用校正img_l = cv2.imread('left_photos/l0.jpg')img_r = cv2.imread('right_photos/r0.jpg')dst_l = cv2.remap(img_l, map1_l, map2_l, cv2.INTER_LINEAR)dst_r = cv2.remap(img_r, map1_r, map2_r, cv2.INTER_LINEAR)# 显示校正后的图像cv2.imshow('Left Image', img_l)cv2.imshow('Right Image', img_r)cv2.imshow('Corrected Left Image', dst_l)cv2.imshow('Corrected Right Image', dst_r)cv2.waitKey(0)cv2.destroyAllWindows()# 拼接校正后的图像def stack_images(img1, img2): """水平拼接图像,方便对比""" height = max(img1.shape[0], img2.shape[0]) width = img1.shape[1] + img2.shape[1] stacked_image = np.zeros((height, width, 3), dtype=np.uint8) # 将左图和右图分别放置 stacked_image[:img1.shape[0], :img1.shape[1], :] = img1 stacked_image[:img2.shape[0], img1.shape[1]:, :] = img2 return stacked_image# 拼接校正后的图像comparison_image = stack_images(dst_l, dst_r)# 绘制水平线,便于对比for i in range(0, comparison_image.shape[0], 50): # 每隔50像素画一条线 cv2.line(comparison_image, (0, i), (comparison_image.shape[1], i), (0, 255, 0), 1)# 显示拼接图像cv2.imshow('Stereo Rectification Comparison', comparison_image)cv2.waitKey(0)cv2.destroyAllWindows()
实验结果分析
左右摄像头画面的区别
可以从图中看出,由于两摄像头的物理位置原因,同时拍摄的物体在不同摄像头的画面的左右摄像头画面存在差异,左摄像头可以拍摄完整的图像,而右侧摄像头拍摄的棋盘不是完整的。
左右相机棋盘图像数据
使用相机拍摄程序,同时使用左右相机对棋盘进行不同角度的拍摄,在对应的文件夹中图片的数量是一致的,且同一时刻的图像使用相同的编号。
角点检测
分别对左右相机拍摄的棋盘图像进行角点检测,利用检测到的角点,进行校正参数的计算。
计算校正映射
校正参数的计算结果如下
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475输出校正参数R1: [[ 9.99968652e-01 7.11958972e-03 3.46507597e-03] [-7.11718582e-03 9.99974424e-01 -7.05587627e-04] [-3.47001084e-03 6.80903918e-04 9.99993748e-01]]R2: [[ 9.99941085e-01 6.91406038e-03 8.36797777e-03] [-6.91985992e-03 9.99975837e-01 6.64307947e-04] [-8.36318251e-03 -7.22174043e-04 9.99964767e-01]]P1: [[509.11951885 0. 296.01989746 0. ] [ 0. 509.11951885 250.99332809 0. ] [ 0. 0. 1. 0. ]]P2: [[509.11951885 0. 296.01989746 -12.52910219] [ 0. 509.11951885 250.99332809 0. ] [ 0. 0. 1. 0. ]]Q: [[ 1. 0. 0. -296.01989746] [ 0. 1. 0. -250.99332809] [ 0. 0. 0. 509.11951885] [ 0. 0. 40.63495621 -0. ]]roi1: (0, 4, 621, 474)roi2: (4, 14, 636, 466)输出重映射矩阵map1_l: [[ 26.016935 26.809679 27.606382 ... 632.6917 633.23663 633.7744 ] [ 25.849155 26.644186 27.443148 ... 632.9653 633.5138 634.0552 ] [ 25.683455 26.480747 27.281948 ... 633.2358 633.7878 634.33276 ] ... [ 18.498121 19.340826 20.18694 ... 636.2985 636.9254 637.5461 ] [ 18.607605 19.44846 20.292744 ... 636.0809 636.7049 637.3227 ] [ 18.718735 19.55772 20.40016 ... 635.8608 636.4819 637.0967 ]]map2_l: [[ 4.2664475 4.1126733 3.9612215 ... 19.76085 20.045544 20.333712 ] [ 5.0976753 4.9460087 4.7966433 ... 20.485853 20.76748 21.052553 ] [ 5.9317703 5.782188 5.634885 ... 21.214697 21.49329 21.775297 ] ... [470.41818 470.5368 470.65363 ... 466.05548 465.8481 465.63785 ] [471.31512 471.4353 471.5537 ... 466.86148 466.65167 466.4389 ] [472.21002 472.33182 472.45178 ... 467.6646 467.4523 467.237 ]]map1_r: [[ 4.5652542e-01 1.4337360e+00 2.4116211e+00 ... 6.3474969e+02 6.3567242e+02 6.3659406e+02] [ 4.2274535e-01 1.4003611e+00 2.3786469e+00 ... 6.3478564e+02 6.3570892e+02 6.3663110e+02] [ 3.8933307e-01 1.3673503e+00 2.3460336e+00 ... 6.3482111e+02 6.3574493e+02 6.3666766e+02] ... [-3.0759840e+00 -2.0907555e+00 -1.1049554e+00 ... 6.3203741e+02 6.3297253e+02 6.3390662e+02] [-3.0623775e+00 -2.0774810e+00 -1.0920093e+00 ... 6.3199725e+02 6.3293188e+02 6.3386554e+02] [-3.0484853e+00 -2.0639238e+00 -1.0787835e+00 ... 6.3195673e+02 6.3289093e+02 6.3382410e+02]]map2_r: [[-13.135011 -13.151552 -13.16768 ... -4.6508904 -4.597651 -4.543879 ] [-12.159264 -12.1754465 -12.191218 ... -3.699166 -3.6464214 -3.593148 ] [-11.183023 -11.198851 -11.214271 ... -2.7468433 -2.6945884 -2.6418087] ... [463.33337 463.3562 463.37875 ... 464.14984 464.12 464.08972 ] [464.31665 464.33975 464.36255 ... 465.11093 465.0807 465.05002 ] [465.29956 465.32294 465.34598 ... 466.07156 466.04092 466.00986 ]]
矫正前后对比
使用计算得到的矫正参数,将左右相机拍摄的棋盘图像进行校正,并显示对比。
矫正前后的对比图如下
分析对比图我们可以很明显的观察到,矫正前后的图像是不同的。不同的位置如图所示。
左相机经过校正后,在图像的右侧出现的空白区域,证明了整体画面是经过调整过的。
右相机经过校正后,在图像的上侧出现的空白区域,且左下角区域的位置出现了调整,说明对画面的位置进行了有效的矫正。
等线分析
绘制等线。观察左右相机拍摄同一物体,对应的像素是否在同一水平线上。
在左右相机合成图像上,观察等线与像素点,可以很明显的观察到同一物体对应的像素点都在同一水平线上。
总结
由于本实验使用的是双目相机,在物理上已经尽可能的保持了两个相机的间距是固定且是水平的,但由于安装等原因,避免不了的存在一定的误差。经过标定和矫正进一步的使得图像的误差变得非常小。
但本次使用的双目相机是红外与彩色相机,彩色相机与红外相机的图像可能在纹理、亮度、对比度等方面差异较大,对于特征点的提取存在一定的困难。若后期进行测距、视差、三维重建等工作,双彩色相机是更为合适的。
本实验,是基于张氏标定法进行的标定,此方法存在一定的弊端。
此方法严重依赖于标定板,标定板若出现损坏、不完整、变形等情况,会造成标定参数的误差。
需要多角度拍摄标定板,若对于某些设备无法完成多角度的拍摄,则会大大降低矫正效果。
对于需要高精度的场景,则需要高精度的标定板,高精度标定的生产难度较大导致价格较昂贵。
在拍摄标定板时,不能出现移动的情况,否则可能会造成标定参数的误差。
在相机和镜头的选择上,尽可能的使用固定间距的双目相机,且使用无畸变镜头,虽然矫正算法可能对画面进行一定程度的矫正,但是对于畸变严重的情况,会出现大面积裁切的情况,无法尽可能的保留有效图像信息。