前言:相机标定是我们相机拍摄的物体都处于三维世界坐标系中,而相机拍摄成像时把三维相机坐标系向二维图像坐标系转换。不同镜头成像时的转换矩阵不同可能引入失真,标定的作用是近似地估算出转换矩阵和失真系数。为了估算,需要知道若干点的三维世界坐标系中的坐标和二维图像坐标系中的坐标。传统的照相机标定方法是通过世界坐标集(Xi,Yi,Zi),以及它们在图像平面上的投影坐标集(ui,vi),计算相机投影矩阵M中的 11个未知参数,需要严格个出三个两两互相垂直的平面来做标定(条件较为严格,一般情况难以实现)。而棋盘标定只需要两个平面,只需要黑白格子相交的角点来标记会比原始标定容易许多。总而言之棋盘标定的意义就在于克服了传统标定法需要的高精度标定物的缺点,而仅需使用一个打印出来的棋盘格就可以。
目录
照相机标定
(一)张正友相机标定原理
1.计算内参和外参的初值
2、最大似然估计
3.径向畸变估计
(二)实现棋盘标定
(三)遇到的问题
(四)完整代码+图片下载
原理参考:https://blog.csdn.net/zkl99999/article/details/48372203
https://blog.csdn.net/u010128736/article/details/52860364
相机标定参考:https://blog.csdn.net/JennyBi/article/details/85764988
相机模型:
张氏标定是一种基于平面棋盘格的标定,首先应该从两个平面的单应性(homography)映射开始着手。单应性(homography):在计算机视觉中被定义为一个平面到另一个平面的投影映射。首先看一下,图像平面与标定物棋盘格平面的单应性。
摄像机模型得到:
其中m的齐次坐标表示图像平面的像素坐标(u,v,1),M的齐次坐标表示世界坐标系的坐标点(X,Y,Z,1)。A[R t]即是上面一篇博客推出的P。R表示旋转矩阵、t表示平移矩阵、S表示尺度因子。A表示摄像机的内参数,具体表达式如下:
(1)计算单应性矩阵H
根据上面的摄像机模型,同理可以设三维世界坐标的点为X=[X,Y,Z,1]TX=[X,Y,Z,1]T,二维相机平面像素坐标为m=[u,v,1]Tm=[u,v,1]T,所以标定用的棋盘格平面到图像平面的单应性关系为:
其中s为尺度因子,K为摄像机内参数,R为旋转矩阵,T为平移向量。令
注意,s对于齐次坐标来说,不会改变齐次坐标值。张氏标定法中,将世界坐标系狗仔在棋盘格平面上,令棋盘格平面为Z=0的平面。则可得
我们把K[r1, r2, t]叫做单应性矩阵H,可以分析一下,H是一个三3*3的矩阵,并且有一个元素是作为齐次坐标。因此,H有8个未知量待解。(x,y)作为标定物的坐标,可以由设计者人为控制,是已知量。(u,v)是像素坐标,我们可以直接通过摄像机获得。对于一组对应的(x,y)-à(u,v)我们可以获得两组方程。现在有8个未知量需要求解,所以我们至少需要八个方程。所以需要四个对应点。四点即可算出,图像平面到世界平面的单应性矩阵H。 这也是张氏标定采用四个角点的棋盘格作为标定物的一个原因。在这里,我们可以将单应性矩阵写成三个列向量的形式,即:
(2)计算内参矩阵
有上述式子可得:
1、r1,r2正交 得:r1r2=0。这个很容易理解,因为r1,r2分别是绕x,y轴旋转的。应用高中立体几何中的两垂直平面上(两个旋转向量分别位于y-z和x-z平面)直线的垂直关系即可轻松推出。
2、旋转向量的模为1,即|r1|=|r2|=1。这个也很容易理解,因为旋转不改变尺度嘛。如果不信可以回到上一篇博客,找到个方向的旋转矩阵化行列式算一下。即
由于旋转矩阵两列相交(r1和r2正交)内积为0可得 :
代入r1 和 r2的值可得:
即每个单应性矩阵能提供两个方程,而内参数矩阵包含5个参数,要求解,至少需要3个单应性矩阵。为了得到三个不同的单应性矩阵,我们使用至少三幅棋盘格平面的图片进行标定。通过改变相机与标定板之间的相对位置来得到三个不同的图片。为了方便计算,定义如下:
可以看到,B是一个对称阵,所以B的有效元素为六个,让这六个元素写成向量b,即
推导得到 :
利用约束条件可以得到:
通过上式,我们至少需要三幅包含棋盘格的图像,可以计算得到B,然后通过cholesky分解,得到相机的内参数矩阵K。但是在实际运行操作的时候,尽量拍照棋盘格图像(15~20张左右),图像越多得到的矩阵越多,计算出来的参数值就越精确。
(3)计算外参矩阵
由上面的相机模型机及内参的推导,可得 :
上述的推导结果是基于理想情况下的解,但由于可能存在高斯噪声,所以使用最大似然估计进行优化。设采集了n副包含棋盘格的图像进行定标,每个图像里有棋盘格角点m个。令第i副图像上的角点Mj在上述计算得到的摄像机矩阵下图像上的投影点为:
其中Ri和ti是第i副图对应的旋转矩阵和平移向量,K是内参数矩阵。则角点mij的概率密度函数为:
构造似然函数:
让L取得最大值,即让下面式子最小。这里使用的是多参数非线性系统优化问题的Levenberg-Marquardt算法[2]进行迭代求最优解。
张氏标定法只关注了影响最大的径向畸变。则数学表达式为:
其中,(u,v)是理想无畸变的像素坐标,(u^,v^)(u^,v^)是实际畸变后的像素坐标。(u0,v0)代表主点,(x,y)是理想无畸变的连续图像坐标,(x^,y^)(x^,y^)是实际畸变后的连续图像坐标。k1和k2为前两阶的畸变参数。
化作矩阵形式:
记做:
计算得到畸变系数k。
使用最大似然的思想优化得到的结果,即像上一步一样,LM法计算下列函数值最小的参数值:
综上式子计算就能得到了张正友相机标定的相机内参、外参和畸变系数。
(1)棋盘环境
运行环境:pyhton3.7、opencv-python-3.4.2.16、IDLE编译器
测试条件:手机拍照、打印棋盘纸张(图像16张,建议15~20张内)、手机型号DIG-AL00
(2)代码
import numpy as np
import cv2
import glob
'''
在这里,我的棋盘格是8*8的,所以角点个数为7*7,当然棋盘格的行列个数可以不一样;
如果想方便代码改变棋盘格数,是以定义两个变量w(列角点数)和h(行角点数),注意如果角点维数超出的话,标定的时候会报错。
'''
# 终止标准
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)......,(6,5,0)
#objp = np.zeros((w*h,3), np.float32)
objp = np.zeros((7*7,3), np.float32)
objp[:,:2] = np.mgrid[0:7,0:7].T.reshape(-1,2)
# 用于存储所有图像中的对象点和图像点的数组。
objpoints = [] # 在现实世界空间的3d点
imgpoints = [] # 图像平面中的2d点。
#glob是个文件名管理工具
images = glob.glob('E:/Python37_course/test5/*.jpg')
print('...loading')
for fname in images:
#对每张图片,识别出角点,记录世界物体坐标和图像坐标
print(f'processing img:{fname}')
img = cv2.imread(fname)
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)#转灰度
print('grayed')
#寻找角点,存入corners,ret是找到角点的flag
#ret, corners = cv2.findChessboardCorners(gray, (w, h),None)
ret, corners = cv2.findChessboardCorners(gray, (7, 7),None)
# 如果找到,添加对象点,图像点(精炼后)
if ret == True:
print('chessboard detected')
objpoints.append(objp)
#执行亚像素级角点检测
corners2 = cv2.cornerSubPix(gray,corners,(11,11),(-1,-1),criteria)
imgpoints.append(corners2)
# 绘制并显示角点
#img = cv2.drawChessboardCorners(img, (w,h), corners2,ret)
img = cv2.drawChessboardCorners(img, (7,7), corners2,ret)
cv2.namedWindow('img',0)
cv2.resizeWindow('img', 500, 500)
cv2.imshow('img',img)
cv2.waitKey(500)
cv2.destroyAllWindows()
'''
传入所有图片各自角点的三维、二维坐标,相机标定。
每张图片都有自己的旋转和平移矩阵,但是相机内参和畸变系数只有一组。
mtx,相机内参;dist,畸变系数;revcs,旋转矩阵;tvecs,平移矩阵。
'''
img2 = cv2.imread("E:/Python37_course/test5/5test11.jpg")
print(f"type objpoints:{objpoints[0].shape}")
print(f"type imgpoints:{imgpoints[0].shape}")
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints, gray.shape[::-1],None,None)
h, w = img2.shape[:2]
'''
优化相机内参(camera matrix),这一步可选。
参数1表示保留所有像素点,同时可能引入黑色像素,
设为0表示尽可能裁剪不想要的像素,这是个scale,0-1都可以取。
'''
newcameramtx, roi=cv2.getOptimalNewCameraMatrix(mtx,dist,(w,h),1,(w,h))
#纠正畸变
dst = cv2.undistort(img2, mtx, dist, None, newcameramtx)
# 裁剪图像,输出纠正畸变以后的图片
x,y,w,h = roi
dst = dst[y:y+h, x:x+w]
cv2.imwrite('calibresult.png',dst)
#打印我们要求的两个矩阵参数
print ("newcameramtx外参:\n",newcameramtx)
print ("dist畸变值:\n",dist)
print ("newcameramtx旋转(向量)外参:\n",rvecs)
print ("dist平移(向量)外参:\n",tvecs)
#计算误差
tot_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)
tot_error += error
print ("total error: ", tot_error/len(objpoints))
(3)运行结果
图像角点标记:
原始图和校准后图片对比:5test15.jpg
5test11.jpg
5test15.jpg图像newcameramtx外参值:
[[3.44713599e+03 0.00000000e+00 1.53488724e+03]
[0.00000000e+00 3.44758105e+03 1.91966761e+03]
[0.00000000e+00 0.00000000e+00 1.00000000e+00]]
5test11.jpg图像newcameramtx外参值:
[[3.48159399e+03 0.00000000e+00 1.55110268e+03]
[0.00000000e+00 3.43468091e+03 1.90954540e+03]
[0.00000000e+00 0.00000000e+00 1.00000000e+00]]
最后得到的图像外参值是不同的,而相机的畸变值、旋转向量、平移向量及误差值固定不变,相关值如下所得:
type objpoints:(49, 3)---3D点
type imgpoints:(49, 1, 2)--2D点
dist畸变系数:
[[ 0.14037782 -1.12437053 0.0079399 -0.00243523 1.45905723]]
newcameramtx旋转(向量)外参:
[array([[-0.32234848],
[ 0.26572816],
[ 1.52758138]]), array([[-0.46813498],
[ 0.03261515],
[ 0.15319958]]), array([[-0.39931943],
[-0.04397268],
[ 1.12483802]]), array([[0.47019468],
[0.45230609],
[1.44794594]]), array([[-0.22042173],
[ 0.31365638],
[ 1.51963207]]), array([[ 0.06341837],
[-0.00373832],
[-0.00836563]]), array([[-0.44678834],
[ 0.52683227],
[ 0.67690394]]), array([[-0.68885068],
[-0.01674914],
[-0.01721774]]), array([[-0.66844586],
[ 0.20340449],
[ 0.59668953]]), array([[-0.72253264],
[-0.03373615],
[-0.02435481]]), array([[-0.6734064 ],
[ 0.38832428],
[ 0.90096693]]), array([[-0.46454233],
[ 0.48592259],
[ 1.94649499]]), array([[-0.36950452],
[ 0.01955682],
[ 0.3449451 ]]), array([[-0.44658564],
[ 0.12988201],
[ 0.42780643]]), array([[-0.33147772],
[ 0.32575634],
[ 1.51486596]])]
dist平移(向量)外参:
[array([[ 3.08231069],
[-3.08235549],
[15.55771513]]), array([[-2.70972466],
[-3.41129815],
[13.64916759]]), array([[ 2.30083442],
[-2.64989815],
[15.18818568]]), array([[ 5.18267983],
[-4.22422826],
[10.97260124]]), array([[ 2.87535752],
[-3.3461206 ],
[12.30648213]]), array([[-2.91681388],
[-3.6738031 ],
[11.04142145]]), array([[-0.51308951],
[-3.04579684],
[16.21627154]]), array([[-2.85174165],
[-3.55459532],
[14.08710113]]), array([[-0.16378072],
[-2.82538306],
[15.23026436]]), array([[-2.85935585],
[-4.65021659],
[13.1658248 ]]), array([[ 1.24402103],
[-4.02067269],
[16.34951725]]), array([[ 4.80757843],
[-3.71838717],
[14.44073719]]), array([[-1.43053372],
[-3.69408439],
[13.99454139]]), array([[-0.44121578],
[-2.64587509],
[14.63936161]]), array([[ 2.88379131],
[-3.32850245],
[12.82971618]])]
存在误差:total error: 0.8280577027245904
通过反投影误差,我们可以来评估结果的好坏。越接近0,说明结果越理想。通过之前计算的内参数矩阵、畸变系数、旋转矩阵和平移向量,使用cv2.projectPoints()计算三维点到二维图像的投影,然后计算反投影得到的点与图像上检测到的点的误差,最后计算一个对于所有标定图像的平均误差,这个值就是反投影误差。
一般情况下,需要把打印的棋盘纸张平整的放在板子上,拍出来的照片和显示图像及物理点和世界点存在的误差才会很理想;之所以我测试的图像反投影误差很大是因为我的棋盘纸张很扭曲,拍出来的图片会有曲线,矩阵之间的转换就存在很大误差,所以一定要注意棋盘格纸张拍照时要放平整。
问题1.Traceback (most recent call last):
File "E:\Python37_course\test5\mian.py", line 38, in
corners2 = cv2.cornerSubPix(gray,corners,(11,11),(-1,-1),criteria)
cv2.error: OpenCV(3.4.2) C:\projects\opencv-python\opencv\modules\imgproc\src\cornersubpix.cpp:58: error: (-215:Assertion failed) count >= 0 in function 'cv::cornerSubPix'
这是因为我的角点个数超出范围,棋盘格大小为8*8,棋盘格焦点数应该为7*7,而我写成了8*8,所以数组超出以致于报错。
问题2:Traceback (most recent call last):
File "E:\Python37_course\test5\mian.py", line 58, in
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints, gray.shape[::-1],None,None)
cv2.error: OpenCV(3.4.2) C:\projects\opencv-python\opencv\modules\calib3d\src\calibration.cpp:3143: error: (-215:Assertion failed) ni == ni1 in function 'cv::collectCalibrationData'
这是因为objpoints和imgpoints中的条目数必须相同。创建一组8 * 8 = 64个objpoints,用于8x8交叉的棋盘,但你的实际棋盘有7 * 7= 49个交叉点。因此,在处理图像时,会出现objpoints和imgpoints列表会有不同的长度。需要修改objp的创建/初始化,使其具有7 * 7 = 49个点,其坐标对应于棋盘上的实际物理坐标。如果还出现同样的报错,就看一下代码这段:是否有加对ret进行角点判断。
如果按照上面的解决方法去做,是可以正常运行出结果。
下载连接:https://pan.baidu.com/s/18H79umDIr-HxB3hTJprXOQ
提取码:njjx