使用OpenCV/python进行双目测距

在做 SLAM 时,希望用到深度图来辅助生成场景,所以要构建立体视觉,在这里使用 OpenCV 的 Stereo 库和 python 来进行双目立体视觉的图像处理。

  • 立体标定
  • 应用标定数据
  • 转换成深度图

标定

在开始之前,需要准备的当然是两个摄相头,根据你的需求将两个摄像头进行相对位置的固定,我是按平行来进行固定的(如果为了追求两个双目图像更高的生命度,也可以将其按一定钝角固定,这样做又限制了场景深度的扩展,根据实际需求选择)

Stereo Camera

由于摄像头目前是我们手动进行定位的,我们现在还不知道两张图像与世界坐标之间的耦合关系,所以下一步要进行的是标定,用来确定分别获取两个摄像头的内部参数,并且根据两个摄像头在同一个世界坐标下的标定参数来获取立体参数。注:不要使用 OpenCV 自带的自动 calbration,其对棋盘的识别率极低,使用 Matlab 的 Camera Calibration Toolbox 更为有效,具体细节请看:摄像机标定和立体标定

同时从两个摄像头获取图片

import cv2
import time

AUTO = True # 自动拍照,或手动按 s 键拍照
INTERVAL = 2 # 自动拍照间隔

cv2.namedWindow("left")
cv2.namedWindow("right")
cv2.moveWindow("left", 0, 0)
cv2.moveWindow("right", 400, 0)
left_camera = cv2.VideoCapture(0)
right_camera = cv2.VideoCapture(1)

counter = 0
utc = time.time()
pattern = (12, 8) # 棋盘格尺寸
folder = "./snapshot/" # 拍照文件目录

def shot(pos, frame):
global counter
path = folder + pos + "_" + str(counter) + ".jpg"

cv2.imwrite(path, frame)
<span class="hljs-built_in">print</span>(<span class="hljs-string">"snapshot saved into: "</span> + path)

while True:
ret, left_frame = left_camera.read()
ret, right_frame = right_camera.read()

cv2.imshow(<span class="hljs-string">"left"</span>, left_frame)
cv2.imshow(<span class="hljs-string">"right"</span>, right_frame)

now = time.time()
<span class="hljs-keyword">if</span> AUTO <span class="hljs-keyword">and</span> now - utc &gt;= INTERVAL:
    shot(<span class="hljs-string">"left"</span>, left_frame)
    shot(<span class="hljs-string">"right"</span>, right_frame)
    counter += <span class="hljs-number">1</span>
    utc = now

key = cv2.waitKey(<span class="hljs-number">1</span>)
<span class="hljs-keyword">if</span> key == <span class="hljs-built_in">ord</span>(<span class="hljs-string">"q"</span>):
    <span class="hljs-keyword">break</span>
<span class="hljs-keyword">elif</span> key == <span class="hljs-built_in">ord</span>(<span class="hljs-string">"s"</span>):
    shot(<span class="hljs-string">"left"</span>, left_frame)
    shot(<span class="hljs-string">"right"</span>, right_frame)
    counter += <span class="hljs-number">1</span>

left_camera.release()
right_camera.release()
cv2.destroyWindow("left")
cv2.destroyWindow("right")

下面是我拍摄的样本之一,可以肉眼看出来这两个摄像头成像都不是水平的,这更是需要标定的存在的意义

在进行标定的过程中,要注意的是在上面标定方法中没有提到的是,单个标定后,要对标定的数据进行错误分析(Analyse Error),如左图,是我对左摄像头的标定结果分析。图中天蓝色点明显与大部分点不聚敛,所以有可能是标定时对这个图片标定出现的错误,要重新标定,在该点上点击并获取其图片名称索引,对其重新标定后,右图的结果看起来还是比较满意的

在进行完立体标定后,我们将得到如下的数据:

Stereo calibration parameters after optimization:

Intrinsic parameters of left camera:

