双目项目实战---测距(获取三维坐标和深度信息)

目录

1.简介

2.模块讲解

2.1 立体校正

2.1.1 校正目的

2.1.2 校正方法

2.2 立体匹配和视差计算

2.3 深度计算

3.完整代码


1.简介

双目视觉是一种通过两个摄像机(或者两个镜头)同时拍摄到同一个场景,再通过计算机算法来获取该场景深度和三维信息的技术。这两个摄像机可以放置在固定的位置上,也可以通过移动来获取不同角度的图像以便更好地理解物体形状和结构。

双目视觉的工作原理是,在两个摄像机之间的距离(称为基线)上拍摄到同一物体时,由于视差效应,看到的物体位置会有所不同。计算机可以通过测量这种视差来推导出物体深度和三维信息。这种方法类似于我们的双眼视觉,两只眼睛通过视差来协作,从而让我们能够感知深度和距离。

双目视觉技术可以应用于各种领域,例如机器人、无人驾驶车辆、虚拟现实等。它可以提供高精度的深度信息,有助于实现障碍物检测、目标识别、环境建模等任务。然而,双目视觉也存在一些挑战,如相机校准、镜头畸变、图像匹配等问题,需要通过算法来克服。

大致步骤:

双目标定 > 立体校正(含消除畸变) > 立体匹配 > 视差计算 > 深度计算(3D坐标)计算

  1. 双目标定: 双目标定是确定两个摄像机(或镜头)的内部参数和相对位置关系的过程。它通过拍摄一组已知世界坐标下的特殊标定板或标记点的图像,结合摄像机的内外参数模型,利用图像坐标与真实世界坐标之间的对应关系,求解出摄像机的畸变参数、内参矩阵和外参矩阵等参数。

  2. 立体校正(含消除畸变): 立体校正是为了使得左右两个摄像机的光轴平行,并且图像水平线对齐。在这一步骤中,通过对摄像机的校准结果进行处理,如旋转和平移校准,来调整两个摄像机的视角和位置,使得它们对应的像素在同一水平线上。

立体校正同时也可以用于消除摄像机镜头的畸变效应。这种镜头畸变一般可以通过相机标定中得到的畸变参数进行校正,以提高后续立体匹配和深度计算的准确性。

  1. 立体匹配: 立体匹配是指在左右两个校正后的图像中找到相应的像素点对应关系。由于左右两张图像的视角稍有不同,因此对应的像素点会有一定的视差(即像素位移)。立体匹配的目标就是找到这些对应关系,可以通过一些特征点匹配算法(如SIFT、SURF等)或者基于区域的方法(如块匹配算法)来实现。

  2. 视差计算: 视差计算是通过立体匹配得到的像素点对应关系,计算出每个像素点的视差值。视差值代表了左右两个摄像机之间的像素位移,可以用来表示目标物体相对于摄像机的距离信息。常见的视差计算算法包括SSD(Sum of Squared Differences)、SAD(Sum of Absolute Differences)、NCC(Normalized Cross Correlation)等。

  3. 深度计算(3D坐标计算): 深度计算是将视差值转换为三维空间中的实际距离。通过已知的立体摄像机的基线(两个摄像机之间的距离)和相机内参,可以利用三角测量原理将视差转化为物体的真实深度。这样就可以得到每个像素点的三维坐标信息,进而构建出整个场景的三维模型。

在整个双目视觉处理流程中,每个步骤的准确性和稳定性都对最终的深度计算结果有重要影响。因此,在实际应用中,需要选择合适的算法和参数设置,并进行充分的实验和调整来获取较好的双目视觉效果。

2.模块讲解

2.1 立体校正

2.1.1 校正目的

立体校正利用双目标定的内外参数(焦距、成像原点、畸变系数)和双目相对位置关系(旋转矩阵和平移向量),分别对左右视图进行消除畸变和行对准,使得左右视图的成像原点坐标一致、两摄像头光轴平行、左右成像平面共面、对极线行对齐。

校正前的左右相机的光心并不是平行的,两个光心的连线就叫基线,像平面与基线的交点就是极点,像点与极点所在的直线就是极线,左右极线与基线构成的平面就是空间点对应的极平面。
校正后,极点在无穷远处,两个相机的光轴平行。像点在左右图像上的高度一致。这也就是极线校正的目标。校正后做后续的立体匹配时,只需在同一行上搜索左右像平面的匹配点即可,能使效率大大提高。

2.1.2 校正方法

