相机标定笔记和python实现

摄像机标定

【相机模型】和【相机参数】相关内容看这里:https://blog.csdn.net/qq_33278461/article/details/101026259

1.什么是相机标定:

图像测量和机器视觉里,为了能确定现实世界的任意一个点到图像上对应像素点的投影位置,需要建立一个相机成像的几何模型。
几何模型的参数就是相机参数,包括内参、外参和畸变参数。
得到这个参数的过程就叫相机标定。

2.相机标定的目的

1).近似得到二维到三维的函数映射,可以用在深度估计,三维重建里;
2).去畸变,就是去掉镜头里的扭曲,一条直线投影到图片上不能保持为一条直线了,跟摄像机镜头有关;

3.相机标定的方法

1).传统相机标定法:
利用尺寸已知的标定物,建立标定物上某些点与图像上对应点之间的映射,通过算法近似得到相机模型的内外参数。
平面标定物,如标定版或者打印的纸需要拍摄两张以上图像标定;三维标定物单张图像也可以标定;
主要分为线性标定法、非线性优化标定法、两步标定法(【张正友标定法】、Tsai经典两步法)

2).主动视觉相机标定方法:
已知相机某些运动信息对相机自标定,无需标定物,但需要控制相机做某些特殊运动;
优点是算法简单,往往能够获得线性解,故鲁棒性较高;
缺点是系统的成本高、实验设备昂贵、实验条件要求高,而且不适合于运动参数未知或无法控制的场合。

3).相机自标定法:
利用相机运动约束,相机运动约束一般太强,实际中不准确;
利用场景约束,利用场景中的平行正交信息,其中空间平行线在相机图像平面上的交点被称为消失点,这种方法是基于消失点的自标定方法;
自标定的好处是灵活,也可以在线自标定,由于它是基于绝对二次曲线或曲面的方法,其算法鲁棒性差。

4.相机标定
4.1.像素坐标系-图像坐标系-相机坐标系-世界坐标系转换公式:

Z c [ u v 1 ] = [ 1 d x γ c x 0 1 d y c y 0 0 1 ] [ f 0 0 0 f 0 0 0 1 ] [ R t 0 T 1 ] [ X w Y w Z w 1 ] = [ f x 0 c x 0 f y c y 0 0 1 ] [ R t 0 T 1 ] [ X w Y w Z w 1 ] Z_c\left[\begin{array}{c}{u } \\ {v} \\ {1}\end{array}\right] =\left[\begin{array}{ccc}{\frac{1}{dx}} & {\gamma} & {c_x} \\ {0} & {\frac{1}{dy}} & {c_y} \\ {0} & {0} & {1}\end{array}\right] \left[\begin{array}{ccc}{f} & {0} & {0} \\ {0} & {f} & {0} \\ {0} & {0} & {1}\end{array}\right] \left[\begin{array}{cc}{\boldsymbol{R}} & {\boldsymbol{t}} \\ {\boldsymbol{0}^{T}} & {1}\end{array}\right] \left[\begin{array}{c}{X_w } \\ {Y_w} \\ {Z_w} \\{1}\end{array}\right]=\left[\begin{array}{ccc}{f_x} & {0} & {c_x} \\ {0} & {f_y} & {c_y} \\ {0} & {0} & {1}\end{array}\right] \left[\begin{array}{cc}{\boldsymbol{R}} & {\boldsymbol{t}} \\ {\boldsymbol{0}^{T}} & {1}\end{array}\right] \left[\begin{array}{c}{X_w } \\ {Y_w} \\ {Z_w} \\{1}\end{array}\right] Zcuv1=dx100γdy10cxcy1f000f0001[R0Tt1]XwYwZw1=fx000fy0cxcy1[R0Tt1]XwYwZw1

4.2.标定步骤:

世界坐标系到相机坐标系,得到相机外参(R,T矩阵),确定相机在现实世界的位置和朝向,是三维到三维的转换;
相机坐标系到像素坐标系,得到相机内参K(焦距和主点位置),是三维到二维的转换;
最后得到投影矩阵 P=K [ R | t ] 是一个3×4矩阵,K是内参,[ R | T ]是外参。
双目标定还需要得到双目之间的平移旋转矩阵,RT矩阵。

