使用OpenCV进行相机标定

前言

前面我总结过两篇关于双目相机的原理以及张氏标定法理论的介绍,而在本文中将通过使用openCV编写标定代码来标定ZED双目相机,不过在此之前先简单了解一下ZED相机(以下介绍参考取官方网站的内容:stereolabs.com/zed/)。

  • 视频流的帧率和分辨率:
模式 FPS 分辨率
2.2K 15 4416x1242
1080p 30 3840x1080
720p 60 2560x720
WVGA 100 1344x376
  • Stereo Baseline:120 mm
  • 光圈:ƒ/2.0
  • 视角:90° (H) x 60° (V) x 110° (D) max
  • 深度范围:0.5 - 20 m (2.3 to 65 ft)
  • 深度值位数:32-bits

获取ZED的图像

三种方法可以采集ZED相机拍摄的图像:

  1. 使用SDK中厂商提供的工具
  2. 使用SDK提供的接口函数
  3. 直接使用OpenCV获取

由于ZED的SDK完全依赖Nvidia的cudn,而我没有Nvidia的显卡,所以没法使用。不过ZED相机遵循UVC接口标准,而且仅仅需要采集其双目的原始图像,所以只用OpenCV即可,代码与其他摄像头一样:

1
2
3
4
5
6
7
8
9
10
11
12
import sys
import os
print(sys.version)
import cv2
import numpy as np
import matplotlib.pyplot as plt

cap = cv2.VideoCapture(0)
if not cap.isOpened():
print('Open Camera fail!')
else:
ret, frame = cap.read()

检查采集到的图像:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
def pairImageShow(img1, img2, mode11=None, model2=None):
plt.figure(figsize=(15,15))
plt.subplot(1,2,1)
if mode11 == 'gray':
plt.imshow(img1, cmap='gray')
else:
plt.imshow(img1)
plt.subplot(1,2,2)
if model2 == 'gray':
plt.imshow(img2, cmap='gray')
else:
plt.imshow(img2)
plt.show()

imgs_l = []
imgs_r = []
image_path = '../stereo_calibration/ZED_stereo_calibration/calib_image'
image_list = os.listdir(image_path)
for image_name in image_list:
# print(image_name)
full_path = os.path.join(image_path, image_name)
img = cv2.imread(full_path)
half_col = img.shape[1]//2
imgs_l.append(img[:,:half_col,:])
imgs_r.append(img[:,half_col:,:])

img_rgb_l = cv2.cvtColor(imgs_l[0], cv2.COLOR_BGR2RGB)
img_rgb_r = cv2.cvtColor(imgs_r[0], cv2.COLOR_BGR2RGB)
pairImageShow(img_rgb_l, img_rgb_r)

角点检测

在前面的张氏标定的原理中,我已经介绍过当前广泛使用的张氏标定法通常用棋盘格作为标定物,通过寻找棋盘格的角点作为空间参考坐标点求未知量。利用opencv中的findChessboardCorners函数可以找到棋盘图像中的角点。

在下面的代码中,boardSize定义了角点的总个数和排列,如(7,5)表示为水平方向为7、竖直方向为5的角点阵列。只有当所有的角点都找到后,findChessboardCorners的返回值isFind才为真。findChessboardCorners函数找到的角点的坐标是不精确的,需要再使用cornerSubPix函数,进行亚像素的调整。对角点检查的结果,使用drawChessboardCorners函数画出来。

角点检测的坐标是角点当前姿态投影到照片的坐标

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def getChessboardCorners(colorimg, boardSize):
grayimg = cv2.cvtColor(colorimg, cv2.COLOR_RGB2GRAY)
isFind, corners = cv2.findChessboardCorners(colorimg, boardSize, 0)
if isFind:
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 20, 0.1) # 阈值
cv2.cornerSubPix(grayimg, corners, (5,5), (-1,-1), criteria)
return corners
else:
return None

