摄像机标定
【相机模型】和【相机参数】相关内容看这里:
1.什么是相机标定:
图像测量和机器视觉里,为了能确定现实世界的任意一个点到图像上对应像素点的投影位置,需要建立一个相机成像的几何模型。
几何模型的参数就是相机参数,包括内参、外参和畸变参数。
得到这个参数的过程就叫相机标定。
2.相机标定的目的
1).近似得到二维到三维的函数映射,可以用在深度估计,三维重建里;
2).去畸变,就是去掉镜头里的扭曲,一条直线投影到图片上不能保持为一条直线了,跟摄像机镜头有关;
3.相机标定的方法
1).传统相机标定法:
利用尺寸已知的标定物,建立标定物上某些点与图像上对应点之间的映射,通过算法近似得到相机模型的内外参数。
平面标定物,如标定版或者打印的纸需要拍摄两张以上图像标定;三维标定物单张图像也可以标定;
主要分为线性标定法、非线性优化标定法、两步标定法(【张正友标定法】、Tsai经典两步法)
2).主动视觉相机标定方法:
已知相机某些运动信息对相机自标定,无需标定物,但需要控制相机做某些特殊运动;
优点是算法简单,往往能够获得线性解,故鲁棒性较高;
缺点是系统的成本高、实验设备昂贵、实验条件要求高,而且不适合于运动参数未知或无法控制的场合。
3).相机自标定法:
利用相机运动约束,相机运动约束一般太强,实际中不准确;
利用场景约束,利用场景中的平行正交信息,其中空间平行线在相机图像平面上的交点被称为消失点,这种方法是基于消失点的自标定方法;
自标定的好处是灵活,也可以在线自标定,由于它是基于绝对二次曲线或曲面的方法,其算法鲁棒性差。
4.相机标定
4.1.像素坐标系-图像坐标系-相机坐标系-世界坐标系转换公式:
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;
以下代码是我在项目里用过的版本的初始版,简单改了下,由于没数据就找了两组以前的图跑了下能直接跑通,数据多是可以用的;
整体文件保存路径为:
left和right文件夹如下:
#!/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相关函数说明
这里的翻译参考自。
- 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
相机模型:
opencv函数:
opencv函数说明:
opencv文档和各种参考内容。