一般实际中标定是这个流程:
1)分别单目标定左右相机,得到相机内参,外参RT,畸变矩阵;对应cv2.calibrateCamera();
2)用1)里得到的矩阵双目标定得到新的内参、畸变、双目之间RT、基本矩阵E、基础矩阵F;对应cv2.stereoCalibrate();
3)用2)中得到的矩阵进行矫正,每个摄像头计算立体校正的映射矩阵,这里不是矫正图像,而是得出进行立体矫正所需要的映射矩阵;对应cv2.stereoRectify();
4)用3)得到的矩阵计算畸变矫正和立体校正的映射变换,对应cv2.initUndistortRectifyMap() ;【这个矩阵可以用来矫正以后所拍摄的图像】
4.3.code
标定会出现很多问题,一般出现bug的地方有:
1).左右图反了,或者不同相机需要旋转一下,比如有的相机拍出的图保存下来是旋转270度等等,需要保证图是正的;
2).标定板不平,打印的纸贴到箱子或者什么东西上表面不同;
3).拍摄时候板子或相机动了,左右相机延迟导致不匹配;
4).拍摄的方向角度不够多,一般我自己弄就选三个距离(或者选一个稍远的距离)找11-20个角度拍一下,其实标定的时候会发现有些图加进来效果变差会删去(可能拍摄时动了),到最后其实留下个6-10组一般都会挺准的;
5).标定板尺寸写错了,比如8*10的横线组成的其实是7*9的标定板,看的是网格内点;还可能是每个小格子大小写错了一般写到几毫米就行;
6).不好标就多拍个20组图,然后多试几种组合删掉一些差得图,看误差retval一般要小于1;

以下代码是我在项目里用过的版本的初始版,简单改了下,由于没数据就找了两组以前的图跑了下能直接跑通,数据多是可以用的;
整体文件保存路径为:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wtnyOXXg-1587459692085)(_v_images/20200421160747869_4878.png)]
left和right文件夹如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l04qLOzN-1587459692089)(_v_images/20200421160822795_6015.png)]

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Google style docstrings.
Example:
    <scripts-name> --help