Focal Length: fc_left = [ 824.93564 825.93598 ] [ 8.21112 8.53492 ]
Principal point: cc_left = [ 251.64723 286.58058 ] [ 13.92642 9.11583 ]
Skew: alpha_c_left = [ 0.00000 ] [ 0.00000 ] => angle of pixel axes = 90.00000 0.00000 degrees
Distortion: kc_left = [ 0.23233 -0.99375 0.00160 0.00145 0.00000 ] [ 0.05659 0.30408 0.00472 0.00925 0.00000 ]

Intrinsic parameters of right camera:

Focal Length: fc_right = [ 853.66485 852.95574 ] [ 8.76773 9.19051 ]
Principal point: cc_right = [ 217.00856 269.37140 ] [ 10.40940 9.47786 ]
Skew: alpha_c_right = [ 0.00000 ] [ 0.00000 ] => angle of pixel axes = 90.00000 0.00000 degrees
Distortion: kc_right = [ 0.30829 -1.61541 0.01495 -0.00758 0.00000 ] [ 0.06567 0.55294 0.00547 0.00641 0.00000 ]

Extrinsic parameters (position of right camera wrt left camera):

Rotation vector: om = [ 0.01911 0.03125 -0.00960 ] [ 0.01261 0.01739 0.00112 ]
Translation vector: T = [ -70.59612 -2.60704 18.87635 ] [ 0.95533 0.79030 5.25024 ]

应用标定数据

我们使用如下的代码来将其配置到 python 中,上面的参数都是手动填写至下面的内容中的,这样免去保存成文件再去读取,在托运填写的时候要注意数据的对应位置

# filename: camera_configs.py
import cv2
import numpy as np

left_camera_matrix = np.array([[824.93564, 0., 251.64723],
[0., 825.93598, 286.58058],
[0., 0., 1.]]
)
left_distortion = np.array([[0.23233, -0.99375, 0.00160, 0.00145, 0.00000]])

right_camera_matrix = np.array([[853.66485, 0., 217.00856],
[0., 852.95574, 269.37140],
[0., 0., 1.]]
)
right_distortion = np.array([[0.30829, -1.61541, 0.01495, -0.00758, 0.00000]])

om = np.array([0.01911, 0.03125, -0.00960]) # 旋转关系向量
R = cv2.Rodrigues(om)[0] # 使用 Rodrigues 变换将 om 变换为 R
T = np.array([-70.59612, -2.60704, 18.87635]) # 平移关系向量

size = (640, 480) # 图像尺寸

进行立体更正

R1, R2, P1, P2, Q, validPixROI1, validPixROI2 = cv2.stereoRectify(left_camera_matrix, left_distortion,
right_camera_matrix, right_distortion, size, R,
T)

计算更正 map

left_map1, left_map2 = cv2.initUndistortRectifyMap(left_camera_matrix, left_distortion, R1, P1, size, cv2.CV_16SC2)
right_map1, right_map2 = cv2.initUndistortRectifyMap(right_camera_matrix, right_distortion, R2, P2, size, cv2.CV_16SC2)

这样,我们得到了左右摄像头的两个 map,并得到了立体的 Q,这些参数都将应用于下面的转换成深度图中

转换成深度图

import numpy as np
import cv2
import camera_configs

cv2.namedWindow("left")
cv2.namedWindow("right")
cv2.namedWindow("depth")
cv2.moveWindow("left", 0, 0)
cv2.moveWindow("right", 600, 0)
cv2.createTrackbar("num", "depth", 0, 10, lambda x: None)
cv2.createTrackbar("blockSize", "depth", 5, 255, lambda x: None)
camera1 = cv2.VideoCapture(0)
camera2 = cv2.VideoCapture(1)

添加点击事件,打印当前点的距离

def callbackFunc(e, x, y, f, p):
if e == cv2.EVENT_LBUTTONDOWN:
print threeD[y][x]