实验利用OpenCV中的stereoRectify()函数实现立体校正,stereoRectify()函数内部采用的是Bouguet的极线校正算法,Bouguet算法步骤:
1、将右图像平面相对于左图像平面的旋转矩阵分解成两个矩阵Rl和Rr,叫做左右相机的合成旋转矩阵
2、将左右相机各旋转一半,使得左右相机的光轴平行。此时左右相机的成像面达到平行,但是基线与成像平面不平行
3、构造变换矩阵Rrect使得基线与成像平面平行。构造的方法是通过右相机相对于左相机的偏移矩阵T完成的
4、通过合成旋转矩阵与变换矩阵相乘获得左右相机的整体旋转矩阵。左右相机坐标系乘以各自的整体旋转矩阵就可使得左右相机的主光轴平行,且像平面与基线平行
5、通过上述的两个整体旋转矩阵,就能够得到理想的平行配置的双目立体系图像。校正后根据需要对图像进行裁剪,需重新选择一个图像中心,和图像边缘从而让左、右叠加部分最大
校正后的效果图:

 相关代码

def getRectifyTransform(height, width, config):
    # 读取内参和外参
    left_K = config.cam_matrix_left
    right_K = config.cam_matrix_right
    left_distortion = config.distortion_l
    right_distortion = config.distortion_r
    R = config.R
    T = config.T

    # 计算校正变换
    R1, R2, P1, P2, Q, roi1, roi2 = cv2.stereoRectify(left_K, left_distortion, right_K, right_distortion,
                                                      (width, height), R, T, alpha=0)
    map1x, map1y = cv2.initUndistortRectifyMap(left_K, left_distortion, R1, P1, (width, height), cv2.CV_32FC1)
    map2x, map2y = cv2.initUndistortRectifyMap(right_K, right_distortion, R2, P2, (width, height), cv2.CV_32FC1)

    return map1x, map1y, map2x, map2y, Q


# 畸变校正和立体校正
def rectifyImage(image1, image2, map1x, map1y, map2x, map2y):
    rectifyed_img1 = cv2.remap(image1, map1x, map1y, cv2.INTER_AREA)
    rectifyed_img2 = cv2.remap(image2, map2x, map2y, cv2.INTER_AREA)

    return rectifyed_img1, rectifyed_img2

注意:立体校正前应先进行消除畸变处理

2.2 立体匹配和视差计算

立体匹配也称作视差估计,立体匹配可划分为四个步骤:匹配代价计算、代价聚合、视差计算和视差优化。立体校正后的左右两幅图像得到后,匹配点是在同一行上的,可以使用OpenCV中的BM算法或者SGBM算法计算视差图。由于SGBM算法的表现要远远优于BM算法,因此采用SGBM算法获取视差图。在立体匹配生成视差图后,可以对视差图进行后处理,如滤波,空洞填充等方法,从而改善视差图的视觉效果

相关代码
 

def stereoMatchSGBM(left_image, right_image, down_scale=False):
    # SGBM匹配参数设置
    if left_image.ndim == 2:
        img_channels = 1
    else:
        img_channels = 3
    blockSize = 3
    paraml = {'minDisparity': 0,
              'numDisparities': 64,
              'blockSize': blockSize,
              'P1': 8 * img_channels * blockSize ** 2,
              'P2': 32 * img_channels * blockSize ** 2,
              'disp12MaxDiff': 1,
              'preFilterCap': 63,
              'uniquenessRatio': 15,
              'speckleWindowSize': 100,
              'speckleRange': 1,
              'mode': cv2.STEREO_SGBM_MODE_SGBM_3WAY
              }

    # 构建SGBM对象
    left_matcher = cv2.StereoSGBM_create(**paraml)
    paramr = paraml
    paramr['minDisparity'] = -paraml['numDisparities']
    right_matcher = cv2.StereoSGBM_create(**paramr)

    # 计算视差图
    size = (left_image.shape[1], left_image.shape[0])
    if down_scale == False:
        disparity_left = left_matcher.compute(left_image, right_image)
        disparity_right = right_matcher.compute(right_image, left_image)
    else:
        left_image_down = cv2.pyrDown(left_image)
        right_image_down = cv2.pyrDown(right_image)
        factor = left_image.shape[1] / left_image_down.shape[1]

        disparity_left_half = left_matcher.compute(left_image_down, right_image_down)
        disparity_right_half = right_matcher.compute(right_image_down, left_image_down)
        disparity_left = cv2.resize(disparity_left_half, size, interpolation=cv2.INTER_AREA)
        disparity_right = cv2.resize(disparity_right_half, size, interpolation=cv2.INTER_AREA)
        disparity_left = factor * disparity_left
        disparity_right = factor * disparity_right

    # 真实视差(因为SGBM算法得到的视差是×16的)
    trueDisp_left = disparity_left.astype(np.float32) / 16.
    trueDisp_right = disparity_right.astype(np.float32) / 16.

    return trueDisp_left, trueDisp_right

2.3 深度计算