"""

import cv2
import numpy as np
import glob
import json


def calibrate(images, objpoints, cheese_size, show_img=False, fnames=[]):
    # termination criteria
    criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
    imgpoints = []
    # debug
    num = 0
    for img in images:
        gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
        # Find the chess board corners
        # import pdb; pdb.set_trace()
        # cv2.imshow('img', img)
        # cv2.waitKey(0)
        ret, corners = cv2.findChessboardCorners(gray, cheese_size, None)

        # If found, add object points, image points (after refining them)
        if ret == True:
            cv2.cornerSubPix(gray, corners, (5, 5), (-1, -1), criteria)
            # print(corners)
            imgpoints.append(corners)
            if show_img:
                # Draw and display the corners
                img = np.copy(img)
                cv2.drawChessboardCorners(img, cheese_size, corners, ret)
                cv2.imshow('img', img)
                cv2.waitKey(0)
        else:
            print("Not find corner in img! in {}".format(fnames[num]))
        num = num + 1
    if show_img:
        cv2.destroyAllWindows()
    #print("gray shape:", gray.shape)
    #print("gray shape-1:", gray.shape[::-1])

    # input('回车下一步')
    return cv2.calibrateCamera(objpoints, imgpoints, gray.shape[::-1], None, None,
                               flags=cv2.CALIB_RATIONAL_MODEL), imgpoints


def drawLine(img, num=16):
    h, w, *_ = img.shape
    for i in range(0, h, h // num):
        cv2.line(img, (0, i), (w, i), (0, 255, 0), 1, 8)
    # for i in range(0, w, w // num):
    #     cv2.line(img, (i, 0), (i, h), (0, 255, 0), 1, 8)
    return img

import os
def listdir(path, list_name):
    for file in os.listdir(path):
        file_path = os.path.join(path, file)
        list_name.append(file_path)


_DIST_ = False
# fns = glob.glob('left_*.jpg')
# print("fns0:", fns)
fns =[]
left_path = '.\\left\\'
#bgr_path = '.\\nir\\'
listdir(left_path, fns) 
print("fns:", fns)
cheese_size = (7, 9)
corner_num = cheese_size[0] * cheese_size[1]
unit = 23  # 实际的格子间距这里是 23mm

objp = np.zeros((corner_num, 3), np.float32)
objp[:, :2] = np.mgrid[0:cheese_size[0], 0:cheese_size[1]].T.reshape(-1, 2)
objp *= unit
print("objp:", objp.shape)
stereo_right_images = []
stereo_left_images = []
objpoints = []
save = False

for fn in fns:
    left_img = cv2.imread(fn)
    stereo_left_images.append(left_img)

    right_img = cv2.imread(fn.replace('left', 'right'))
    stereo_right_images.append(right_img)

    objpoints.append(objp)
print(len(stereo_left_images), len(stereo_right_images))

# 单目标定
x, stereo_right_corners = calibrate(
    stereo_right_images, objpoints, cheese_size=cheese_size, show_img=True, fnames=fns)
stereo_right_ret, stereo_right_mtx, stereo_right_dist, stereo_right_rvecs, stereo_right_tvecs = x
# stereo_nir_dist = np.zeros_like(stereo_nir_dist)
print('right cali done...')

x, stereo_left_corners = calibrate(
    stereo_left_images, objpoints, cheese_size=cheese_size, show_img=True, fnames=fns)
stereo_left_ret, stereo_left_mtx, stereo_left_dist, stereo_left_rvecs, stereo_left_tvecs = x
print('left cali done...')

if _DIST_:
    stereo_right_dist = np.zeros_like(stereo_right_dist)
    stereo_left_dist = np.zeros_like(stereo_left_dist)

# 双目标定
retval, stereo_left_mtx, stereo_left_dist, stereo_right_mtx, stereo_right_dist, R_left2right, T_left2right, E_left2right, F_left2right= \
    cv2.stereoCalibrate(np.array(objpoints), np.squeeze(np.array(stereo_left_corners)),
                        np.squeeze(np.array(stereo_right_corners)
                                   ), stereo_left_mtx, stereo_left_dist,
                        stereo_right_mtx, stereo_right_dist, (stereo_left_images[0].shape[0],
                                                          stereo_left_images[0].shape[1]),
                        flags=cv2.CALIB_RATIONAL_MODEL)  # python3 transfer stereo_left -> stereo_right
h, w, c = stereo_right_images[0].shape
print('stereo cali done...')

if _DIST_:
    stereo_right_dist = np.zeros_like(stereo_right_dist)
    stereo_left_dist = np.zeros_like(stereo_rgb_dist)

# 双目矫正
R1, R2, P1, P2, Q, validPixROI1, validPixROI2 = cv2.stereoRectify(
    stereo_left_mtx, stereo_left_dist, stereo_right_mtx, stereo_right_dist, (w, h), R_left2right, T_left2right, alpha=0)
R1[0, :] *= -1
R2[0, :] *= -1
print('stereo rectify done...')

# 得到映射变换
stereo_left_mapx, stereo_left_mapy = cv2.initUndistortRectifyMap(
    stereo_left_mtx, stereo_left_dist, R1, P1, (w, h), 5)
stereo_right_mapx, stereo_right_mapy = cv2.initUndistortRectifyMap(
    stereo_right_mtx, stereo_right_dist, R2, P2, (w, h), 5)
print('initUndistortRectifyMap done...')

if save:
    np.save('R1', R1)
    np.save('R2', R2)
    np.save('P1', P1)   
    np.save('P2', P2)
    np.save('Q', Q)
    np.save('stereo_right_mtx', stereo_right_mtx)
    np.save('stereo_right_dist', stereo_right_dist)
    np.save('stereo_left_mtx', stereo_left_mtx)
    np.save('stereo_left_dist', stereo_left_dist)
    np.save('R_left2right', R_left2right)
    np.save('T_left2right', T_left2right)
    np.save('stereo_left_mapx', stereo_left_mapx)
    np.save('stereo_left_mapy', stereo_left_mapy)
    np.save('stereo_right_mapx', stereo_righr_mapx)
    np.save('stereo_right_mapy', stereo_right_mapy)
    print('save parameters done...')

print('stereo_right_mtx', stereo_right_mtx)
print('stereo_right_dist', stereo_right_dist)
print('stereo_left_mtx', stereo_left_mtx)
print('stereo_left_dist', stereo_left_dist)
print('R_left2right', R_left2right)
print('T_left2right', T_left2right)
print('P2', P2) # 内参

# 可视化验证,看网格是否对齐
for fn in fns:
    left_img = cv2.imread(fn)
    right_img = cv2.imread(fn.replace('left', 'right'))

    frame0 = cv2.remap(right_img, stereo_right_mapx,
                       stereo_right_mapy, cv2.INTER_LINEAR)
    frame1 = cv2.remap(left_img, stereo_left_mapx,
                       stereo_left_mapy, cv2.INTER_LINEAR)

    img = np.concatenate((frame0, frame1), axis=1).copy()
    img = drawLine(img, 32)
    cv2.imshow('img', img)
    ret = cv2.waitKey(0)

4.4.标定完有什么用?

重建任务可能需要用到内外参;
常见的双目匹配任务需要用到内参矩阵(代码里的P2),里面保存了FB,用来转换视差和深度,disp=FB/depth,也可以利用FB大小验证标定结果,FB=焦距*双目间距离,距离一般我们都知道大概多远;
双目匹配等任务会对图像进行矫正后使用,用到了代码里的cv2.remap()、mapx矩阵和mapy矩阵;

4.5.opencv相关函数说明

这里的翻译参考自https://blog.csdn.net/qq_36537774/article/details/85005552。

  • 4.5.1立体标定函数 stereoCalibrate() :
    同时标定两个摄像头,计算出两个摄像头的自己的内外参数矩阵,还能求出两个摄像头之间的旋转矩阵R,平移矩阵T。
double stereoCalibrate(InputArrayOfArrays objectPoints, InputArrayOfArrays imagePoints1,
             InputArrayOfArrays imagePoints2, InputOutputArray cameraMatrix1,
             InputOutputArray distCoeffs1, InputOutputArray cameraMatrix2, 
             InputOutputArray distCoeffs2, Size imageSize, OutputArray R,
             OutputArray T, OutputArray E, OutputArray F, TermCriteria criteria=
             TermCriteria(TermCriteria::COUNT+TermCriteria::EPS, 30, 1e-6), int
             flags=CALIB_FIX_INTRINSIC )
 
1.objectPoints- vector<point3f> 型的数据结构,存储标定角点在世界坐标系中的位置;
2.imagePoints1- vector<vector<point2f>> 型的数据结构,存储标定角点在第一个摄像机下的投影后的亚像素坐标;
3.imagePoints2- vector<vector<point2f>> 型的数据结构,存储标定角点在第二个摄像机下的投影后的亚像素坐标;
4.cameraMatrix1-输入/输出型的第一个摄像机的相机矩阵。如果CV_CALIB_USE_INTRINSIC_GUESS , CV_CALIB_FIX_ASPECT_RATIO ,CV_CALIB_FIX_INTRINSIC , or CV_CALIB_FIX_FOCAL_LENGTH其中的一个或多个标志被设置,该摄像机矩阵的一些或全部参数需要被初始化;
5.distCoeffs1-第一个摄像机的输入/输出型畸变向量。根据矫正模型的不同,输出向量长度由标志决定;
6.cameraMatrix2-输入/输出型的第二个摄像机的相机矩阵。参数意义同第一个相机矩阵相似;
7.distCoeffs2-第一个摄像机的输入/输出型畸变向量。根据矫正模型的不同,输出向量长度由标志决定;
8.imageSize-图像的大小;
9.R-输出型,第一和第二个摄像机之间的旋转矩阵;
10.T-输出型,第一和第二个摄像机之间的平移矩阵;
11.E-输出型,基本矩阵;
12.F-输出型,基础矩阵;
13.term_crit-迭代优化的终止条件
14.flag-
 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
  • 4.5.2立体校正函数 stereoRectify() :
    为每个摄像头计算立体校正的映射矩阵,这里不对图片进行立体矫正,而是计算进行立体矫正所需要的映射矩阵。
void stereoRectify(InputArray cameraMatrix1, InputArray distCoeffs1, 
           InputArray cameraMatrix2,InputArray distCoeffs2, Size imageSize, 
           InputArray R, InputArray T,OutputArray R1, OutputArray R2, OutputArray P1, 
           OutputArray P2, OutputArray Q, int flags=CALIB_ZERO_DISPARITY, double alpha=-1, 
           Size newImageSize=Size(), Rect* validPixROI1=0, Rect* validPixROI2=0 )

1.cameraMatrix1-第一个摄像机的摄像机矩阵;
2.distCoeffs1-第一个摄像机的畸变向量;
3.cameraMatrix2-第二个摄像机的摄像机矩阵;
4.distCoeffs1-第二个摄像机的畸变向量;
5.imageSize-图像大小;
6.R- stereoCalibrate() 求得的R矩阵;
7.T- stereoCalibrate() 求得的T矩阵;
8.R1-输出矩阵,第一个摄像机的校正变换矩阵(旋转变换);
9.R2-输出矩阵,第二个摄像机的校正变换矩阵(旋转矩阵);
10.P1-输出矩阵,第一个摄像机在新坐标系下的投影矩阵;
11.P2-输出矩阵,第二个摄像机在想坐标系下的投影矩阵;
12.Q-4*4的深度差异映射矩阵;
13.flags-可选的标志有两种零或者 CV_CALIB_ZERO_DISPARITY ,如果设置 CV_CALIB_ZERO_DISPARITY 的话,该函数会让两幅校正后的图像的主点有相同的像素坐标。否则该函数会水平或垂直的移动图像,以使得其有用的范围最大
14.alpha-拉伸参数。如果设置为负或忽略,将不进行拉伸。如果设置为0,那么校正后图像只有有效的部分会被显示(没有黑色的部分),如果设置为1,那么就会显示整个图像。设置为0~1之间的某个值,其效果也居于两者之间。
15.newImageSize-校正后的图像分辨率,默认为原分辨率大小。
16.validPixROI1-可选的输出参数,Rect型数据。其内部的所有像素都有效
17.validPixROI2-可选的输出参数,Rect型数据。其内部的所有像素都有效
  • 4.5.3映射变换计算函数 initUndistortRectifyMap():

是计算畸变矫正和立体校正的映射变换,可以用map1和map2来矫正图像。

void initUndistortRectifyMap(InputArray cameraMatrix, InputArray 
                 distCoeffs, InputArray R,InputArray newCameraMatrix, Size size, int 
                 m1type, OutputArray map1, OutputArray map2)
 
1.cameraMatrix-摄像机参数矩阵
2.distCoeffs-畸变参数矩阵
3.R- stereoCalibrate() 求得的R矩阵
4.newCameraMatrix-矫正后的摄像机矩阵(可省略)
5.Size-没有矫正图像的分辨率
6.m1type-第一个输出映射的数据类型,可以为 CV_32FC1  或  CV_16SC2 
7.map1-输出的第一个映射变换
8.map2-输出的第二个映射变换
  • 4.5.4几何变换函数 remap():
    利用映射矩阵对一张图进行映射。
void remap(InputArray src, OutputArray dst, InputArray map1, InputArray 
      map2, int interpolation,int borderMode=BORDER_CONSTANT, const Scalar& 
      borderValue=Scalar())
1.src-原图像
2.dst-几何变换后的图像
3.map1-第一个映射,无论是点(x,y)或者单纯x的值都需要是CV_16SC2 ,CV_32FC1 , 或 CV_32FC2类型
4.map2-第二个映射,y需要是CV_16UC1 , CV_32FC1类型。或者当map1是点(x,y)时,map2为空。
5.interpolation-插值方法,但是不支持最近邻插值

参考:
百度百科: https://baike.baidu.com/item/%E7%9B%B8%E6%9C%BA%E6%A0%87%E5%AE%9A/6912991?fr=aladdin
相机模型:https://blog.csdn.net/qq_33278461/article/details/101026259
opencv函数:https://blog.csdn.net/qq_36537774/article/details/85005552
opencv函数说明:https://blog.csdn.net/jaccen2012/article/details/51217936
opencv文档和各种参考内容。

  • 5
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值