在图像测量过程以及机器视觉应用中,为确定空间物体表面某点的三维几何位置与其在图像中对应点之间的相互关系,必须建立相机成像的几何模型,这些几何模型参数就是相机参数。在大多数条件下这些参数必须通过实验与计算才能得到,这个求解参数(内参、外参、畸变参数)的过程就称之为相机标定(或摄像机标定)。
下面我们讲述的张正友的棋盘标定方法
常规的标定方法需要昂贵的设备,并且需要用到两个或这三个平面标定的方法。而张正友教授提出的单平面棋盘格的摄像机标定方法克服了传统标定法需要的高精度标定物的缺点,仅需使用一个打印出来的棋盘格就可以,只需要相机从很少的位置观测平面,相机和平面都可以以未知速度自由移动,提高了精度,便于操作。
目录
相机标定:
1、实现步骤
2、原理
3、实现
- 打印一张棋盘格A4纸张(黑白间距已知),并贴在一个平板上
- 针对棋盘格拍摄若干张图片(一般10-20张)
- 在图片中检测特征点(Harris特征)
- 利用解析解估算方法计算出5个内部参数,以及6个外部参数
- 根据极大似然估计策略,设计优化目标并实现参数的refinement
了解坐标系:世界坐标系 、相机坐标系 、图像坐标系 、像素坐标系
(1)计算外参:
s: 世界坐标系到图像坐标系的尺度因子
A: 相机内参矩阵
: 像主点坐标
α, β: 焦距与像素横纵比的融合
γ: 径向畸变参数
有:
所以描述空间坐标到图像坐标的映射(标定用的棋盘格平面到图像平面的单应性关系):
不妨设棋盘格位于Z = 0 定义旋转矩阵R的第i列为, 则有:
于是空间到图像的映射可改为:
其中H 是描述单应性矩阵,可通过最小二乘法,从角点世界坐标到图像坐标的关系求解
该单应性矩阵有8个自由度
(2)计算内参
设H中的第i列为
根据b的定义,可以推导出如下公式
可以推导出
可以通过b求解内参
(3)最大似然估计:
给定 n 张棋盘格图像,每张图像有 m 个角点最小化下述公式等同于极大似然估计
上述非线性优化问题可以利用Levenberg-Marquardt 算法求解
(1)数据采集(以下照片均用vivoX6拍摄,后置1300万像素):
(2)角点范围:
角点范围选择在内部的角点,如下图,计算角点个数,可得出角点范围是(10,7)
代码实现:
# 获取标定板角点的位置
objp = np.zeros((7 * 10, 3), np.float32)
objp[:, :2] = np.mgrid[0:10, 0:7].T.reshape(-1, 2) # 将世界坐标系建在标定板上,所有点的Z坐标全部为0,所以只需要赋值x和y
(3)检测角点,如下图所示(可以看到一点一点的其他颜色的点线,即角点所在)
代码实现:
images = glob.glob("E:/study_work/python/chass/chass*.jpg") # 数据集位置
for fname in images:
img = cv2.imread(fname)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
size = gray.shape[::-1]
ret, corners = cv2.findChessboardCorners(gray, (10, 7), None) # FindChessboardCorners可以用来寻找棋盘图的内角点位置。
# print(corners) # 输出角点位置
if ret:
obj_points.append(objp)
corners2 = cv2.cornerSubPix(gray, corners, (5, 5), (-1, -1), criteria) # 在原角点的基础上寻找亚像素角点
#print(corners2)
if [corners2]:
img_points.append(corners2)
else:
img_points.append(corners)
cv2.drawChessboardCorners(img, (10, 7), corners, ret) # 记住,OpenCV的绘制函数一般无返回值
cv2.namedWindow('img', cv2.WINDOW_NORMAL)
cv2.resizeWindow('img', 300, 300)
cv2.imshow('img', img)
cv2.waitKey(1550)
print(len(img_points)) # 数据集数
cv2.destroyAllWindows()
(4)标定
calibrateCamera(obj_points, img_points, size, None, None)函数的参数:
输入:
object_points :世界坐标系中的点。
image_points :其对应的图像点。
size :图像的大小,在计算相机的内参数和畸变矩阵需要用到;
输出:
mtx :内参数矩阵。
dist :畸变矩阵。
rvecs :旋转向量。
tvecs :位移向量。
# 标定
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(obj_points, img_points, size, None, None)
print("ret:", ret) # 重投影误差
print("mtx:", mtx) # 内参数矩阵
print("dist:", dist) # 畸变系数 distortion cofficients = (k_1,k_2,p_1,p_2,k_3)
print("rvecs:", rvecs) # 旋转向量 # 外参数
print("tvecs:", tvecs) # 平移向量 # 外参数
最终获得结果:
('ret:', 6.387358221084299)
('mtx:', array([[3.55408013e+03, 0.00000000e+00, 1.68053708e+03],
[0.00000000e+00, 3.55723861e+03, 1.91776549e+03],
[0.00000000e+00, 0.00000000e+00, 1.00000000e+00]]))
('dist:', array([[ 0.01910032, -0.19905562, -0.00462884, 0.00202598, 0.10239582]]))
('rvecs:', [array([[ 0.1274326 ],
[-0.29783355],
[-1.5991523 ]]), array([[-0.13641865],
[-0.319848 ],
[-1.72116027]]), array([[-0.09864324],
[ 0.28665719],
[ 1.63265021]]), array([[-0.05830874],
[ 0.31117782],
[ 1.94392567]]), array([[-0.10453709],
[ 0.24194198],
[ 1.29262607]]), array([[-0.13191112],
[-0.13485868],
[-0.02647893]]), array([[-0.02911514],
[-0.17164865],
[-0.03323363]]), array([[-0.03534615],
[-0.29120072],
[-1.46631705]]), array([[-0.26969463],
[-0.20763061],
[-2.90533348]]), array([[-0.20751206],
[ 0.25434448],
[ 3.06432357]]), array([[-0.12517833],
[ 0.20881084],
[ 1.18000834]]), array([[ 0.10895448],
[ 0.2964589 ],
[-1.57296904]]), array([[-0.13316889],
[ 0.22665759],
[ 1.21687107]]), array([[-0.08467883],
[ 0.25280433],
[ 1.28381419]]), array([[-0.04860953],
[ 0.19417008],
[ 1.30282838]]), array([[-0.12652901],
[-0.1759088 ],
[-1.21537181]]), array([[ 0.25988458],
[-0.03280638],
[-1.59038273]]), array([[ 0.26140813],
[-0.03432914],
[-1.59950426]]), array([[ 0.2122981 ],
[-0.1024351 ],
[-1.55314373]]), array([[ 0.12081663],
[-0.29557693],
[-1.59939464]]), array([[-0.26918703],
[ 0.33299711],
[ 2.99942314]]), array([[-0.17057648],
[ 0.09603281],
[ 3.06626992]]), array([[-0.10991202],
[ 0.07735781],
[ 3.08955594]]), array([[-0.29859171],
[ 0.04110083],
[ 3.11495828]]), array([[-0.28996965],
[-0.12607658],
[-3.09220506]]), array([[-0.3495925 ],
[ 0.15750843],
[-2.82968449]]), array([[-0.2425881 ],
[-0.32746475],
[-1.88870249]])])
('tvecs:\n', [array([[-3.64148114],
[ 3.89988798],
[13.35930667]]), array([[-2.35233325],
[ 4.63768667],
[11.57437973]]), array([[ 3.14499234],
[-4.52236594],
[12.72175028]]), array([[ 4.401942 ],
[-3.11075935],
[14.609587 ]]), array([[ 0.98267759],
[-5.01043614],
[14.76642512]]), array([[-4.83492942],
[-3.35578719],
[13.68891821]]), array([[-5.1685001 ],
[-4.22242649],
[14.51493466]]), array([[-4.19250475],
[ 4.51184171],
[15.13310748]]), array([[ 2.73752879],
[ 4.21140176],
[16.87581278]]), array([[ 4.71888839],
[ 4.76778641],
[15.2967357 ]]), array([[ 0.18967263],
[-5.93830845],
[14.74401309]]), array([[-4.94812704],
[ 5.64844296],
[16.5677126 ]]), array([[ 0.55945721],
[-5.72501885],
[15.24965403]]), array([[ 1.76204064],
[-5.67136694],
[17.3277721 ]]), array([[ 1.09610056],
[-6.51158308],
[16.25478718]]), array([[-5.80074055],
[ 2.93641549],
[15.6066899 ]]), array([[-3.47911177],
[ 6.27050138],
[13.71652186]]), array([[-3.42746643],
[ 6.25465603],
[13.70600233]]), array([[-3.86800164],
[ 4.75846406],
[13.71044708]]), array([[-3.61677224],
[ 3.89065227],
[13.43165293]]), array([[ 7.28324882],
[ 0.21999095],
[17.12910998]]), array([[ 6.88188799],
[ 1.56417541],
[16.13166934]]), array([[ 6.43697601],
[ 1.22207914],
[12.75589536]]), array([[ 7.74446697],
[ 1.66656285],
[12.97462042]]), array([[ 4.66957692],
[ 2.38972723],
[10.52249476]]), array([[ 3.62404253],
[ 2.8215497 ],
[14.02929816]]), array([[-1.87281046],
[ 5.50095845],
[13.03559769]])])