w, h = 7, 5
boardSize = (w, h)
corners_l = []
corners_r = []
for i in range(len(image_list)):
# print('find corner in image:', i)
corner_l = getChessboardCorners(imgs_l[i], boardSize)
corner_r = getChessboardCorners(imgs_r[i], boardSize)
if corners is not None and corners is not None:
corners_l.append(corner_l)
corners_r.append(corner_r)

cornerimg = cv2.drawChessboardCorners(imgs_l[0], boardSize, corners_l[0], True)
cornerimg_rgb = cv2.cvtColor(cornerimg, cv2.COLOR_BGR2RGB)
plt.figure(figsize=(15,15))
plt.imshow(cornerimg_rgb)
plt.show()

下图为检查结果的截图:

corner detected

计算角点的空间坐标系

我们都以左上角的角点的为坐标原点,以棋盘格的横向和竖向方向为空间的两个坐标轴,建立空间坐标系,所以所有角点均有一个坐标值为0,而且不管棋盘怎么移动,角点的空间坐标都不变。使用numpy的mgrid函数生成网格坐标是十分好用的,虽然理解上需要点时间。

1
2
3
4
5
6
7
8
9
10
11
block_w = 51
block_h = 50

corner_real_coord = np.zeros((w*h,3), np.float32)
grid_data = np.mgrid[0:w,0:h]
grid_data[0,:,:] *= block_w
grid_data[1,:,:] *= block_h
corner_real_coord[:,:2] = grid_data.T.reshape(-1,2)
img_pair_num = len(corners_l)
print(img_pair_num)
corner_real_coords = [corner_real_coord for i in range(img_pair_num)]

计算内参和外参与畸变系数

前面的张氏标定原理有介绍,一个单应性矩阵有可以表示一个姿态下图像坐标到空间坐标的映射,而一个单应性矩阵有8个未知量,所以在一张棋盘格纸上需要至少超出8个以上的角点才能求得单应性矩阵。而当单应性矩阵求得后,能够提供两个方程来求相机得内参矩阵。在加入了倾斜因子后,内参矩阵中有5个未知量,所以至少需要3个姿态的棋盘照片。

在opencv中提供了calibrateCamera函数对相机进行标定,这个函数将标定结果、相机的内参数矩阵、畸变系数、旋转矩阵和平移向量一步完成并返回。

1
2
3
4
5
6
7
ret_l, mtx_l, dist_l, rvecs_l, tvecs_l = cv2.calibrateCamera(corner_real_coord, corners_l, (img_rgb_l.shape[1],img_rgb_l.shape[0]), None, None)
ret_r, mtx_r, dist_r, rvecs_r, tvecs_r = cv2.calibrateCamera(corner_real_coords, corners_r,(img_rgb_l.shape[1],img_rgb_l.shape[0]), None, None)
print('error: ',ret_l)
print('内参:',mtx_l)
print('畸变系数:',dist_l)
# print('旋转向量:',rvecs_l)
# print('平移向量:',tvecs_l)

去畸变

使用计算得到畸变系数,使用opencv中提供的undistort函数对图像进行去畸变(要注意如果照片少,畸变系数的误差会很大,校正的效果也不好)。

1
2
3
4
5
dst_img = cv2.undistort(imgs_r[0], mtx_r, dist_r)
dst_img_rgb = cv2.cvtColor(dst_img, cv2.COLOR_BGR2RGB)
plt.figure(figsize=(15,15))
plt.imshow(dst_img_rgb)
plt.show()

立体标定与矫正

使用opencv的stereoCalibrate函数能够同时对两个镜头进行标定,标定结果可以得到两个摄像头的内外参数矩阵和两个摄像头的相对位置的R和T,有一点要注意的就是:由于我们上面已经分别对每个摄像头求出了他们各自的内参,所以在这里就不用重复做了,需要将stereoCalibrate函数的flags参数设置为CALIB_FIX_INTRINSIC。

1
2
3
4
5
6
7
8
crit = (cv2.TERM_CRITERIA_COUNT + cv2.TERM_CRITERIA_EPS, 100, 1e-5)
ret = cv2.stereoCalibrate(corner_real_coords, corners_l, corners_r,
mtx_l, dist_l,
mtx_r, dist_r,
(img_rgb_l.shape[1],img_rgb_l.shape[0]),
flags = cv2.CALIB_FIX_INTRINSIC,
criteria = crit)
R, T = ret[-4:-2]

