《相机标定及python实现》

文章目录

  • 相机标定
    • 标定方法
    • 标定板
    • 开源标定
  • OpenCV标定
    • 单目标定
    • 立体标定
    • 畸变校正
    • 手眼标定
    • 已知内参标定外参
  • QA
  • 参考文献

相机标定方法及实现


相机标定

标定方法

  • 传统标定方法
  • 自标定
  • 基于主动视觉标定

《相机标定及python实现》_第1张图片

标定板

  • 棋盘格
rowNum = 9
colNum = 9
DPI = 96  # dot per inch
inch2cm = 2.54
K= int(blockSize / inch2cm * DPI)
img = np.zeros((rowNum *K, colNum *K, 3), "uint8")
for i in range(rowNum):
	for j in range(colNum):
		if (i+j) % 2 != 0:
			img[i*K:i*K+K, j*K:j*K+K] = 255
  • 圆点阵列

开源标定

  • OpenCV标定
  • Halcon
  • Matlab Calibration Toolbox标定工具箱

OpenCV标定

  • 主要以张正友标定法来实现
  • 获取相机内参 f x , f y , u 0 , v 0 , k 1 , k 2 , k 3 , p 1 , p 2 f_x, f_y, u_0, v_0, k_1, k_2, k_3, p_1, p_2 fx,fy,u0,v0,k1,k2,k3,p1,p2
  • 得到相机外参,每张标定图片对应一组外参

单目标定

  • 准备标定板(平整,尺寸已知)

  • 不同角度拍摄一组照片(>=4张)

  • 根据标定板,生成一组对应世界坐标

    • 实际标定板行数和列数,设置行-1 列-1,识别内部的点,所以拍摄棋盘格时,最外的行列遮挡也可;
    • 实际尺寸影响对相机的像素焦距、像主点、畸变系数、外参中的旋转矩阵没有影响,dx\dy改变,外参中的平移矩阵、双目标定中的基线有影响
  • (亚像素)角点提取

    • findChessboardCorners 角点检测
    • cornerSubPix 亚像素角点检测
  • 标定

    • calibrateCamera

      calibrateCamera(objectPoints, imagePoints, imageSize, cameraMatrix, distCoeffs, rvecs=None, tvecs=None, flags=None, criteria=None)
      CV_CALIB_USE_INTRINSIC_GUESS:使用该参数时,在cameraMatrix矩阵中应该有fx,fy,u0,v0的估计值。否则的话,将初始化(u0,v0)图像的中心点,使用最小二乘估算出fx,fy。
      CV_CALIB_FIX_PRINCIPAL_POINT:在进行优化时会固定光轴点。当CV_CALIB_USE_INTRINSIC_GUESS参数被设置,光轴点将保持在中心或者某个输入的值。
      CV_CALIB_FIX_ASPECT_RATIO:固定fx/fy的比值,只将fy作为可变量,进行优化计算。当CV_CALIB_USE_INTRINSIC_GUESS没有被设置,fx和fy将会被忽略。只有fx/fy的比值在计算中会被用到。
      CV_CALIB_ZERO_TANGENT_DIST:设定切向畸变参数(p1,p2)为零。
      CV_CALIB_FIX_K1,…,CV_CALIB_FIX_K6:对应的径向畸变在优化中保持不变。
      CV_CALIB_RATIONAL_MODEL:计算k4,k5,k6三个畸变参数。如果没有设置,则只计算其它5个畸变参数。
      
  • 评价,重投影误差

    • projectPoints
  • 内参优化

    • getOptimalNewCameraMatrix

      alpha=1,视场不变,所有像素都保留,有黑色像素混入
      alpha=0,尽可能裁剪不想要的像素,都是有效,这是个scale
      建议采用alpha=1,保留黑边和ROI
      
