相机标定方法及实现
相机标定
标定方法
标定板
rowNum = 9
colNum = 9
DPI = 96 # dot per inch
inch2cm = 2.54
K= int(blockSize / inch2cm * DPI)
img = np.zeros((rowNum *K, colNum *K, 3), "uint8")
for i in range(rowNum):
for j in range(colNum):
if (i+j) % 2 != 0:
img[i*K:i*K+K, j*K:j*K+K] = 255
开源标定
- OpenCV标定
- Halcon
- Matlab Calibration Toolbox标定工具箱
OpenCV标定
- 主要以张正友标定法来实现
- 获取相机内参
f
x
,
f
y
,
u
0
,
v
0
,
k
1
,
k
2
,
k
3
,
p
1
,
p
2
f_x, f_y, u_0, v_0, k_1, k_2, k_3, p_1, p_2
fx?,fy?,u0?,v0?,k1?,k2?,k3?,p1?,p2?
- 得到相机外参,每张标定图片对应一组外参
单目标定
-
准备标定板(平整,尺寸已知) -
不同角度拍摄一组照片(>=4张) -
根据标定板,生成一组对应世界坐标
- 实际标定板行数和列数,设置行-1 列-1,识别内部的点,所以拍摄棋盘格时,最外的行列遮挡也可;
- 实际尺寸影响对相机的像素焦距、像主点、畸变系数、外参中的旋转矩阵没有影响,dx\dy改变,外参中的平移矩阵、双目标定中的基线有影响
-
(亚像素)角点提取
findChessboardCorners 角点检测cornerSubPix 亚像素角点检测 -
标定
-
calibrateCamera calibrateCamera(objectPoints, imagePoints, imageSize, cameraMatrix, distCoeffs, rvecs=None, tvecs=None, flags=None, criteria=None)
CV_CALIB_USE_INTRINSIC_GUESS:使用该参数时,在cameraMatrix矩阵中应该有fx,fy,u0,v0的估计值。否则的话,将初始化(u0,v0)图像的中心点,使用最小二乘估算出fx,fy。
CV_CALIB_FIX_PRINCIPAL_POINT:在进行优化时会固定光轴点。当CV_CALIB_USE_INTRINSIC_GUESS参数被设置,光轴点将保持在中心或者某个输入的值。
CV_CALIB_FIX_ASPECT_RATIO:固定fx/fy的比值,只将fy作为可变量,进行优化计算。当CV_CALIB_USE_INTRINSIC_GUESS没有被设置,fx和fy将会被忽略。只有fx/fy的比值在计算中会被用到。
CV_CALIB_ZERO_TANGENT_DIST:设定切向畸变参数(p1,p2)为零。
CV_CALIB_FIX_K1,…,CV_CALIB_FIX_K6:对应的径向畸变在优化中保持不变。
CV_CALIB_RATIONAL_MODEL:计算k4,k5,k6三个畸变参数。如果没有设置,则只计算其它5个畸变参数。
-
评价,重投影误差
-
内参优化
class ZZY_Calib:
def __init__(self, rows, cols, length_chess=1):
self.rows = rows
self.cols = cols
self.length_chess = length_chess # 实际标定板棋盘格尺寸,影响到dx\dy,外参中的平移矩阵、双目标定中的基线,其余没有影响
self.criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) ### find corner iter stop
self.obj_pts = self.gen_world_coor(self.rows, self.cols)
def gen_world_coor(self, rows, cols):
obj_pts = np.zeros((rows * cols, 3), np.float32)
obj_pts[:, :2] = np.mgrid[0:rows, 0:cols].T.reshape(-1, 2)
return obj_pts
def show_chessboard_corner(self, img, corners, ret=1):
# 在棋盘上绘制角点,可视化工具
img = cv2.drawChessboardCorners(img,(self.rows, self.cols), corners, ret)
cv2.namedWindow('img', 0)
cv2.resizeWindow('img', 500, 500)
cv2.imshow('img',img)
cv2.waitKey(0)
cv2.destroyAllWindows()
def calib(self, path_img, path_campara, show_corner=False):
self.l_obj_pts=[]
self.l_img_pts=[]
obj_pts = self.gen_world_coor(self.rows, self.cols)
l_file = glob.glob(os.path.join(path_img, "*.jpg"))
print("Loading [%d] calib Images" % len(l_file))
for ind, file in enumerate(l_file):
print("calib [%d]:%s ..." % (ind, file))
img = cv2.imdecode(np.fromfile(file, dtype='uint8'), -1)
if len(img.shape) == 3:
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
else:
img_gray = img
ret, corners = cv2.findChessboardCorners(img_gray, (self.rows, self.cols), None)
if ret:
if show_corner:
self.show_chessboard_corner(img, corners, ret)
self.l_obj_pts.append(obj_pts)
corners2 = cv2.cornerSubPix(img_gray, corners, (11,11), (-1,-1), self.criteria) # 执行亚像素级角点检测
self.l_img_pts.append(corners2)
'''
传入所有图片各自角点的三维、二维坐标,相机标定。
每张图片都有自己的旋转和平移矩阵,但是相机内参和畸变系数只有一组
mtx,相机内参;dist,畸变系数;revcs,旋转矩阵;tvecs,平移矩阵。
'''
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(self.l_obj_pts, self.l_img_pts, img_gray.shape[::-1], None, None)
# Calibration Error
tot_error = 0
for i in range(len(self.l_obj_pts)):
imgpoints2, _ = cv2.projectPoints(self.l_obj_pts[i], rvecs[i], tvecs[i], mtx, dist)
error = cv2.norm(self.l_img_pts[i],imgpoints2, cv2.NORM_L2)/len(imgpoints2)
tot_error += error
print ("total error: ", tot_error/len(self.l_obj_pts))
# np.savez(self.path_campara, mtx=mtx, dist=dist)
print('ret', ret)
print('内参矩阵:', mtx)
print('畸变系数:', dist)
print('旋转矩阵:', rvecs)
print('平移矩阵:', tvecs)
'''
优化相机内参(camera matrix),可选,提高精度。
alpha= 1, 所有像素都保留,有黑色像素混入
alpha=0, 尽可能裁剪不想要的像素,都是有效,这是个scale
'''
alpha=1
newcameramtx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist, (img_gray.shape[1], img_gray.shape[0]), alpha, (img_gray.shape[1], img_gray.shape[0]))
print('优化后的相机内参:', newcameramtx)
print("ROI:", roi)
np.savez(path_campara, mtx=mtx, dist=dist, new_mtx=newcameramtx, roi=roi)
def read_campara(self, path):
# 读取相机内参数
with np.load(path) as X:
mtx, dist = [X[i] for i in ('mtx', 'dist')]
return mtx, dist
立体标定
-
获取两个相机的相对位姿关系:包括旋转和平移矩阵 -
分别标定左右相机 -
立体标定 -
stereoCalibrate stereoCalibrate(objectPoints, imagePoints1, imagePoints2, cameraMatrix1, distCoeffs1, cameraMatrix2, distCoeffs2, imageSize, R=None, T=None, E=None, F=None, flags=None, criteria=None)
objectPoints 标定角点在世界坐标系中的位置;
imagePoints1 标定角点在第一个摄像机下的投影后的亚像素坐标;
imagePoints2 标定角点在第二个摄像机下的投影后的亚像素坐标;
cameraMatrix1 第一个摄像机的相机矩阵;
distCoeffs1 第一个摄像机的输入/输出型畸变向量。根据矫正模型的不同,输出向量长度由标志决定;
cameraMatrix2 第二个摄像机的相机矩阵。参数意义同第一个相机矩阵相似;
distCoeffs2 第一个摄像机的输入/输出型畸变向量。根据矫正模型的不同,输出向量长度由标志决定;
imageSize 图像的大小;
R 第一和第二个摄像机之间的旋转矩阵;
T 第一和第二个摄像机之间的平移矩阵;
E 基本矩阵;
F 基础矩阵;
term_crit 迭代优化的终止条件
-
flags CV_CALIB_FIX_INTRINSIC 如果该标志被设置,那么就会固定输入的cameraMatrix和distCoeffs不变,只求解
$R,T,E,F$.
CV_CALIB_USE_INTRINSIC_GUESS 根据用户提供的cameraMatrix和distCoeffs为初始值开始迭代
CV_CALIB_FIX_PRINCIPAL_POINT 迭代过程中不会改变主点的位置
CV_CALIB_FIX_FOCAL_LENGTH 迭代过程中不会改变焦距
CV_CALIB_SAME_FOCAL_LENGTH 强制保持两个摄像机的焦距相同
CV_CALIB_ZERO_TANGENT_DIST 切向畸变保持为零
CV_CALIB_FIX_K1,...,CV_CALIB_FIX_K6 迭代过程中不改变相应的值。如果设置了 CV_CALIB_USE_INTRINSIC_GUESS 将会使用用户提供的初始值,否则设置为零
CV_CALIB_RATIONAL_MODEL 畸变模型的选择,如果设置了该参数,将会使用更精确的畸变模型,distCoeffs的长度就会变成8
-
立体校正
stereoRectify initUndistortRectifyMap remap -
获取视差图像
StereoSGBM_create stereo.compute reprojectImageTo3D
def calib2(self, path, path_l, path_r, obj_pts, img_pts_l, img_pts_r):
print("[Calib 2Cam]")
with np.load(path_l) as X:
Otmx_l, dist_l, size = [X[i] for i in ('Otmx', 'dist', 'size')]
with np.load(path_r) as X:
Otmx_r, dist_r = [X[i] for i in ('Otmx', 'dist')]
flags = 0
flags |= cv2.CALIB_FIX_INTRINSIC # 固定cam 和 dist不变,只求解R,T,E,F
# 立体标定
retS, MLS, dLS, MRS, dRS, R, T, E, F = cv2.stereoCalibrate(obj_pts, img_pts_l, img_pts_r, Otmx_l, dist_l, Otmx_r, dist_r,
(size[0],size[1]), criteria=self.criteria_stero, flags=flags)
def stereoRectify(self, path, imgL, imgR):
with np.load(path) as X:
steroMap_l0, steroMap_l1, steroMap_r0, steroMap_r1 = [X[i] for i in ("steroMapL0", "steroMapL1", "steroMapR0", "steroMapR1")]
imgL_rect = cv2.remap(imgL, steroMap_l0, steroMap_l1, cv2.INTER_LANCZOS4, cv2.BORDER_CONSTANT, 0)
imgR_rect = cv2.remap(imgR, steroMap_r0, steroMap_r1, cv2.INTER_LANCZOS4, cv2.BORDER_CONSTANT, 0)
imgHsatck = np.hstack([imgL_rect, imgR_rect])
imgHstack0 = np.hstack([imgL, imgR])
imgVstack = np.vstack([imgHstack0, imgHsatck])
cv2.imencode(".jpg", imgHsatck)[1].tofile(r'D:\Programs\StereoVision\SteroVision\Data\stereoRectify.jpg')
cv2.namedWindow("hstack", cv2.WINDOW_NORMAL)
cv2.imshow("hstack", imgVstack)
cv2.waitKey(0)
畸变校正
沿着透镜半径方向分布的畸变,靠近透镜中心畸变较明显,表现在短焦镜头,主要包括桶形畸变和枕形畸变。
x
′
=
x
(
1
+
k
1
r
2
+
k
2
r
4
+
k
3
r
6
)
y
′
=
y
(
1
+
k
1
r
2
+
k
2
r
4
+
k
3
r
6
)
\begin{aligned} x' &= x(1+{k_1}r^2+{k_2}r^4+{k_3}r^6) \\ y' &=y(1+{k_1}r^2+{k_2}r^4+{k_3}r^6) \end{aligned}
x′y′?=x(1+k1?r2+k2?r4+k3?r6)=y(1+k1?r2+k2?r4+k3?r6)?
由于透镜本身与相机传感器平面(成像平面)不平行产生,由于透镜粘贴到镜头模组偏差导致。
x
′
=
x
+
[
2
p
1
y
+
p
2
(
r
2
+
2
x
2
)
]
y
′
=
y
+
[
2
p
2
x
+
p
1
(
r
2
+
2
y
2
)
]
\begin{aligned} x' = x + [2p_1y + p_2(r^2 + 2x^2)] \\ y' = y + [2p_2x + p_1(r^2 + 2y^2)] \end{aligned}
x′=x+[2p1?y+p2?(r2+2x2)]y′=y+[2p2?x+p1?(r2+2y2)]?
一般由镜头设计加工安装误差导致,一般情况可忽略
x
′
=
x
+
s
1
(
x
2
+
y
2
)
y
′
=
y
+
s
2
(
x
2
+
y
2
)
\begin{aligned} x' = x + s_1(x^2 + y^2) \\ y' = y + s_2(x^2 + y^2) \end{aligned}
x′=x+s1?(x2+y2)y′=y+s2?(x2+y2)?
-
畸变参数(一般考虑OpenCV前五个参数, k1, k2, p1, p2, k3)
- 径向畸变 k1 k2 k3
- 切向畸变 p1 p2
- 薄棱镜畸变 s1 s2
-
畸变校正有两种方法
def undistort_img(self, img, mtx, dist, newcameramtx, roi):
img_dst = cv2.undistort(img, mtx, dist, None, newcameramtx)
# 这步只是输出纠正畸变以后的图片
x, y, w, h = roi
dst = img_dst[y:y + h, x:x + w]
cv2.imwrite('calibresult.png', dst)
手眼标定
def grab_one_cam(path_img):
VideoCapture = cv2.VideoCapture(0) # USB摄像头
# VideoCapture = cv2.VideoCapture(rtsp) # RTSP
if not VideoCapture.isOpened():
print("Error open video!")
exit()
frame_no = 0
while VideoCapture.isOpened():
ret, frame = VideoCapture.read()
if not ret:
break
k = show_image("frame", frame, 1)
if k == ord("Q"):
break
elif k == ord("S"):
cv2.imencode(".jpg", frame)[1].tofile(os.path.join(path_img, "img_%04d.jpg" % frame_no))
frame_no += 1
VideoCapture.release()
已知内参标定外参
- 标定已知相机内参
- 通过对应点计算投影矩阵
- 根据内参和对应点计算外参矩阵
cv2.solvePnPRansac cv2.Rodrigues , 旋转向量转化为旋转矩阵
def calib_image(self, img, path_cam):
# 已知相机内参和标定图片,输出外参数
mtx, dist = self.read_campara(path_cam)
if len(img.shape) == 3:
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
else:
img_gray = img.copy()
ret, corners = cv2.findChessboardCorners(img_gray, (self.rows, self.cols), None)
if ret:
exact_corners = cv2.cornerSubPix(img_gray, corners, (11, 11), (-1, -1), self.criteria)
_, rvec, tvec, inliers = cv2.solvePnPRansac(self.obj_pts, exact_corners, mtx, dist)
rotation_m, _ = cv2.Rodrigues(rvec) # 罗德里格斯变换,从旋转向量到旋转矩阵
rotation_t = np.hstack([rotation_m, tvec])
rotation_t_Homogeneous_matrix = np.vstack([rotation_t, np.array([[0, 0, 0, 1]])])
QA
fu = fx * dx
fv = fy * dy
fx、fy内参矩阵中的像素焦距
fu、fv为毫米焦距
dx、dy为像素到实际尺寸的转换关系,像素<->毫米
**实际尺寸**影响对相机的像素焦距、像主点、畸变系数、外参中的旋转矩阵没有影响,dx\dy改变,外参中的平移矩阵、双目标定中的基线有影响
实际标定板行数和列数,设置行-1 列-1,识别内部的点,所以拍摄棋盘格时,最外的行列遮挡也可;
参考文献
- 《Camera Calibration and 3D Reconstruction》
|