【自动驾驶多传感器融合系列】单目相机标定原理及实操
文前白话
- 相机成像在实际应用中存在误差,误差的来源主要是:
1、传感器 / 敏感元件(sensor)制造产生的误差(如,sensor成像单元非正方形,歪斜)
2、镜头制造和相机安装过程中产生的误差(镜头一般存在非线性的径向畸变,来源于透镜的形状(相机坐标系转化到图像坐标系过程),如,广角透镜、鱼眼透镜;若镜头与相机的sensor元件组装不平行,还会产生切向畸变)
-
为了确定空间物体表面某点的三维几何位置与其在相机生成的图像中对应点之间的相互关系,必须建立相机成像的几何模型,这些几何模型参数就是相机参数。
这些参数需要通过实验与计算得到,求解参数的过程就称之为相机标定。标定结果的精度及算法的稳定性会直接影响相机工作成像的准确性。 -
相机标定的方法:
1、自标定:找图像中特征点
2、标定板标定: 特征点易求,稳定性好,常采用的方法 -
相机标定的类型,按照相机是否静止,可分为:
-
静态相机标定(标定板动,相机静止)
-
动态相机标定(标定板静止,相机运动)
相机标定原理
& 四种坐标系
-
相机标定过程中涉及的坐标系类型:世界坐标系,相机坐标系,图像坐标系,像素坐标系。
-
世界坐标系(xw,yw,zw):摄像机与被摄物体可以放置在环境中任意位置,这样就需要在环境中建立一个坐标系,来表示摄像机和被摄物体的位置,这个坐标系就成为世界坐标系。世界坐标系可以任意选择,为假想坐标系,在被指定后随即 不变且唯一,即为绝对坐标系
为什么需要世界坐标系:
将不同视点/视角拍摄的图像信息整合在一起就必须将所有的信息放在同一个坐标系下,这个坐标系应与各张图像的 相机/物体/像素 这些相对坐标系无关,在确定后应不变且唯一,即应为绝对坐标系,即是所说的 世界坐标系 。
- 在单目相机中,通常选择拍摄第一张图像时的相机坐标系作为世界坐标系,即以拍摄第一张图像时相机的光心(小孔)作为原点,X轴为水平方向,Y轴为竖直方向,Z轴指向拍摄第一张图像时相机所观察的方向。选定后世界坐标系便不再发生变化,即不变且唯一。
- 在双目相机(A,B)中,与单目相机大同小异,可选取其中一个相机A拍摄第一张图像时的相机坐标系为世界坐标系,即以相机A拍摄第一张图像时相机的光心(小孔)作为原点,X轴为水平方向,Y轴为竖直方向,Z轴指向拍摄第一张图像时相机A所观察的方向。
-
相机坐标系(xc,yc,zc):以相机的光心(小孔)作为原点,X轴为水平方向,Y轴为竖直方向,Z轴指向相机所观察的方向(即是与成像平面垂直)形成的三维直角坐标系,其随相机的移动而变化,即为相对坐标系。
-
图像坐标系(x,y):也叫平面坐标系,为引出像素坐标系而过渡引入,原点为透镜光轴与成像平面的交点,X轴与Y轴分别平行于相机坐标系的Xc与Yc轴,是二维平面直角坐标系,单位为毫米mm,其依托于相机坐标系,为相对坐标系。
- 像素坐标系(u,v):以像素为单位,坐标原点在左上角。固定在图像上的以像素为单位的平面直角坐标系,其原点位于图像左上角,X轴与Y轴平行于图像坐标系的X和Y轴,其依托于图像坐标系,同样是相对坐标系。
- 需要知道:平面坐标系到像素坐标系系涉及到的是单位的转换和平移,即是传感器以mm单位到像素单位之间有转换关系。如,CCD传感器上面的8mm x 6mm,转换到像素大小是640x480。dx,dy表示像素坐标系中每个像素的物理大小。
& 坐标系转换
-
坐标系转换就是为了将空间的三维世界坐标系转换至图像处理的二维像素坐标系。
-
1、世界坐标系->相机坐标系
世界坐标系是以物体的中心作为原点,相机坐标系是以相机位置作为原点,如果将一个相机放在我们认为的世界坐标系的原点上,此时,相机坐标==世界坐标,但是实际这样的情况几乎很少发生。
-
实际情况下相机与世界坐标原点不一定重合,不管相机与世界坐标系原点距离如何,存在一个平移向量,使用 T表示;同时相机 和物体也不一定是水平对齐的姿态,所以坐标转换还包括以x,y,z轴为轴的旋转角度,用R来表示。
-
t 来表示平移, 任意一个平移包括 (x,y,z)三个方向;
-
表示 R 旋转向量时,如果绕着 某一个轴 进行旋转,如沿着z轴旋转,那么其实三维坐标点的z值是不会变的,唯一改变的是x和y方向的值。
通过图上的位置关系,由三角形相似可以得到任意一点的坐标转换关系,其过程为:
- 则,综合旋转与平移,世界坐标系和相机坐标系的转换可以表示为:
-
引入齐次坐标,既能够用来明确区分向量和点,同时也更易用于进行仿射几何变换。齐次坐标的左右是用(N+1)维来代表 N 维坐标。
-
若将三维坐标视为一个列向量,那么矩阵*列向量得到的新向量的每一个分量,都是旧的列向量的线性函数,因而三维笛卡尔坐标与矩阵的乘法只能实现三维坐标的缩放和旋转,而无法实现坐标平移。
-
将三维的笛卡尔坐标添加一个额外坐标,就可以实现坐标平移了,而且保持了三维向量与矩阵乘法具有的缩放和旋转操作。这个就称为齐次坐标。而这种变换也称为仿射变换(affine transformation),不属于线性变换。
-
仿射变换是:“线性变换”+“平移”。
-
2、相机坐标系->图像(平面)坐标系
两个不同的相机,即使在所处的位置一样的情况下,拍摄的两张照片,很大概率是不一样的。主要是相机自身的相关的参数,需要解相机小孔成像的原理。
- 相机坐标系中的点P(x_c,y_c,z_c ) ,映射到图像坐标系下的点为P(x, y ), 由 相似三角形几何关系,有:
- 3、图像坐标系->像素坐标系
- [u0,v0] 可以理解为,从图像坐标系中心点到像素坐标系中心点的一个偏移量,属于相机内参的一部分。已知图像坐标系的一个点 P(x, y ),还应该知道,横坐标的每一个mm对应像素是多少,以进行转换。
- 图像坐标系的单位为mm,像素坐标系的单位为像素pt,所以需要进行单位的转换。
- 假设每个像素代表的物理尺寸为dx,dy,则有:
- 由上述 相机坐标系与图像坐标系的关系,可以得到像素坐标系和相机坐标系的关系式:
- 为了简化,将方程进行整理:
相机畸变与畸变矫正
- 在几何光学和阴极射线管(CRT)显示中,畸变(distortion) 是对直线投影(rectilinear projection)的一种偏移。直线投影是场景内的一条直线投影到图片上也保持为一条直线,而畸变简单来说就是一条直线投影到图片上不能保持为一条直线了,这是一种光学畸变(optical aberration)。
- 畸变一般可以分为两大类,包括径向畸变和切向畸变。主要的是径向畸变有时也会有轻微的切向畸变。
径向畸变
- 径向畸变来自于透镜形状;
- 光线在远离透镜中心的地方比靠近中心的地方更加弯曲。对于常用的普通透镜来说,这种现象更加严重。筒形畸变在价格低的网络摄像机中非常明显,但在高端摄像机中不明显,因为这些透镜系统做了很多消除径向畸变的工作。
切向畸变
- 切向畸变来自于整个摄像机的组装过程。
畸变矫正
-
针孔相机模型中,只要确定相机参数和畸变参数就可以唯一的确定针孔相机模型, 这个过程就称为「相机标定」。
-
对单目视觉而言,求得内参和畸变参数后,就可以对拍摄的图像做变换和矫正。矫正完拍摄的图像之后,对图像做其他任务处理。
-
对于双目视觉而言,需要用到世界坐标系。对单目视觉做完内参和畸变参数的矫正之后,就可以用这些变换后的图像,同时结合世界坐标系实现定位或者其他用途了。
相机标定方法分类
- 自标定法
- 标定板标定法
单目相机标定的实操步骤
- 在像素坐标系到世界坐标系关系公式中:
- 在标定的时候,如果物体在距离相机的不同位置,那么就必须在不同的位置对相机做标定。当物体离相机远的时候,在相机上成像小,一个像素代表的实际尺寸就大,当物体离相机近的时候,那么成像大,一个像素代表的实际物体尺寸就小。因此,不同的位置都需要去标定。
① 标定板图片
-
图示:
-
标定板图片PDF& 本次标定使用的棋盘格图片19张:
-
分享链接:https://pan.baidu.com/s/1P4jl30-EK_nch_-et0-9XA .
提取码:xqq4 -
说明:
标定图片需要使用标定板在不同位置、不同角度、不同姿态下拍摄的多张图片进行标定,10~20张左右。 -
为什么不用一张?
根据张正友标定法,只需要求得B矩阵和H矩阵就可以换算出参数矩阵。而B矩阵和H矩阵的求解是通过最大似然估计来优化得到的。
- 优化过程必然是需要多幅图像共同作用的。
- 标定的过程,可以理解为一个矩阵关系的转换过程。
- 根据实际的标定板形状,在计算机中假定了一个“ 标准的 ”假想标定板图像,而标定的过程就是,所有拍摄到的图像中的标定板,都应该往计算机中假定的标定板靠拢。如果不能完全对比上,那么就有系数上的关系,这个系数上的关系就是要标定的参数。
② 提取每张标定图片的角点信息
- 图示:
- 代码实现:
# 使用FindChessboardCorners()函数提取角点信息
ret, corners = cv2.findChessboardCorners(gray,(col,row), None)
③ 进一步提取每张标定图的亚像素角点信息
- 使用cornerSubPix函数在角点检测中精确化角点位置
#为了得到稍微精确一点的角点坐标,进一步对角点进行亚像素寻找;
corners2 = cv2.cornerSubPix(gray, corners, (5, 5), (-1, -1), (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_COUNT, 10, 0.001))
④ 在标定图片上绘制内角点
- 代码:
# 使用 drawChessboardCorners函数用于绘制被成功标定的内角点
cv2.drawChessboardCorners(img, (col,row), corners2, ret)
cv2.imwrite(save_path, img)
print("保存找到角点的图像地址为: ",save_path)
- 单张角点效果:
⑤ 进行标定,获取相机参数
# 相机标定过程
def cam_calib_calibrate(img_dir, rlt_dir1, col, row, img_num):
w = 0
h = 0
all_corners = []
patterns = []
#标定相机,设定一个“理想标准”的标定板,标定就是把实际拍摄图像中的板子往“理想标准”板子上靠拢,靠拢的过程就是计算参数的过程
x,y = np.meshgrid(range(col),range(row))
prod = row * col
pattern_points=np.hstack((x.reshape(prod,1),y.reshape(prod,1),np.zeros((prod,1)))).astype(np.float32)
for i in range(1,img_num+1):
img_path = img_dir + "\\" + str(i) + ".jpg"
print (img_path)
#读取图像
img = cv2.imread(img_path)
(h, w) = img.shape[:2]
#提取角点
ret, corners = cam_calib_find_corners(img, rlt_dir1, i, col, row)
#合并所有角点
all_corners.append(corners)
patterns.append(pattern_points)
# 获取到棋盘标定图的内角点图像坐标之后,使用calibrateCamera()函数进行标定,计算相机内参和外参系数
rms, cameraMatrix, distCoeffs, rvecs, tvecs = cv2.calibrateCamera(patterns, all_corners, (w, h), None, None)
print("rms",rms) # 残差rms,表示得到参数后,经过校准实际点和投影点位置的差异
print("cameraMatrix",cameraMatrix) # cameraMatrix为内参数矩阵
print("distCoeffs",distCoeffs) # distCoeffs为畸变矩阵
print("rvecs",rvecs) # rvecs为旋转向量
print("tvecs",tvecs) # tvecs为位移向量
return (cameraMatrix, distCoeffs)
- 获取的参数:
⑥ 通过标定结果对测试图进行矫正
#对参数做处理,使得最后的输出的矫正图像去表不必要的边缘。
newcameramtx,roi = cv2.getOptimalNewCameraMatrix(cameraMatrix,distCoeffs,(w1,h1),1,(w1,h1))
#对测试图像进行矫正
dst = cv2.undistort(img, cameraMatrix, distCoeffs, None, newcameramtx)
附: 完整相机标定代码(详细注释)
- python+opencv
import argparse
from argparse import RawTextHelpFormatter
import numpy as np
import os
import cv2
import yaml
# 对每一张标定图片,提取角点信息
def cam_calib_find_corners(img, rlt_dir1, img_idx, col, row):
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 使用FindChessboardCorners()函数提取角点信息
ret, corners = cv2.findChessboardCorners(gray,(col,row), None)
#为了得到稍微精确一点的角点坐标,进一步对角点进行亚像素寻找;# 使用cornerSubPix函数在角点检测中精确化角点位置
corners2 = cv2.cornerSubPix(gray, corners, (5, 5), (-1, -1), (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_COUNT, 10, 0.001))
if ret == True:
#保存角点图像
# res_find_corners_img_path = img_dir + "\\res\\"
save_path = rlt_dir1 + "\\" + str(img_idx) + "_corner.jpg"
if not os.path.exists(rlt_dir1):
os.mkdir(rlt_dir1)
print("rlt_dir1 directory made")
# 使用 drawChessboardCorners函数用于绘制被成功标定的内角点
cv2.drawChessboardCorners(img, (col,row), corners2, ret)
cv2.imwrite(save_path, img)
print("保存找到角点的图像地址为: ",save_path)
return (ret, corners2)
# 相机标定过程
def cam_calib_calibrate(img_dir, rlt_dir1, col, row, img_num):
w = 0
h = 0
all_corners = []
patterns = []
#标定相机,设定一个“理想标准”的标定板,标定就是把实际拍摄图像中的板子往“理想标准”板子上靠拢,靠拢的过程就是计算参数的过程
x,y = np.meshgrid(range(col),range(row))
prod = row * col
pattern_points=np.hstack((x.reshape(prod,1),y.reshape(prod,1),np.zeros((prod,1)))).astype(np.float32)
for i in range(1,img_num+1):
img_path = img_dir + "\\" + str(i) + ".jpg"
print (img_path)
#读取图像
img = cv2.imread(img_path)
(h, w) = img.shape[:2]
#提取角点
ret, corners = cam_calib_find_corners(img, rlt_dir1, i, col, row)
#合并所有角点
all_corners.append(corners)
patterns.append(pattern_points)
# 获取到棋盘标定图的内角点图像坐标之后,使用calibrateCamera()函数进行标定,计算相机内参和外参系数
rms, cameraMatrix, distCoeffs, rvecs, tvecs = cv2.calibrateCamera(patterns, all_corners, (w, h), None, None)
print("rms",rms) # 残差rms,表示得到参数后,经过校准实际点和投影点位置的差异
print("cameraMatrix",cameraMatrix) # cameraMatrix为内参数矩阵
print("distCoeffs",distCoeffs) # distCoeffs为畸变矩阵
print("rvecs",rvecs) # rvecs为旋转向量
print("tvecs",tvecs) # tvecs为位移向量
return (cameraMatrix, distCoeffs)
#write the matrix to yaml
mtx=cameraMatrix.tolist()
dist=distCoeffs.tolist()
data={"camera_matrix":mtx,"dist_coeff":dist}
with open("parameter.yaml","w") as file:
yaml.dump(data,file)
def cam_calib_correct_img(crct_img_dir, cameraMatrix, distCoeffs):
for i in range(1,3):
crct_img_path = crct_img_dir + "\\" + str(i) + ".jpg"
img = cv2.imread(crct_img_path)
(h1, w1) = img.shape[:2]
#对参数做处理,使得最后的输出的矫正图像去表不必要的边缘。
newcameramtx,roi = cv2.getOptimalNewCameraMatrix(cameraMatrix,distCoeffs,(w1,h1),1,(w1,h1))
#矫正
dst = cv2.undistort(img, cameraMatrix, distCoeffs, None, newcameramtx)
# 保存矫正图像
x,y,w,h = roi
dst = dst[y:y+h, x:x+w]
res_path = crct_img_dir + "\\rlt\\"
rlt_path2 = res_path + str(i) + "_crct.jpg"
if not os.path.exists(res_path):
os.mkdir(res_path)
print("res_path directory made")
# elif not os.path.exists(rlt_path2):
# os.mkdir(rlt_path2)
else:
print("res_path directory existed")
cv2.imwrite(rlt_path2, dst)
print()
print("保存校正后的图像,地址:",rlt_path2)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="读取标定的图片并保存结果",formatter_class=RawTextHelpFormatter)
parser.add_argument("--img_dir",help="输入的标定图片路径",type=str,metavar='', default="E:\\002_GIT\\cam_calibration\\camer2\\calib_img")
parser.add_argument("--rlt_dir1",help="保存绘画角点后的标定图路径",type=str,metavar='',default="E:\\002_GIT\\cam_calibration\\camer2\\calib_img\\rlt")
parser.add_argument("--crct_img_dir",help="待矫正待测试图像路径",type=str,metavar='',default="E:\\002_GIT\\cam_calibration\\camer2\\crct_img")
parser.add_argument("--rlt_dir2",help="矫正后图像路径",type=str,metavar='',default="E:\\002_GIT\\cam_calibration\\camer2\\crct_img\\rlt")
parser.add_argument("--row_num",help="每一行有多少个角点,边缘处的不算",type=int,metavar='',default="9")
parser.add_argument("--col_num",help="每一列有多少个角点,边缘处的不算",type=int,metavar='',default="6")
parser.add_argument("--img_num",help="多少幅图像",type=int,metavar='',default="19")
args=parser.parse_args()
# 标定相机
cameraMatrix, distCoeffs = cam_calib_calibrate(args.img_dir, args.rlt_dir1, args.row_num, args.col_num, args.img_num)
#矫正图片
cam_calib_correct_img(args.crct_img_dir, cameraMatrix, distCoeffs)
- 分享相机标定内参的另外一种代码:
import numpy as np
import os
import cv2
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
import pickle
def calibrate_camera():
# 棋盘格个数及其横纵内角点个数
objp_dict = {
1: (9, 6),
2: (9, 6),
3: (9, 6),
4: (9, 6),
5: (9, 6),
6: (9, 6),
7: (9, 6),
8: (9, 6),
9: (9, 6),
10: (9, 6),
11: (9, 6),
12: (9, 6),
13: (9, 6),
14: (9, 6),
15: (9, 6),
16: (9, 6),
17: (9, 6),
18: (9, 6),
19: (9, 6),
20: (9, 6),
}
objp_list = []
corners_list = []
for k in objp_dict:
nx, ny = objp_dict[k]
objp = np.zeros((nx*ny,3), np.float32)
objp[:,:2] = np.mgrid[0:nx, 0:ny].T.reshape(-1, 2)
fname = 'mark2/%s.jpg' % str(k)
img = cv2.imread(fname)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 棋盘格角点
ret, corners = cv2.findChessboardCorners(gray, (nx, ny), None)
if ret == True:
objp_list.append(objp)
corners_list.append(corners)
else:
print('Warning: ret = %s for %s' % (ret, fname))
# 相机标定
img = cv2.imread('mark2/1.jpg') # 储存选取好的标定板照片的文件夹路径,并选定到第一张
img_size = (img.shape[1], img.shape[0])
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(objp_list, corners_list, img_size,None,None)
return mtx, dist
if __name__ == '__main__':
mtx, dist = calibrate_camera()
# 采用Matlab相机标定工具包相机标定所得内参矩阵,畸变系数
# mtx = np.array([[1.5596e+03, 0, 1.2790e+03], [0, 1.5652e+03, 1.0674e+03], [0, 0, 1]])
# dist = np.array([[-0.3571, 0.2105, 5.4513e-04, 5.9984e-04, 0]])
save_dict = {'mtx': mtx, 'dist': dist}
print(mtx, dist)
np.savez('calibrate_camera\\In_mtx2', mtx=mtx, dist=dist)
#读取畸变图片
img = mpimg.imread('mark2/1.jpg') # 储存选取好的标定板照片的文件夹路径,并选定到第一张
dst = cv2.undistort(img, mtx, dist, None, mtx)
plt.imshow(dst)
plt.savefig('undistort_calibration.jpg')
- 生成的 undistort_calibration.jpg 结果:
参考知识渊博大佬的文章
参考文章 1: 相机模型中的世界坐标系究竟指什么?.
参考文章 2: 世界坐标系,相机坐标系,图像坐标系,像素坐标系的转换.
参考文章 3: 如何通俗地讲解「仿射变换」这个概念?.
参考文章 4: 深入探索透视投影变换.
参考文章 5: 相机参数标定(camera calibration)及标定结果如何使用.
参考文章 6: 单目相机标定实践(完整过程).
参考文章 7: 相机标定(Camera calibration).
参考文章 8: 相机标定(理论推导+具体实现).