class ZZY_Calib:
    def __init__(self, rows, cols, length_chess=1):
        self.rows = rows
        self.cols = cols
        self.length_chess = length_chess  # 实际标定板棋盘格尺寸,影响到dx\dy,外参中的平移矩阵、双目标定中的基线,其余没有影响
        self.criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) ### find corner iter stop
        self.obj_pts = self.gen_world_coor(self.rows, self.cols)
        
    def gen_world_coor(self, rows, cols):
        obj_pts =  np.zeros((rows * cols,  3), np.float32)
        obj_pts[:, :2] = np.mgrid[0:rows, 0:cols].T.reshape(-1, 2)
        return obj_pts
        
    def show_chessboard_corner(self, img, corners, ret=1):
        # 在棋盘上绘制角点,可视化工具
        img = cv2.drawChessboardCorners(img,(self.rows, self.cols), corners, ret)
        cv2.namedWindow('img', 0)
        cv2.resizeWindow('img', 500, 500)
        cv2.imshow('img',img)
        cv2.waitKey(0)
        cv2.destroyAllWindows()
        
   def calib(self, path_img, path_campara, show_corner=False):
        self.l_obj_pts=[]
        self.l_img_pts=[]
        obj_pts = self.gen_world_coor(self.rows, self.cols)
        l_file = glob.glob(os.path.join(path_img, "*.jpg"))
        print("Loading [%d] calib Images" %  len(l_file))
        for ind, file in enumerate(l_file):
            print("calib [%d]:%s ..." % (ind, file))
            img = cv2.imdecode(np.fromfile(file, dtype='uint8'), -1)
            if len(img.shape) == 3:
                img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
            else:
                img_gray = img
            ret, corners = cv2.findChessboardCorners(img_gray, (self.rows, self.cols), None)
            if ret:
                if show_corner:
                    self.show_chessboard_corner(img, corners, ret)
                self.l_obj_pts.append(obj_pts)
                corners2 = cv2.cornerSubPix(img_gray, corners, (11,11), (-1,-1), self.criteria)                  # 执行亚像素级角点检测
                self.l_img_pts.append(corners2)
        '''
        传入所有图片各自角点的三维、二维坐标,相机标定。
        每张图片都有自己的旋转和平移矩阵,但是相机内参和畸变系数只有一组
        mtx,相机内参;dist,畸变系数;revcs,旋转矩阵;tvecs,平移矩阵。
        '''
        ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(self.l_obj_pts, self.l_img_pts, img_gray.shape[::-1], None, None)
        # Calibration Error
        tot_error = 0
        for i in range(len(self.l_obj_pts)):
            imgpoints2, _ = cv2.projectPoints(self.l_obj_pts[i], rvecs[i], tvecs[i], mtx, dist)
            error = cv2.norm(self.l_img_pts[i],imgpoints2, cv2.NORM_L2)/len(imgpoints2)
            tot_error += error
        print ("total error: ", tot_error/len(self.l_obj_pts))
        # np.savez(self.path_campara, mtx=mtx, dist=dist)
        print('ret', ret)
        print('内参矩阵:', mtx)
        print('畸变系数:', dist)
        print('旋转矩阵:', rvecs)
        print('平移矩阵:', tvecs)
        '''
        优化相机内参(camera matrix),可选,提高精度。
        alpha= 1, 所有像素都保留,有黑色像素混入 
        alpha=0, 尽可能裁剪不想要的像素,都是有效,这是个scale
        '''
        alpha=1
        newcameramtx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist, (img_gray.shape[1], img_gray.shape[0]), alpha, (img_gray.shape[1], img_gray.shape[0]))
        print('优化后的相机内参:', newcameramtx)
        print("ROI:", roi)
        np.savez(path_campara, mtx=mtx, dist=dist, new_mtx=newcameramtx, roi=roi)
        
     def read_campara(self, path):
        # 读取相机内参数
        with np.load(path) as X:
            mtx, dist = [X[i] for i in ('mtx', 'dist')]
        return mtx, dist