在实际矫正中,我们是根据两个相机的相对关系,最终得到其在同以平面对其的图像,因此我们需要先求得两个摄像头得矫正变换矩阵。通过opencv得stereoRectify函数可以得到,左右相机需要得变换矩阵Rl、Rr和投影矩阵Pl、Pr:

1
2
3
ret = cv2.stereoRectify(mtx_l, dist_l, mtx_r, dist_r, (img_rgb_l.shape[1],img_rgb_l.shape[0]), R, T)
Rl, Rr, Pl, Pr = ret[:4]
Rl, Rr, Pl, Pr

经过立体标定后,双目相机的相对位置就知道了,然后就可以进行立体矫正,在opencv中需要分两个步骤完成,通过initUndistortRectifyMap得到映
射关系,再使用remap函数完成矫正。

1
2
3
4
5
6
7
8
9
map_l1, map_l2 = cv2.initUndistortRectifyMap(mtx_l, dist_l, Rl, Pl, (img_rgb_l.shape[1],img_rgb_l.shape[0]), cv2.CV_32FC1)
map_r1, map_r2 = cv2.initUndistortRectifyMap(mtx_r, dist_r, Rr, Pr, (img_rgb_l.shape[1],img_rgb_l.shape[0]), cv2.CV_32FC1)

reimg_l = cv2.remap(imgs_l[10], map_l1, map_l2, cv2.INTER_LINEAR)
reimg_r = cv2.remap(imgs_r[10], map_r1, map_r2, cv2.INTER_LINEAR)
dst_img_rgb = cv2.cvtColor(reimg_r, cv2.COLOR_BGR2RGB)
plt.figure(figsize=(15,15))
plt.imshow(dst_img_rgb)
plt.show()

画水平线检查,立体矫正得效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
cav_w, cav_h = imgs_l[0].shape[1]//2, imgs_l[0].shape[0]//2
cavimg = np.zeros((cav_h, cav_w*2, 3), np.uint8)

reimg_l = cv2.remap(imgs_l[1], map_l1, map_l2, cv2.INTER_LINEAR)
reimg_r = cv2.remap(imgs_r[1], map_r1, map_r2, cv2.INTER_LINEAR)

cavimg[:,:cav_w,:] = cv2.resize(reimg_l, (cav_w, cav_h))
cavimg[:,cav_w:,:] = cv2.resize(reimg_r, (cav_w, cav_h))
step = cav_h//10
for i in range(10):
rowval = i*step
cv2.line(cavimg, (0, rowval), (cav_w*2, rowval), (0,0,255), 1, 8)

dst_img_rgb = cv2.cvtColor(cavimg, cv2.COLOR_BGR2RGB)
plt.figure(figsize=(15,15))
plt.imshow(dst_img_rgb)
plt.show()

保存和读取校准参数,opencv中有模块FileStorage来保存和读取数据,常用保存格式为yml或xml,FileStorage让矩阵的的保存和读取非常方便。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 def saveCaliResult(file_name, names, params):
WRITE = 1
f = cv2.FileStorage(file_name, WRITE)
for name, val in zip(names, params):
f.write(name, val)
f.release()

def loadCaliResult(califile):
READ = 0
fs = cv2.FileStorage(califile, READ)
mtx_l = fs.getNode('mtx_l').mat()
mtx_r = fs.getNode('mtx_r').mat()
dist_l = fs.getNode('dist_l').mat()
dist_r = fs.getNode('dist_r').mat()
Rl = fs.getNode('Rl').mat()
Rr = fs.getNode('Rr').mat()
Pl = fs.getNode('Pl').mat()
Pr = fs.getNode('Pr').mat()
fs.release()
return mtx_l, mtx_r, dist_l, dist_r, Rl, Rr, Pl, Pr

参考

  1. 使用OpenCV进行标定(Python)
  2. 双摄像头立体成像(三)-畸变矫正与立体校正
Compartir