得到视差图后,计算像素深度值,公式如下:
depth = ( f * baseline) / disp
其中,depth表示深度图;f表示归一化的焦距,也就是内参中的fx; baseline是两个相机光心之间的距离,称作基线距离;disp是视差值
实验直接利用opencv中的cv2.reprojectImageTo3D()函数计算深度图,代码如下

def getDepthMapWithQ(disparityMap: np.ndarray, Q: np.ndarray) -> np.ndarray:
    points_3d = cv2.reprojectImageTo3D(disparityMap, Q)
    depthMap = points_3d[:, :, 2]
    reset_index = np.where(np.logical_or(depthMap < 0.0, depthMap > 65535.0))
    depthMap[reset_index] = 0
    return depthMap.astype(np.float32)

3.完整代码

import sys
import cv2
import numpy as np
import stereoconfig


# 预处理
def preprocess(img1, img2):
    # 彩色图->灰度图
    if (img1.ndim == 3):  # 判断是否为三维数组
        img1 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)  # 通过OpenCV加载的图像通道顺序是BGR
    if (img2.ndim == 3):
        img2 = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY)

    # 直方图均衡
    img1 = cv2.equalizeHist(img1)
    img2 = cv2.equalizeHist(img2)

    return img1, img2


# 消除畸变
def undistortion(image, camera_matrix, dist_coeff):
    undistortion_image = cv2.undistort(image, camera_matrix, dist_coeff)

    return undistortion_image


# 获取畸变校正和立体校正的映射变换矩阵、重投影矩阵
# @param:config是一个类,存储着双目标定的参数:config = stereoconfig.stereoCamera()
def getRectifyTransform(height, width, config):
    # 读取内参和外参
    left_K = config.cam_matrix_left
    right_K = config.cam_matrix_right
    left_distortion = config.distortion_l
    right_distortion = config.distortion_r
    R = config.R
    T = config.T

    # 计算校正变换
    R1, R2, P1, P2, Q, roi1, roi2 = cv2.stereoRectify(left_K, left_distortion, right_K, right_distortion,
                                                      (width, height), R, T, alpha=0)

    map1x, map1y = cv2.initUndistortRectifyMap(left_K, left_distortion, R1, P1, (width, height), cv2.CV_32FC1)
    map2x, map2y = cv2.initUndistortRectifyMap(right_K, right_distortion, R2, P2, (width, height), cv2.CV_32FC1)

    return map1x, map1y, map2x, map2y, Q


# 畸变校正和立体校正
def rectifyImage(image1, image2, map1x, map1y, map2x, map2y):
    rectifyed_img1 = cv2.remap(image1, map1x, map1y, cv2.INTER_AREA)
    rectifyed_img2 = cv2.remap(image2, map2x, map2y, cv2.INTER_AREA)

    return rectifyed_img1, rectifyed_img2