立体标定

  • 获取两个相机的相对位姿关系:包括旋转和平移矩阵

  • 分别标定左右相机

  • 立体标定

  • stereoCalibrate

    stereoCalibrate(objectPoints, imagePoints1, imagePoints2, cameraMatrix1, distCoeffs1, cameraMatrix2, distCoeffs2, imageSize, R=None, T=None, E=None, F=None, flags=None, criteria=None)
    objectPoints    标定角点在世界坐标系中的位置;
    imagePoints1    标定角点在第一个摄像机下的投影后的亚像素坐标;
    imagePoints2    标定角点在第二个摄像机下的投影后的亚像素坐标;
    cameraMatrix1   第一个摄像机的相机矩阵;
    distCoeffs1     第一个摄像机的输入/输出型畸变向量。根据矫正模型的不同,输出向量长度由标志决定;
    cameraMatrix2	第二个摄像机的相机矩阵。参数意义同第一个相机矩阵相似;
    distCoeffs2	第一个摄像机的输入/输出型畸变向量。根据矫正模型的不同,输出向量长度由标志决定;
    imageSize	图像的大小;
    R			第一和第二个摄像机之间的旋转矩阵;
    T			第一和第二个摄像机之间的平移矩阵;
    E			基本矩阵;
    F  			基础矩阵;
    term_crit 	迭代优化的终止条件
    
    • flags

       CV_CALIB_FIX_INTRINSIC 如果该标志被设置,那么就会固定输入的cameraMatrix和distCoeffs不变,只求解
      $R,T,E,F$.
       CV_CALIB_USE_INTRINSIC_GUESS 根据用户提供的cameraMatrix和distCoeffs为初始值开始迭代
       CV_CALIB_FIX_PRINCIPAL_POINT 迭代过程中不会改变主点的位置
       CV_CALIB_FIX_FOCAL_LENGTH 迭代过程中不会改变焦距
       CV_CALIB_SAME_FOCAL_LENGTH 强制保持两个摄像机的焦距相同
       CV_CALIB_ZERO_TANGENT_DIST 切向畸变保持为零
       CV_CALIB_FIX_K1,...,CV_CALIB_FIX_K6 迭代过程中不改变相应的值。如果设置了 CV_CALIB_USE_INTRINSIC_GUESS 将会使用用户提供的初始值,否则设置为零
       CV_CALIB_RATIONAL_MODEL 畸变模型的选择,如果设置了该参数,将会使用更精确的畸变模型,distCoeffs的长度就会变成8
      
  • 立体校正

    • stereoRectify
    • initUndistortRectifyMap
    • remap
  • 获取视差图像

    • StereoSGBM_create
    • stereo.compute
    • reprojectImageTo3D
    def calib2(self, path, path_l, path_r, obj_pts, img_pts_l, img_pts_r):
        print("[Calib 2Cam]")
        with np.load(path_l) as X:
            Otmx_l, dist_l, size = [X[i] for i in ('Otmx', 'dist', 'size')]
        with np.load(path_r) as X:
            Otmx_r, dist_r = [X[i] for i in ('Otmx', 'dist')]
        flags = 0
        flags |= cv2.CALIB_FIX_INTRINSIC    # 固定cam 和 dist不变,只求解R,T,E,F
        # 立体标定
        retS, MLS, dLS, MRS, dRS, R, T, E, F = cv2.stereoCalibrate(obj_pts, img_pts_l, img_pts_r, Otmx_l, dist_l, Otmx_r, dist_r,
                                                                   (size[0],size[1]), criteria=self.criteria_stero, flags=flags)
  • 立体校正
    def stereoRectify(self, path, imgL, imgR):
        with np.load(path) as X:
            steroMap_l0, steroMap_l1, steroMap_r0, steroMap_r1 = [X[i] for i in ("steroMapL0", "steroMapL1", "steroMapR0", "steroMapR1")]
        imgL_rect = cv2.remap(imgL, steroMap_l0, steroMap_l1, cv2.INTER_LANCZOS4, cv2.BORDER_CONSTANT, 0)
        imgR_rect = cv2.remap(imgR, steroMap_r0, steroMap_r1, cv2.INTER_LANCZOS4, cv2.BORDER_CONSTANT, 0)
        imgHsatck = np.hstack([imgL_rect, imgR_rect])

        imgHstack0 = np.hstack([imgL, imgR])
        imgVstack = np.vstack([imgHstack0, imgHsatck])
        cv2.imencode(".jpg", imgHsatck)[1].tofile(r'D:\Programs\StereoVision\SteroVision\Data\stereoRectify.jpg')

        cv2.namedWindow("hstack", cv2.WINDOW_NORMAL)
        cv2.imshow("hstack", imgVstack)
        cv2.waitKey(0)

畸变校正

  • (1) 径向畸变

沿着透镜半径方向分布的畸变,靠近透镜中心畸变较明显,表现在短焦镜头,主要包括桶形畸变和枕形畸变。
x ′ = x ( 1 + k 1 r 2 + k 2 r 4 + k 3 r 6 ) y ′ = y ( 1 + k 1 r 2 + k 2 r 4 + k 3 r 6 ) \begin{aligned} x' &= x(1+{k_1}r^2+{k_2}r^4+{k_3}r^6) \\ y' &=y(1+{k_1}r^2+{k_2}r^4+{k_3}r^6) \end{aligned} xy=x(1+k1r2+k2r4+k3r6)=y(1+k1r2+k2r4+k3r6)

  • (2) 切向畸变

