相机标定是计算机视觉和摄影测量领域中的一个重要概念。
目的:
应用:
在进行相机标定之前,我们需要了解相机的成像模模型。
相机成像模型是描述相机成像过程的数学模型,它可以帮助我们理解相机成像的原理,从而更好地进行相机标定。
搞清相机标定的原理,我们首先需要明确几个坐标系的概念:
像素坐标系
像素坐标就是图像,坐标值就是像素在图像中的位置。一般像素坐标系的左上角的顶点就是远点,水平向右是u,垂直向下是v轴。
例如,在上图中,任意一个像素点的坐标可以表示为(ui,vi)。
图像坐标系
在像素坐标系中,每个像素的坐标是用像素来表示的,然而,像素的表示方法却不能反应图像中物体的物理尺寸,因此,有必要将像素坐标转换为图像坐标。
将像素坐标系的原点平移到图像的中心,就定为图像坐标系的原点,图像坐标系的x轴与像素坐标系的u轴平行,方向相同,而图像坐标系的y轴与像素坐标系的v轴平行,方向相同。
在图中,假设图像中心的像素坐标是(u0,v0),相机中感光器件每个像素的物理尺寸是dx * dy,那么,图像坐标系的坐标(x,y)与像素坐标系的坐标(u,v)之间的关系可以表示为:
写成矩阵的形式就为:
改写为齐次坐标的形式:
相机坐标系是以相机的光轴作为Z轴,光线在相机光学系统的中心位置就是原点Oc(实际上就是透镜的中心),相机坐标系的水平轴Xc与垂直轴Yc分别于图像坐标系的X轴和Y轴平行。
在图中,相机坐标系的原点与图像坐标系的原点之间的距离OcOi之间的距离为f(也就是焦距)。
上图中,如果有一个物体成像到图像坐标系,则可以用下图来表示(B点是相机坐标系中物体的点坐标,P是图像坐标系中成像的坐标):
可以知道相机坐标系与图像坐标系的关系为:
世界坐标系
世界坐标系是图像与真实物体之间的一个映射关系。
如果是单目视觉的话,主要就是真实物体尺寸与图像尺寸的映射关系。
如果是多目视觉的话,那么就需要知道多个相机之间的关系,这个关系就需要在同一个坐标系下进行换算。
在下图中,世界坐标系的原点是Ow,而Xw,Yw,Zw轴并不是与其他坐标系平行的,而是有一定的角度,并且有一定的平移。
当对相机坐标系安装一定的参数,分别绕着X,Y,Z轴做平移和旋转后,就得到在世界坐标系中的坐标。
通过前面的几个步骤,我们已经得到了各个坐标系之间的相互转换关系,进一步的就可以得到从像素坐标系到世界坐标系的变换关系:
公式中,左边二维矩阵就表示相机的外参,可以看到,外参就是相机相对于世界坐标系的旋转和平移变换关系。内参是相机固有的属性,实际上就是焦距,像元尺寸。同时还可以看到,公式中有一个Zc,它表示物体离光学中心的距离。这也就说明,在标定的时候,如果物体在距离相机的不同位置,那么我们就必须在不同的位置对相机做标定。简单点来理解就是,当物体离相机远的时候,在图像上就很小,那么一个像素代表的实际尺寸就大,当物体离相机近的时候,那么成像效果就大,一个像素代表的实际物体尺寸就小。因此,对于每一个位置都需要去标定。
畸变的英文单词是distortion。从英文的意思来看就是物体看起来是不正常的,比如说形状改变了,扭曲了或者其他的变化。
造成图像畸变的原因有很多,总结起来可以分为两类:径向畸变和切向畸变。
径向畸变
可以这样来理解,对于透镜而言,以透镜的中心作为原点,往外是透镜的半径的方向,当光线越靠近中心的位置,畸变越小,沿着半径方向远离中心的时候,畸变越大。典型的径向畸变有桶形畸变和枕形畸变。如下图所示。
径向畸变的矫正公式如下(这里不给出推导过程,直接使用)
式中,(x,y)是理想的无畸变的坐标(图像坐标系),(xdr,ydr)是畸变后图像像素点的坐标,并且r与(x,y)的关系为:
切向畸变
切向畸变可以这样理解,当透镜与成像平面不行时,就产生了畸变,类似于透视变换。打个比方的例子。
切向畸变的矫正公式如下(这里不给出推导过程,直接使用):
畸变矫正
通过上面介绍的径向畸变和切向畸变模型,可以得到两个模型最终作用于真实图像后的矫正模型。
棋盘格标定板:棋盘格标定板是一种具有规则黑白格子的平面板,每个格子的角点都可以作为特征点进行提取。
由于棋盘格的尺寸和格子数量已知,因此可以很容易地获取这些角点在世界坐标系中的三维坐标。
标定算法流程如下所示:
(1). 棋盘格角点检测:使用cv2.findChessboardCorners函数检测棋盘格图案在图像中的角点。这个函数寻找特定大小的棋盘格内角点的位置。
(2). 优化角点像素坐标:对检测到的角点进行优化,使用cv2.cornerSubPix函数对像素级别的角点坐标进行精确化处理。这个步骤提高了检测到的角点的精度。
(3). 收集标定数据:将优化后的角点坐标(二维图像中的坐标)和已知的棋盘格上的三维坐标(世界坐标系中的坐标)对应起来,并保存这些数据,用于后续的相机标定。
(4). 相机标定:使用cv2.calibrateCamera函数根据已知的三维-二维点对来进行相机标定。该函数利用收集到的标定数据,计算出相机的内部参数(如相机矩阵、畸变参数等)和外部参数(相机的位置和朝向)。
(5). 保存相机参数:最后,将计算得到的相机内参、畸变参数、相机位置和朝向等数据,以便后续使用。
整个过程基于相机成像原理和棋盘格的已知几何特征,通过收集标定图像上的棋盘格角点信息,利用这些已知的2D-3D点对,计算出相机的内部和外部参数,从而实现相机的标定。
首先,使用棋盘格对准卓越之星的摄像头,保存多个图像,保存在checkboard文件夹下,注意,需要不同距离和不同角度的图像。注意,函数提取的是内角点,也就是不包含标定板最外层的四条边上的角点。
可以使用下面的代码,手持打印好的标定板在摄像头前变换位姿,在检测到全部棋盘格内角点后进行保存。
import cv2
import numpy as np
import glob
def find_corners(img, chess_col, chess_row, sav_path, is_save=False):
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, corners = cv2.findChessboardCorners(gray, (chess_col, chess_row), None)
# 终止条件
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
corners2 = cv2.cornerSubPix(gray, corners, (5, 5), (-1, -1), criteria)
if ret == True:
cv2.drawChessboardCorners(img, (chess_col, chess_row), corners2, ret)
if is_save is True:
cv2.imwrite(sav_path, img)
return ret, corners2
当我们保存了一定数量的棋盘格图片后,就可以基于这些图片数据进行标定。
以下为基于python的标定代码:
import glob # 用于查找匹配指定模式的所有路径名
import cv2 # OpenCV库,用于计算机视觉任务
import pickle # 用于序列化和反序列化Python对象结构
import numpy as np # NumPy库,用于进行高效的数值计算
# 定义棋盘格的维度,这里设定为6行8列,根据打印出的角点行列填写
CHECKERBOARD = (6, 8)
# 设置角点搜索的停止条件和优化准则
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
# 初始化存储3D世界坐标点和2D图像坐标点的列表
objpoints = [] #真实世界的3d点
imgpoints = [] #图像中的2d点
# 定义世界坐标系中的3D点,这些点对应于棋盘格的每个角点
#创建了一个形状为(1, CHECKERBOARD[0] * CHECKERBOARD[1], 3)的三维数组
#并且该数组的所有元素都被初始化为0
objp = np.zeros((1, CHECKERBOARD[0] * CHECKERBOARD[1], 3), np.float32)
# 使用mgrid生成网格坐标,并将其整形为2列的形式,然后赋值给objp的前两列
objp[0, :, :2] = np.mgrid[0:CHECKERBOARD[0], 0:CHECKERBOARD[1]].T.reshape(-1, 2)
# 使用glob查找指定目录下的所有.jpg文件 ,这里面的jpg是我们之前保存的图片
images = glob.glob('./checkboard/*.jpg')
# 遍历每个图像文件
for fname in images:
# 读取图像
img = cv2.imread(fname)
# 将图像转换为灰度图
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 在灰度图中查找棋盘格的角点
ret, corners = cv2.findChessboardCorners(gray, CHECKERBOARD,
cv2.CALIB_CB_ADAPTIVE_THRESH + cv2.CALIB_CB_FAST_CHECK + cv2.CALIB_CB_NORMALIZE_IMAGE)
# 如果找到了角点
if ret == True:
# 将3D世界坐标点添加到objpoints列表中
objpoints.append(objp)
# 细化角点的位置
corners2 = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria)
# 将细化后的2D图像坐标点添加到imgpoints列表中
imgpoints.append(corners2)
# 在图像上画出角点,并显示图像
img = cv2.drawChessboardCorners(img, CHECKERBOARD, corners2, ret)
# 设置图像显示宽度和高度
desired_width = 640
desired_height = 480
# 调整图像大小
img_resized = cv2.resize(img, (desired_width, desired_height))
# 显示调整大小后的图像
cv2.imshow('Visualize IMG', img_resized)
# 等待100ms,以便用户能够查看图像
cv2.waitKey(100)
# 关闭所有OpenCV窗口
cv2.destroyAllWindows()
h, w = img.shape[:2] #获取图像的宽和高
# 使用找到的角点进行相机标定,获取相机内参、畸变系数等参数
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints, gray.shape[::-1], None, None)
# 打印标定得到的相机参数
print("Camera matrix : \n")
print(mtx)
print("dist : \n")
print(dist)
print("rvecs : \n")
print(rvecs)
print("tvecs : \n")
print(tvecs)
def correction(img, matrix, dist):
(h1, w1) = img.shape[:2]
# 对参数做处理,使得最后的输出的矫正图像去表不必要的边缘。
newcameramtx, roi = cv2.getOptimalNewCameraMatrix(matrix, dist, (w1, h1), 1, (w1, h1))
# 矫正
dst = cv2.undistort(img, matrix, dist, None, newcameramtx)
# 保存矫正图像
x, y, w, h = roi
dst = dst[y:y + h, x:x + w]
return dst
可以使用如下函数计算重投影误差:
# 定义一个函数,用于计算相机标定后的平均误差
def mean_error(obj_pts, img_pts, matrix, dist, r_vecs, t_vecs):
# 初始化平均误差为0
mean_error = 0
# 遍历所有的世界坐标点和对应的图像坐标点
for i in range(len(obj_pts)):
# 使用OpenCV的projectPoints函数,根据相机内参、畸变参数、旋转向量和平移向量,
# 将世界坐标点重新投影到图像坐标上,得到新的图像坐标点img_pts2
img_pts2, _ = cv2.projectPoints(obj_pts[i], r_vecs[i], t_vecs[i], matrix, dist)
# 计算原始图像坐标点和重新投影后的图像坐标点之间的L2范数误差,
# 并将其平均到每个点上(通过除以点的数量)
error = cv2.norm(img_pts[i], img_pts2, cv2.NORM_L2) / len(img_pts2)
# 将每个点的平均误差累加到总平均误差上
mean_error += error
# 计算所有点的平均误差,通过除以点的组数(即标定图像的数量)
return mean_error / len(obj_pts)
这个函数的主要目的是评估相机标定的准确性。它通过比较原始的图像坐标点和根据标定参数重新投影后的图像坐标点之间的差异来实现这一点。差异越小,说明标定参数越准确。函数返回的是所有标定图像的平均误差,这个值越小表示标定结果越精确。
使用同一个相机再拍摄新的图像验证重投影误差:
def main(fx_val:float, fy_val:float):
# ###################
chess_path = r'./images/'
test_img_path = r"./test/test4.jpg"
# ###################
ret, matrix, dist, r_vecs, t_vecs, obj_pts, img_pts = calibration(chess_path, 8, 6, fx_val,fy_val)
save_para(ret, matrix, dist, r_vecs, t_vecs)
test_img = cv2.imread(test_img_path)
test_img = cv2.resize(test_img, None, fx=fx_val, fy=fy_val)
correction_test_img = correction(test_img, matrix, dist)
cv2.imshow("test_img", test_img)
cv2.imshow("correction_test_img", correction_test_img)
m_error = mean_error(obj_pts, img_pts, matrix, dist, r_vecs, t_vecs)
print("重投影误差:", m_error)
cv2.waitKey(0)
if __name__ == '__main__':
main(0.7,0.7)