参考文档
摄像机标定(Camera calibration)简单来说是从世界坐标系换到图像坐标系的过程,也就是求最终的投影矩阵P的过程。基本的坐标系:世界坐标系;相机坐标系;图像坐标系;
一般来说,标定的过程分为两个部分:
第一步是从世界坐标系转换为相机坐标系,这一步是三维点到三维点的转换,包括 R,t (相机外参)等参数;
第二步是从相机坐标系转为图像坐标系,这一步是三维点到二维点的转换,包括 K (相机内参)等参数;
坐标系介绍
知道上面的简单知识后,如果知道相机坐标系中的一个点 X(现实三维世界中的点),在像平面坐标系对应的点是 x,要求求从相机坐标系转为像平面坐标系的转换,也就是从 X 点的( X , Y , Z )通过一定的转换变为x点的(x,y)注意(X,Y,Z)(大写)是在相机坐标系 , 而(小写)是在像平面坐标系(还不是图像坐标系,原点不同。)观察第二个图,很简单的可以得到这个转换:
可以表示为矩阵计算为:
可以简写为(P就是所谓的投影矩阵,当然现在还不完整):
加入偏移量:
通过上面,可以把相机坐标系转换到像平面坐标系,但是像平面坐标系和图像坐标系虽然在同一个平面上,但是原点并不是同一个,而目标是要转换到图像坐标系下,所以还需要一步操作,如下图:
其中主点 p 是像平面坐标系的原点,但在图像坐标系中的位置为(px,py)在这里,图形坐标系的原点是图片的左下角,所以可以得到:
相当于在上面的基础上加了一个p点坐标的偏移量,同时可以表示为矩阵计算为(在这里用的是齐次坐标的表示方式):
最后得到相机内参K
和投影矩阵P:
坐标系介绍
从世界坐标系转换到相机坐标系是三维空间到三维空间的变换,一般来说需要一个平移操作和一个旋转操作就可以完成这个转换,用公式表示如下(可以理解为世界坐标系原点先平移到相机坐标系的位置然后在做一次坐标系旋转,使坐标轴对齐。):
根据上面的公式可以得到从一个三维点从世界坐标系转换到相机坐标的变换公式如下(也是用的齐次坐标的表示方式):
根据上面的讨论知道了怎样从世界坐标系转换到相机坐标系(平移和旋转)以及从相机坐标系转换到图像坐标系(相机内参变换),所以带入上面的矩阵计算,可以得到:
这样就得到了最终的投影矩阵 P:
其中:
在这里,K一般称为相机内参(intrinsic parameters),描述了相机的内部参数,包括焦距 f、主点 p的位置、以及像素与真实环境的大小比例等,这个是固有属性,是提供好的;R和t称为相机外参(extrinsic parameters),R在这里是旋转矩阵,可以转换为三维的旋转向量,分别表示绕x ,y ,z 三个轴的旋转角度,t 目前就是一个平移向量,分别表示在x ,y ,z、三个方向上的平移量。
径向畸变的效应有三种,一种是桶形畸变(barrel distortion),另一种是枕形畸变(pincushion distortion),还有一种是两种的结合叫做胡子畸变(mustache distortion)
公式修正:
切向畸变是由于透镜与成像平面不严格的平行,其可以用如下公式修正:
最终得到5个畸变参数:
那么可以利用这些来进行最终的任务相机标定,简单的过程可以描述为通过标定板,如下图,可以得到n个对应的世界坐标三维点 Xi和对应的图像坐标二维点 xi,这些三维点到二维点的转换都可以通过上面提到的相机内参 K ,相机外参 R 和 t ,以及畸变参数 D 经过一系列的矩阵变换得到。现在就用这些对应关系来求解这些相机参数。最后就是用线性方法求解方程式。
import cv2
import numpy as np
import glob
# 找棋盘格角点
# 阈值
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
#棋盘格模板规格
w = 7 #内角点个数,内角点是和其他格子连着的点
h = 7
# 世界坐标系中的棋盘格点,例如(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/5_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))
mtx为内参数矩阵,dist为畸变系数:
rvecs为旋转向量:
tvecs为平移向量:
矫正前的图片:
矫正后的图片:
矫正后的图片可以明显的看出左上角的变化