在图像测量过程以及机器视觉应用中,为确定空间物体表面某点的三维几何位置与其在图像中对应点之间的相互关系,必须建立相机成像的几何模型,这些几何模型参数就是相机参数。在大多数条件下这些参数必须通过实验与计算才能得到,这个求解参数的过程就称之为相机标定(或摄像机标定)。无论是在图像测量或者机器视觉应用中,相机参数的标定都是非常关键的环节,其标定结果的精度及算法的稳定性直接影响相机工作产生结果的准确性。因此,做好相机标定是做好后续工作的前提,提高标定精度是科研工作的重点所在。
由于摄像机可安放在环境中的任意位置,在环境中选择一个基准坐标系来描述摄像机的位置,并用它描述环境中任何物体的位置,该坐标系称为世界坐标系。摄像机坐标系与世界坐标系之间的关系可以用旋转矩阵与平移向量来描述。
与此相关的是图像坐标系和摄像机坐标系
摄像机采集的数字图像在计算机内可以存储为数组,数组中的每一个元素(象素,pixel)的值即是图像点的亮度(灰度)。如图所示,在图像上定义直角坐标系u-v,每一象素的坐标(u,v)分别是该象素在数组中的列数和行数。故(u,v)是以
象素为单位的图像坐标系坐标。
由于图像坐标系只表示象素位于数字图像的列数和行数,并没有用物理单位表示出该象素在图像中的物理位置,因而需要再建立以物理单位(例如厘米)表示的成像平面坐标系x-y,如上图所示。我们用(x,y)表示以物理单位度量的成像平面坐标系的坐标。在x-y坐标系中,原点定义在摄像机光轴和图像平面的交点处,称为图像的主点(principal point),该点一般位于图像中心处,但由于摄像机制作的原因,可能会有些偏离,在坐标系下的坐标为(u0,v0),每个象素在x轴和y轴方向上的物理尺寸为dx、dy,两个坐标系的关系如下:
其中s’表示因摄像机成像平面坐标轴相互不正交引出的倾斜因子(skew factor)。
摄像机成像几何关系可由图表示,其中O点称为摄像机光心,轴和轴
与成像平面坐标系的x轴和y轴平行,轴为摄像机的光轴,和图像平面垂直。光轴与图像平面的交点为图像主点O’,由点O与轴组成的直角坐标系称为摄像机坐标系。OO’为摄像机焦距。
在环境中还选择一个参考坐标系来描述摄像机和物体的位置,该坐标系称为世界坐标系。摄像机坐标系和世界坐标系之间的关系可用旋转矩阵R与平移向量t来描述。由此,空间中一点P在世界坐标系和摄像机坐标系下的齐次坐标分别为和且存在如下关系:
其中R是3×3的旋转矩阵,t是3×1的平移向量,为(0,0,0),M1是两个坐标系之间的联系矩阵。
摄像机坐标系
摄影机坐标系的原点为摄像机光心,x轴与y轴与图像的X,Y轴平行,z轴为摄像机光轴,它与图像平面垂直,以此构成的空间直角坐标系称为摄像机坐标系,也称为相机坐标系,摄像机坐标系是三维坐标系。光轴与图像平面的交点,即为图像坐标系的原点,与图像的X、Y轴构成的直角坐标系即为图像坐标系,图像坐标系是二维坐标系。
普通相机的成像模型一采用小孔成像,即初中蜡烛成像实验,物体经小孔后,在成像平面成倒立的像。
小孔成像模型虽然充分考虑了相机内部参数对成像的影响,但没有考虑成像系统另一个重要的部分,镜头。镜头常用的有普通镜头、广角镜头、鱼眼镜头等,在无人驾驶和视觉slam领域,鱼眼镜头和广角镜头用的很多,主要是视角很大,可以观测到更多的信息。任何镜头都存在不同程度的畸变,不同类型的镜头用到的畸变模型也不相同。根据镜头制造和成像的物理特性,普通镜头主要考虑径向畸变和切向畸变,且畸变模型都可以用多项式来近似。而对于大广角、鱼眼镜头,普通镜头的物理模型不能适用了。
透过镜头边缘的光线很容易产生径向畸变,光线离镜头中心越远,畸变越大。
从左到右依次为正常无畸变、桶形畸变、枕形畸变
径向畸变可以用如下公式修正:
切向畸变主要发生在相机sensor和镜头不平行的情况下;因为有夹角,所以光透过镜头传到图像传感器上时,成像位置发生了变化。
切向畸变可以用如下公式修正:
世界坐标系下的点P与图像坐标的关系可以表达为:
相机标定就是标定内参和外参,通过一种理论数学模型和优化的手段来近似实际的物理成像关系。
我们需要知道成像平面内的物体在机器人或者车辆坐标系下的位置时,需要进行一个坐标转换,称为外参(Extrinsic parameters),它与相机制造、镜头畸变没有任何关系,只与相机在世界坐标系内的安装位置和角度有关。从纯数学的角度来说,刚体运动和坐标变换总是可以分解为一个旋转运动和一个平移运动。
由上述算式可得:
上述公式得出B矩阵,由内参A和矩阵B的关系可得:
测试数据(拍摄了12张图,根据测试结果最后采用了6张):
代码:
import cv2
import numpy as np
import glob
# 找棋盘格角点
# 阈值
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
#棋盘格模板规格
w = 14 #内角点个数,内角点是和其他格子连着的点
h = 9
# 世界坐标系中的棋盘格点,例如(0,0,0), (1,0,0), (2,0,0) ....,(8,5,0),去掉Z坐标,记为二维矩阵
objp = np.zeros((w*h,3), np.float32)
objp[:,:2] = np.mgrid[0:w,0:h].T.reshape(-1,2)
# 储存棋盘格角点的世界坐标和图像坐标对
objpoints = [] # 在世界坐标系中的三维点
imgpoints = [] # 在图像平面的二维点
images = glob.glob('picture/*.jpg')
for fname in images:
img = cv2.imread(fname)
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
# 找到棋盘格角点
# 棋盘图像(8位灰度或彩色图像) 棋盘尺寸 存放角点的位置
ret, corners = cv2.findChessboardCorners(gray, (w,h),None)
# 如果找到足够点对,将其存储起来
if ret == True:
# 角点精确检测
# 输入图像 角点初始坐标 搜索窗口为2*winsize+1 死区 求角点的迭代终止条件
cv2.cornerSubPix(gray,corners,(11,11),(-1,-1),criteria)
objpoints.append(objp)
imgpoints.append(corners)
# 将角点在图像上显示
cv2.drawChessboardCorners(img, (w,h), corners, ret)
cv2.imshow('findCorners',img)
cv2.waitKey(1000)
cv2.destroyAllWindows()
#标定、去畸变
# 输入:世界坐标系里的位置 像素坐标 图像的像素尺寸大小 3*3矩阵,相机内参数矩阵 畸变矩阵
# 输出:标定结果 相机的内参数矩阵 畸变系数 旋转矩阵 平移向量
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints, gray.shape[::-1], None, None)
# mtx:内参数矩阵
# dist:畸变系数
# rvecs:旋转向量 (外参数)
# tvecs :平移向量 (外参数)
print (("ret:"),ret)
print (("mtx:/n"),mtx) # 内参数矩阵
print (("dist:/n"),dist) # 畸变系数 distortion cofficients = (k_1,k_2,p_1,p_2,k_3)
print (("rvecs:/n"),rvecs) # 旋转向量 # 外参数
print (("tvecs:/n"),tvecs) # 平移向量 # 外参数
# 去畸变
img2 = cv2.imread('picture/2_d.jpg')
h,w = img2.shape[:2]
# 我们已经得到了相机内参和畸变系数,在将图像去畸变之前,
# 我们还可以使用cv.getOptimalNewCameraMatrix()优化内参数和畸变系数,
# 通过设定自由自由比例因子alpha。当alpha设为0的时候,
# 将会返回一个剪裁过的将去畸变后不想要的像素去掉的内参数和畸变系数;
# 当alpha设为1的时候,将会返回一个包含额外黑色像素点的内参数和畸变系数,并返回一个ROI用于将其剪裁掉
newcameramtx, roi=cv2.getOptimalNewCameraMatrix(mtx,dist,(w,h),0,(w,h)) # 自由比例参数
dst = cv2.undistort(img2, mtx, dist, None, newcameramtx)
# 根据前面ROI区域裁剪图片
x,y,w,h = roi
dst = dst[y:y+h, x:x+w]
cv2.imwrite('calibresult.jpg',dst)
# 反投影误差
# 通过反投影误差,我们可以来评估结果的好坏。越接近0,说明结果越理想。
# 通过之前计算的内参数矩阵、畸变系数、旋转矩阵和平移向量,使用cv2.projectPoints()计算三维点到二维图像的投影,
# 然后计算反投影得到的点与图像上检测到的点的误差,最后计算一个对于所有标定图像的平均误差,这个值就是反投影误差。
total_error = 0
for i in range(len(objpoints)):
imgpoints2, _ = cv2.projectPoints(objpoints[i], rvecs[i], tvecs[i], mtx, dist)
error = cv2.norm(imgpoints[i],imgpoints2, cv2.NORM_L2)/len(imgpoints2)
total_error += error
print (("total error: "), total_error/len(objpoints))
外参矩阵:
(1)旋转向量:
(2)平移向量
反向投影误差:
在实验过程中,根据不同的标定板,参数也不同,横纵向内角点个数也直接影响了标定的结果,从结果来看,反向投影误差越接近0其结果越好,0.2数值还可以,但是原因是使用的标定图片较少,如果多了误差会大很多。原因可能是标定板的面不够平整,以及拍摄的问题,图片光线有影响。