# 立体校正检验----画线
def draw_line(image1, image2):
    # 建立输出图像
    height = max(image1.shape[0], image2.shape[0])
    width = image1.shape[1] + image2.shape[1]

    output = np.zeros((height, width, 3), dtype=np.uint8)
    output[0:image1.shape[0], 0:image1.shape[1]] = image1
    output[0:image2.shape[0], image1.shape[1]:] = image2

    # 绘制等间距平行线
    line_interval = 50  # 直线间隔:50
    for k in range(height // line_interval):
        cv2.line(output, (0, line_interval * (k + 1)), (2 * width, line_interval * (k + 1)), (0, 255, 0), thickness=2,
                 lineType=cv2.LINE_AA)

    return output


# 视差计算
def stereoMatchSGBM(left_image, right_image, down_scale=False):
    # SGBM匹配参数设置
    if left_image.ndim == 2:
        img_channels = 1
    else:
        img_channels = 3
    blockSize = 3
    paraml = {'minDisparity': 0,
              'numDisparities': 64,
              'blockSize': blockSize,
              'P1': 8 * img_channels * blockSize ** 2,
              'P2': 32 * img_channels * blockSize ** 2,
              'disp12MaxDiff': 1,
              'preFilterCap': 63,
              'uniquenessRatio': 15,
              'speckleWindowSize': 100,
              'speckleRange': 1,
              'mode': cv2.STEREO_SGBM_MODE_SGBM_3WAY
              }

    # 构建SGBM对象
    left_matcher = cv2.StereoSGBM_create(**paraml)
    paramr = paraml
    paramr['minDisparity'] = -paraml['numDisparities']
    right_matcher = cv2.StereoSGBM_create(**paramr)

    # 计算视差图
    size = (left_image.shape[1], left_image.shape[0])
    if down_scale == False:
        disparity_left = left_matcher.compute(left_image, right_image)
        disparity_right = right_matcher.compute(right_image, left_image)
    else:
        left_image_down = cv2.pyrDown(left_image)
        right_image_down = cv2.pyrDown(right_image)
        factor = left_image.shape[1] / left_image_down.shape[1]

        disparity_left_half = left_matcher.compute(left_image_down, right_image_down)
        disparity_right_half = right_matcher.compute(right_image_down, left_image_down)
        disparity_left = cv2.resize(disparity_left_half, size, interpolation=cv2.INTER_AREA)
        disparity_right = cv2.resize(disparity_right_half, size, interpolation=cv2.INTER_AREA)
        disparity_left = factor * disparity_left
        disparity_right = factor * disparity_right

    # 真实视差(因为SGBM算法得到的视差是×16的)
    trueDisp_left = disparity_left.astype(np.float32) / 16.
    trueDisp_right = disparity_right.astype(np.float32) / 16.

    return trueDisp_left, trueDisp_right


def getDepthMapWithQ(disparityMap: np.ndarray, Q: np.ndarray) -> np.ndarray:
    points_3d = cv2.reprojectImageTo3D(disparityMap, Q)  # points_3d 是一个三维的数组,前面两个是宽和高,第三维是一个(x,y,z)的坐标
    points = points_3d[:, :, 0:3]

    depthMap = points_3d[:, :, 2]  # 索引三维数组的最后一维,就是深度信息
    reset_index = np.where(np.logical_or(depthMap < 0.0, depthMap > 65535.0))
    depthMap[reset_index] = 0

    return depthMap.astype(np.float32)


if __name__ == '__main__':
    # 读取图片
    cap = cv2.VideoCapture(0)
    cap.set(3, 1280)
    cap.set(4, 480)  # 打开并设置摄像头

    while True:
        ret, frame = cap.read()
        iml = frame[0:480, 0:640]
        imr = frame[0:480, 640:1280]  # 分割双目图像


        if (iml is None) or (imr is None):
            print("Error: Images are empty, please check your image's path!")
            sys.exit(0)
        height, width = iml.shape[0:2]    # 对图像进行切片操作,前面两位是高和宽

        iml_, imr_ = preprocess(iml, imr)  # 预处理,一般可以削弱光照不均的影响,不做也可以

        # 读取相机内参和外参
        # 使用之前先将标定得到的内外参数填写到stereoconfig.py中的StereoCamera类中
        config = stereoconfig.stereoCamera()
        # print(config.cam_matrix_left)

        # 立体校正
        map1x, map1y, map2x, map2y, Q = getRectifyTransform(height, width, config)  # 获取用于畸变校正和立体校正的映射矩阵以及用于计算像素空间坐标的重投影矩阵
        iml_rectified, imr_rectified = rectifyImage(iml, imr, map1x, map1y, map2x, map2y)
        # print(Q)

        # 绘制等间距平行线,检查立体校正的效果
        line = draw_line(iml_rectified, imr_rectified)
        cv2.imwrite('check_rectification.png', line)

        # 立体匹配
        disp, _ = stereoMatchSGBM(iml_rectified, imr_rectified, False)  # 这里传入的是经立体校正的图像

        cv2.imwrite('disaprity.png', disp * 4)

        # fx = config.cam_matrix_left[0, 0]
        # fy = fx
        # cx = config.cam_matrix_left[0, 2]
        # cy = config.cam_matrix_left[1, 2]

        # print(fx, fy, cx, cy)

        # 计算像素点的3D坐标(左相机坐标系下)
        points_3d = cv2.reprojectImageTo3D(disp, Q)  # 参数中的Q就是由getRectifyTransform()函数得到的重投影矩阵



        # 设置想要检测的像素点坐标(x,y)
        x = 120
        y = 360
        cv2.circle(iml, (x, y), 5, (0, 0, 255), -1)

        # x1 = points_3d[y, x]   # 索引 (y, x) 对应的是三维坐标 (x1, y1, z1)
        # print(x1)

        print('x:', points_3d[int(y), int(x), 0], 'y:', points_3d[int(y), int(x), 1], 'z:',
              points_3d[int(y), int(x), 2])  # 得出像素点的三维坐标,单位mm
        print('distance:', (points_3d[int(y), int(x), 0] ** 2 + points_3d[int(y), int(x), 1] ** 2 + points_3d[
            int(y), int(x), 2] ** 2) ** 0.5)  # 计算距离,单位mm


        cv2.namedWindow("disparity", 0)
        cv2.imshow("disparity", iml)
        # cv2.setMouseCallback("disparity", onMouse,  0)


        # 等待用户按键,如果按下 'q' 键或者 Esc 键,则退出循环
        c = cv2.waitKey(1) & 0xFF
        if c == 27 or c == ord('q'):
            break

    # 释放视频对象并关闭窗口
    cap.release()
    cv2.destroyAllWindows()

猜你喜欢

转载自blog.csdn.net/weixin_45303602/article/details/133869770