cv2.setMouseCallback("depth", callbackFunc, None)

while True:
ret1, frame1 = camera1.read()
ret2, frame2 = camera2.read()

if not ret1 or not ret2:
    break

# 根据更正map对图片进行重构
img1_rectified = cv2.<span class="hljs-built_in">remap</span>(frame1, camera_configs.left_map1, camera_configs.left_map2, cv2.INTER_LINEAR)
img2_rectified = cv2.<span class="hljs-built_in">remap</span>(frame2, camera_configs.right_map1, camera_configs.right_map2, cv2.INTER_LINEAR)

# 将图片置为灰度图,为StereoBM作准备
imgL = cv2.<span class="hljs-built_in">cvtColor</span>(img1_rectified, cv2.COLOR_BGR2GRAY)
imgR = cv2.<span class="hljs-built_in">cvtColor</span>(img2_rectified, cv2.COLOR_BGR2GRAY)

# 两个trackbar用来调节不同的参数查看效果
num = cv2.<span class="hljs-built_in">getTrackbarPos</span>(<span class="hljs-string">"num"</span>, <span class="hljs-string">"depth"</span>)
blockSize = cv2.<span class="hljs-built_in">getTrackbarPos</span>(<span class="hljs-string">"blockSize"</span>, <span class="hljs-string">"depth"</span>)
if blockSize % <span class="hljs-number">2</span> == <span class="hljs-number">0</span>:
    blockSize += <span class="hljs-number">1</span>
if blockSize &lt; <span class="hljs-number">5</span>:
    blockSize = <span class="hljs-number">5</span>

# 根据Block Maching方法生成差异图(opencv里也提供了SGBM/Semi-Global Block Matching算法,有兴趣可以试试)
stereo = cv2.<span class="hljs-built_in">StereoBM_create</span>(numDisparities=<span class="hljs-number">16</span>*num, blockSize=blockSize)
disparity = stereo.<span class="hljs-built_in">compute</span>(imgL, imgR)

disp = cv2.<span class="hljs-built_in">normalize</span>(disparity, disparity, alpha=<span class="hljs-number">0</span>, beta=<span class="hljs-number">255</span>, norm_type=cv2.NORM_MINMAX, dtype=cv2.CV_8U)
# 将图片扩展至<span class="hljs-number">3</span>d空间中,其z方向的值则为当前的距离
threeD = cv2.<span class="hljs-built_in">reprojectImageTo3D</span>(disparity.<span class="hljs-built_in">astype</span>(np.float32)/<span class="hljs-number">16</span>., camera_configs.Q)

cv2.<span class="hljs-built_in">imshow</span>(<span class="hljs-string">"left"</span>, img1_rectified)
cv2.<span class="hljs-built_in">imshow</span>(<span class="hljs-string">"right"</span>, img2_rectified)
cv2.<span class="hljs-built_in">imshow</span>(<span class="hljs-string">"depth"</span>, disp)

key = cv2.<span class="hljs-built_in">waitKey</span>(<span class="hljs-number">1</span>)
if key == <span class="hljs-built_in">ord</span>(<span class="hljs-string">"q"</span>):
    break
elif key == <span class="hljs-built_in">ord</span>(<span class="hljs-string">"s"</span>):
    cv2.<span class="hljs-built_in">imwrite</span>(<span class="hljs-string">"./snapshot/BM_left.jpg"</span>, imgL)
    cv2.<span class="hljs-built_in">imwrite</span>(<span class="hljs-string">"./snapshot/BM_right.jpg"</span>, imgR)
    cv2.<span class="hljs-built_in">imwrite</span>(<span class="hljs-string">"./snapshot/BM_depth.jpg"</span>, disp)

camera1.release()
camera2.release()
cv2.destroyAllWindows()

下面则是一附成像图,最右侧的为生成的 disparity 图,按照上面的代码,在图上点击则可以读取到该点的距离

Have fun.