前段时间进行了摄像头标定,我使用过matlab直接标定,效果感觉还可以。那么这次纯粹是使用Opencv来进行一次标定。

准备标定板

标定板是必不可少的,这里我说明一下,建议使用长宽不等的标定板,这样可以避免出现错误的姿态误差。比如我下面准备的图片。

board

我是直接打印出来,其实并不建议直接打印,因为我打印出来后发现整张纸有点潮,晾干后不太平整,所以并不太建议直接打印。

标定程序

先说一下步骤:

  • 定位出图片中所有的角点
  • 利用所有的角点去进行单目标定
  • 利用所有的角点去进行双目标定

定位角点

当然,在找点前需要把图片导进来,导进来是一个 numpy 的矩阵,形状为(h, w, 3)

接下来我就直接定义了个函数,用来专门定位。

这个函数简单介绍一下,有两个参数:

  • image是图片的矩阵
  • size是标定板的角点的尺寸,上面的那张标定图就是(11, 8)

返回值就只有两种可能,如果能找到就直接返回点,找不到就返回None

程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def cornerFind(image, size):
"""
角点查找
:param image: 图片
:param size: 角点个数(横向个数,纵向个数)
:return:
"""
# 寻找
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)

# 灰度化
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

# 定位角点
success, corners = cv2.findChessboardCorners(gray, size, None)

if success:
# 成功定位到的话
# 再次使用亚像素定位角点,这样精度更高
output = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria)
return output
else:
return None

单张图片定位角点的程序有了,那么就直接使用。

接下来就是把每次的结果直接放进列表里:

  • 定义obj_points列表,专门放标定板的坐标
  • 定义left_img_points列表,专门放左相机拍摄的角点
  • 定义right_img_points列表,专门放右相机拍摄的角点

那么,完整的程序就是如下:

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# 标定板的尺寸
board = (11, 8)
# 每个块的宽度
size = 19.2

bw, bh = board

# 标定板的坐标
objp = np.zeros((bw * bh, 3), np.float32)
objp[:, :2] = np.mgrid[0:bw, 0:bh].T.reshape(-1, 2)
objp *= size

obj_points = [] # 存储标定板的坐标
left_img_points = [] # 存储图片上的
right_img_points = [] # 存储图片上的

image_size = None

# 利用循环来读取图片
for left_path, right_path in zip(left_image_list, right_image_list):

# 读取图片(左右相机图片)
left_image = cv2.imread(left_path)
right_image = cv2.imread(right_path)

# 获取图片尺寸
image_size = left_image.shape[1::-1]

# 获取角点
left_corners = cornerFind(left_image, board)
right_corners = cornerFind(right_image, board)

if (left_corners is not None) and (right_corners is not None):

# 添加一个标定板坐标
obj_points.append(objp)

# 添加一个图片坐标
left_img_points.append(left_corners)
right_img_points.append(right_corners)

# 画出来
cv2.drawChessboardCorners(left_image, board, left_corners, True)
cv2.drawChessboardCorners(right_image, board, right_corners, True)

# 画出第一个点
cv2.circle(left_image, left_corners[0, 0].astype(int), 10, (255, 0, 0))
cv2.circle(right_image, right_corners[0, 0].astype(int), 10, (255, 0, 0))

# 显示出来
# cv2.imshow("left", left_image)
# cv2.imshow("right", right_image)
# print(left_corners)
# cv2.waitKey(0)

这里我来说一下size这个变量,Opencv的量纲是毫米,size就是单个方框的尺寸,我打印出来之后测量了一下就是 19.2 mm,所以objp *= size的作用就是,将标定板做成实际坐标。

看一下效果吧:

image

单目标定

单目就直接使用cv2.calibrateCamera()就可以直接标定。

会返回出五个结果,分别是:精度、内参矩阵、畸变参数、旋转矩阵、平移向量。

1
2
3
4
5
6
# 左相机标定
_, l_mtx, l_dist, _, _ = cv2.calibrateCamera(
obj_points, left_img_points, image_size, None, None)
# 右相机标定
_, r_mtx, r_dist, _, _ = cv2.calibrateCamera(
obj_points, right_img_points, image_size, None, None)

在接下来的使用中只会使用到内参矩阵和畸变参数,所以,不会直接使用其他的返回值。

双目标定

双目就直接使用cv2.stereoCalibrate()函数。

返回值是:精度、左相机内参矩阵、左相机畸变参数、右相机内参矩阵、右相机畸变参数、旋转矩阵、平移向量、本征矩阵、基本矩阵。

1
2
3
4
5
6
7
# 双目相机标定
ret, l_mtx, l_dist, r_mtx, r_dist, R, T, E, F = cv2.stereoCalibrate(
obj_points, # 位置信息
left_img_points, right_img_points, # 左右图片的点
l_mtx, l_dist, # 左内参矩阵、畸变参数
r_mtx, r_dist, # 右内参矩阵、畸变参数
image_size)

说明一下,旋转矩阵和平移向量是第二个相机(右相机)相对于第一个相机而言(左相机)。

参数保存

随便使用一个方法把这些参数保存下来,这样就可以下此就直接使用了。

可以保存成json格式,方便自己私自打开查看。

使用

以上就是标定的过程,具体的使用还是如下所示。

去除畸变和图像校正

标定完了之后,就可以直接使用了,主要是分为两步:

  • 去除畸变:目的是消除图片的切向畸变和径向畸变
  • 图像校正:将对应位置进行调平

这里我就直接使用cv2.stereoRectify()函数和cv2.initUndistortRectifyMap()函数,我再这里直接封装成了一个类:

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
30
31
32
33
34
35
36
37
38
39
40
41
class RectifyDistort(object):
def __init__(self, size: tuple, datas: dict):

# 计算出立体矫正所需要的映射矩阵
R1, R2, P1, P2, Q, validPixROI1, validPixROI2 = cv2.stereoRectify(
datas["leftCameraMatrix"], datas["leftDistCoeffs"],
datas["rightCameraMatrix"], datas["rightDistCoeffs"],
size, datas["R"], datas["T"])

self.Q = Q

# 左-重映射矩阵
self.left_map = cv2.initUndistortRectifyMap(
datas["leftCameraMatrix"],
datas["leftDistCoeffs"],
R1, P1, size, cv2.INTER_NEAREST)

# 右-重映射矩阵
self.right_map = cv2.initUndistortRectifyMap(
datas["rightCameraMatrix"],
datas["rightDistCoeffs"],
R2, P2, size, cv2.INTER_NEAREST)

def __call__(self, image: np.ndarray, camera: str):
"""
校正图
:param image:
:param camera: 摄像头区分
:return:
"""

if camera == "left":
map1, map2 = self.left_map
elif camera == "right":
map1, map2 = self.right_map
else:
raise ValueError

new_image = cv2.remap(image, map1, map2, cv2.INTER_LINEAR)

return new_image

直接看图片效果。

未处理图片:

old_image

处理图片:

new_image

Q矩阵的使用

在上面的程序中,还会得到一个Q矩阵,这是一个关键的矩阵,可以利用此矩阵直接将世界坐标计算出来。

公式为:

直接使用Q矩阵乘上一个向量就可以直接得出,世界坐标。

这里说明两点:

  • x、y、d的单位是像素
  • X/W,Y/W,Z/W,才是最终的世界坐标,单位是毫米
  • d是视差值,左像素和右像素的之差