由于透镜本身与相机传感器平面(成像平面)不平行产生,由于透镜粘贴到镜头模组偏差导致。
x ′ = x + [ 2 p 1 y + p 2 ( r 2 + 2 x 2 ) ] y ′ = y + [ 2 p 2 x + p 1 ( r 2 + 2 y 2 ) ] \begin{aligned} x' = x + [2p_1y + p_2(r^2 + 2x^2)] \\ y' = y + [2p_2x + p_1(r^2 + 2y^2)] \end{aligned} x=x+[2p1y+p2(r2+2x2)]y=y+[2p2x+p1(r2+2y2)]

  • (3) 薄棱镜畸变

一般由镜头设计加工安装误差导致,一般情况可忽略
x ′ = x + s 1 ( x 2 + y 2 ) y ′ = y + s 2 ( x 2 + y 2 ) \begin{aligned} x' = x + s_1(x^2 + y^2) \\ y' = y + s_2(x^2 + y^2) \end{aligned} x=x+s1(x2+y2)y=y+s2(x2+y2)

  • 畸变参数(一般考虑OpenCV前五个参数, k1, k2, p1, p2, k3)

    • 径向畸变 k1 k2 k3
    • 切向畸变 p1 p2
    • 薄棱镜畸变 s1 s2
  • 畸变校正有两种方法

    • UndistortImage

    • initUndistortRectifyMap() + remap()

    • 单独使用几次时,差别不大,当多次图片畸变校正时,建议使用一次initUndistortRectifyMap,获取映射矩阵mapxmapy后,作为remap输入,再使用多次的remap校正

    def undistort_img(self, img, mtx, dist, newcameramtx, roi):
        img_dst = cv2.undistort(img, mtx, dist, None, newcameramtx)
        # 这步只是输出纠正畸变以后的图片
        x, y, w, h = roi
        dst = img_dst[y:y + h, x:x + w]
        cv2.imwrite('calibresult.png', dst)

手眼标定

  • 交互保存标定图片
def grab_one_cam(path_img):
    VideoCapture = cv2.VideoCapture(0) # USB摄像头
	# VideoCapture = cv2.VideoCapture(rtsp)   # RTSP  
    if not VideoCapture.isOpened():
        print("Error open video!")
        exit()
    frame_no = 0
    while VideoCapture.isOpened():
        ret, frame = VideoCapture.read()
        if not ret:
            break
        k = show_image("frame", frame, 1)
        if k == ord("Q"):
            break
        elif k == ord("S"):
            cv2.imencode(".jpg", frame)[1].tofile(os.path.join(path_img, "img_%04d.jpg" % frame_no))
            frame_no += 1
    VideoCapture.release()

已知内参标定外参

  • 标定已知相机内参
  • 通过对应点计算投影矩阵
  • 根据内参和对应点计算外参矩阵
    • cv2.solvePnPRansac
    • cv2.Rodrigues, 旋转向量转化为旋转矩阵
    def calib_image(self, img, path_cam):
        # 已知相机内参和标定图片,输出外参数
        mtx, dist = self.read_campara(path_cam)
        if len(img.shape) == 3:
            img_gray =  cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
        else:
            img_gray = img.copy()
        ret, corners = cv2.findChessboardCorners(img_gray, (self.rows, self.cols), None)
        if ret:
            exact_corners = cv2.cornerSubPix(img_gray, corners, (11, 11), (-1, -1), self.criteria)
            _, rvec, tvec, inliers = cv2.solvePnPRansac(self.obj_pts, exact_corners, mtx, dist)
            rotation_m, _ = cv2.Rodrigues(rvec)  # 罗德里格斯变换,从旋转向量到旋转矩阵
            rotation_t = np.hstack([rotation_m, tvec])
            rotation_t_Homogeneous_matrix = np.vstack([rotation_t, np.array([[0, 0, 0, 1]])])

QA

  • Q:像素焦距与毫米焦距(标定出来像素焦距)
fu = fx * dx
fv = fy * dy
fx、fy内参矩阵中的像素焦距
fu、fv为毫米焦距
dx、dy为像素到实际尺寸的转换关系,像素<->毫米
  • Q:实际棋盘格尺寸设置对标定结果的影响
**实际尺寸**影响对相机的像素焦距、像主点、畸变系数、外参中的旋转矩阵没有影响,dx\dy改变,外参中的平移矩阵、双目标定中的基线有影响
  • Q:标定棋盘格的行数和列数设置
实际标定板行数和列数,设置行-1 列-1,识别内部的点,所以拍摄棋盘格时,最外的行列遮挡也可;

参考文献

  1. Camera Calibration and 3D Reconstruction

你可能感兴趣的:(Camera,python,自动驾驶,opencv)