Python+OpenCV实战:手把手教你搭建双目视觉系统(附完整代码)

一起搭建双目视觉系统(一) 双目视觉系统的搭建(一) 1.相机的标定 在搭建双目视觉系统之前首要的工作是进行相机的标定,相机的标定主要是有 阅读详情

从零到一:用Python和OpenCV构建你的双目视觉系统

最近在做一个机器人避障的小项目,需要让机器“看见”并理解周围环境的深度。市面上深度相机不少,但要么价格不菲,要么对环境光敏感。于是,我把目光投向了双目立体视觉——这个模拟人眼原理,用两个普通摄像头就能获取深度信息的技术。听起来很酷,对吧?但真正动手时,才发现从标定到重建,每一步都有不少“坑”。

这篇文章,就是我这段时间折腾的完整记录。它不是一篇理论综述,而是一份面向Python开发者的实战手册。我会带你一步步搭建一个可运行的双目视觉系统,重点不是复现论文,而是解决那些教程里很少提及的实操问题:比如标定板到底怎么摆拍效果最好?立体匹配算法参数调起来像玄学怎么办?生成的3D点云为什么全是噪点?我会把踩过的坑、试出来的有效参数,以及能直接跑起来的代码,毫无保留地分享给你。

无论你是想为机器人添加一双“慧眼”,还是对3D重建感兴趣,希望这篇文章能成为你上手路上的一块坚实垫脚石。

1. 环境搭建与数据准备:万事开头“细”

在敲下第一行代码前,把环境和数据准备好,能避免后面一大堆莫名其妙的错误。这里我推荐使用 Anaconda 来管理环境,它能很好地处理Python包依赖的“地狱”问题。

1.1 创建并配置专属环境

打开你的终端(或Anaconda Prompt),我们创建一个名为 stereo_vision 的独立环境,并安装核心依赖。

# 创建Python 3.9环境(3.8-3.11均可)
conda create -n stereo_vision python=3.9 -y

# 激活环境
conda activate stereo_vision

# 安装核心科学计算和视觉库
pip install opencv-contrib-python==4.8.1.78  # 包含主模块和contrib扩展
pip install numpy==1.24.3                     # 数值计算基础
pip install matplotlib==3.7.2                  # 用于可视化
pip install open3d==0.17.0                     # 强大的3D点云处理与可视化库

注意:安装 opencv-contrib-python 而非 opencv-python,因为一些高级立体视觉功能(如更丰富的标定选项)在contrib版本中。版本号尽量固定,避免新版本API变动导致代码报错。

安装完成后,可以写个简单的脚本来验证:

# test_env.py
import cv2
import numpy as np
print(f"OpenCV Version: {cv2.__version__}")
print(f"NumPy Version: {np.__version__}")
# 尝试导入Open3D,非必须但推荐
try:
    import open3d as o3d
    print("Open3D imported successfully.")
except ImportError as e:
    print(f"Open3D import failed: {e}")

1.2 获取与制作标定数据

双目系统的精度,很大程度上取决于相机标定的质量。而标定质量,又依赖于你采集的标定板图像。你需要一个棋盘格标定板。你可以自己打印,但更推荐使用表面覆亚光膜的标定板,减少反光。

采集图像时,务必牢记以下几点:

  • 数量与姿态:至少准备15-20对(左右相机各一张为一对)图像。标定板在画面中应呈现多种姿态:平放、倾斜、旋转、远近、占据画面不同位置。
  • 清晰与完整:确保棋盘格的所有内角点都清晰可见,没有模糊或遮挡。
  • 同步性:理想情况下,左右相机应同时拍摄。如果无法硬件同步,尽量保持标定板静止,快速连续拍摄左右图像。
  • 文件管理:建议建立清晰的目录结构,例如:
/stereo_calib_data
├── left/        # 存放左相机拍摄的所有标定图像 left_01.jpg, left_02.jpg...
├── right/       # 存放右相机拍摄的所有标定图像 right_01.jpg, right_02.jpg...
└── pairs.txt    # (可选)记录成对的图像文件名

我这里提供了一个脚本,可以快速预览并筛选出角点检测成功的图像对,避免无效数据进入标定流程。

# preview_calibration_images.py
import cv2
import numpy as np
import os
from glob import glob

def find_chessboard_corners(img_path, pattern_size=(9, 6)):
    """检测单张图像的棋盘格角点"""
    img = cv2.imread(img_path)
    if img is None:
        return False, None
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    # pattern_size 是内角点数量,即棋盘格内部黑白格子的交点数
    ret, corners = cv2.findChessboardCorners(gray, pattern_size, None)
    if ret:
        # 亚像素级角点精确化
        criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
        corners_refined = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria)
        return True, corners_refined
    return False, None

def main():
    left_imgs = sorted(glob('./stereo_calib_data/left/*.jpg'))
    right_imgs = sorted(glob('./stereo_calib_data/right/*.jpg'))
    pattern_size = (9, 6)  # 根据你的标定板修改

    valid_pairs = []
    for i, (l_path, r_path) in enumerate(zip(left_imgs, right_imgs)):
        l_found, l_corners = find_chessboard_corners(l_path, pattern_size)
        r_found, r_corners = find_chessboard_corners(r_path, pattern_size)

        if l_found and r_found:
            valid_pairs.append((l_path, r_path))
            print(f"Pair {i+1}: SUCCESS - {os.path.basename(l_path)} / {os.path.basename(r_path)}")
            # 可选:可视化角点
            # img_l = cv2.drawChessboardCorners(cv2.imread(l_path), pattern_size, l_corners, l_found)
            # img_r = cv2.drawChessboardCorners(cv2.imread(r_path), pattern_size, r_corners, r_found)
            # cv2.imshow('Left', img_l); cv2.imshow('Right', img_r)
            # if cv2.waitKey(500) & 0xFF == ord('q'): break
        else:
            print(f"Pair {i+1}: FAILED  - {os.path.basename(l_path)} / {os.path.basename(r_path)}")
            if not l_found:
                print(f"  -> Left image corners not found.")
            if not r_found:
                print(f"  -> Right image corners not found.")

    print(f"\nTotal valid pairs: {len(valid_pairs)}/{len(left_imgs)}")
    # 将有效的图像对保存下来,供后续标定使用
    with open('./stereo_calib_data/valid_pairs.txt', 'w') as f:
        for l, r in valid_pairs:
            f.write(f"{l},{r}\n")

if __name__ == '__main__':
    main()

2. 相机标定:为你的双目系统“测量视力”

标定,就是精确测量两个相机各自的“视力参数”(内参)以及它们之间的“相对位置”(外参)。这是所有后续步骤的基石,标定不准,三维重建的结果就会失真。

2.1 理解标定参数:内参与外参

在开始写代码前,花两分钟理解我们要获取的参数,这能帮你更好地解读结果和调试。

参数类型具体参数物理意义在OpenCV中的常见形态
内参 (Intrinsics)焦距 (fx, fy)相机镜头将3D点投影到2D图像平面的能力。fx, fy可能略有不同。3x3矩阵 K 的主对角线元素
主点 (cx, cy)图像平面的光学中心,通常是图像中心附近。3x3矩阵 K 的第三列前两个元素
畸变系数 (k1, k2, p1, p2, k3...)描述镜头引起的图像扭曲,如桶形畸变、枕形畸变。1x5或1x4的向量 dist
外参 (Extrinsics)旋转矩阵 (R)描述右相机坐标系相对于左相机坐标系的旋转。3x3矩阵
平移向量 (T)描述右相机原点相对于左相机原点的位置偏移。其模长即为双目基线距离。3x1向量

双目标定,就是同时求解左右相机的内参、畸变,以及它们之间的R和T。

2.2 实战标定代码与关键技巧

下面是我在实际项目中使用的标定代码,其中包含了一些提高标定精度的技巧。

# stereo_calibrate.py
import cv2
import numpy as np
import json
from pathlib import Path

class StereoCalibrator:
    def __init__(self, pattern_size=(9, 6), square_size=0.025):
        """
        初始化标定器
        :param pattern_size: 棋盘格内角点数量 (width, height)
        :param square_size: 每个棋盘格方块的物理尺寸(单位:米)
        """
        self.pattern_size = pattern_size
        self.square_size = square_size
        self.objp = None  # 3D世界坐标点
        self.img_size = None
        self._prepare_object_points()

    def _prepare_object_points(self):
        """生成棋盘格角点的世界坐标 (Z=0)"""
        objp = np.zeros((self.pattern_size[0] * self.pattern_size[1], 3), np.float32)
        # mgrid生成网格,乘以square_size得到实际物理坐标
        objp[:, :2] = np.mgrid[0:self.pattern_size[0], 0:self.pattern_size[1]].T.reshape(-1, 2)
        objp *= self.square_size
        self.objp = objp

    def calibrate(self, left_img_paths, right_img_paths):
        """
        执行双目标定
        :return: 标定结果字典
        """
        # 1. 角点检测与收集
        objpoints = []  # 世界坐标系中的3D点
        left_imgpoints = []
        right_imgpoints = []
        all_left_imgs = []
        all_right_imgs = []

        criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)

        print("Detecting chessboard corners...")
        for i, (l_path, r_path) in enumerate(zip(left_img_paths, right_img_paths)):
            l_img = cv2.imread(str(l_path))
            r_img = cv2.imread(str(r_path))
            if l_img is None or r_img is None:
                print(f"Warning: Cannot read image pair {i}")
                continue

            if self.img_size is None:
                self.img_size = (l_img.shape[1], l_img.shape[0])  # (width, height)
            elif self.img_size != (l_img.shape[1], l_img.shape[0]):
                raise ValueError(f"Image size mismatch at pair {i}")

            l_gray = cv2.cvtColor(l_img, cv2.COLOR_BGR2GRAY)
            r_gray = cv2.cvtColor(r_img, cv2.COLOR_BGR2GRAY)

            # 查找角点
            l_ret, l_corners = cv2.findChessboardCorners(l_gray, self.pattern_size, None)
            r_ret, r_corners = cv2.findChessboardCorners(r_gray, self.pattern_size, None)

            if l_ret and r_ret:
                # 亚像素精确化
                l_corners_refined = cv2.cornerSubPix(l_gray, l_corners, (11, 11), (-1, -1), criteria)
                r_corners_refined = cv2.cornerSubPix(r_gray, r_corners, (11, 11), (-1, -1), criteria)

                objpoints.append(self.objp)
                left_imgpoints.append(l_corners_refined)
                right_imgpoints.append(r_corners_refined)
                all_left_imgs.append(l_img)
                all_right_imgs.append(r_img)
                print(f"  Pair {i+1}: OK")
            else:
                print(f"  Pair {i+1}: Failed (L:{l_ret}, R:{r_ret})")

        if len(objpoints) < 10:
            raise ValueError(f"Only {len(objpoints)} valid pairs found. Need at least 10 for reliable calibration.")

        print(f"\nStarting calibration with {len(objpoints)} valid image pairs...")

        # 2. 单独标定左右相机(获取初始内参)
        print("Calibrating individual cameras...")
        rms_left, K_left, dist_left, _, _ = cv2.calibrateCamera(
            objpoints, left_imgpoints, self.img_size, None, None)
        rms_right, K_right, dist_right, _, _ = cv2.calibrateCamera(
            objpoints, right_imgpoints, self.img_size, None, None)

        print(f"  Left camera RMS error: {rms_left:.4f} pixels")
        print(f"  Right camera RMS error: {rms_right:.4f} pixels")

        # 3. 双目标定(优化所有参数)
        print("Performing stereo calibration...")
        flags = cv2.CALIB_FIX_INTRINSIC  # 使用单目标定的内参作为初始值,但允许微调
        # 也可以尝试 CALIB_USE_INTRINSIC_GUESS 让算法自己优化
        # flags = 0

        rms_stereo, K_left, dist_left, K_right, dist_right, R, T, E, F = cv2.stereoCalibrate(
            objpoints, left_imgpoints, right_imgpoints,
            K_left, dist_left, K_right, dist_right,
            self.img_size, criteria=criteria, flags=flags)

        print(f"  Stereo RMS error: {rms_stereo:.4f} pixels")
        print(f"  Rotation matrix (R):\n{R}")
        print(f"  Translation vector (T): {T.T} (Baseline: {np.linalg.norm(T):.4f} meters)")

        # 4. 计算重投影误差,评估标定质量
        mean_error = self._compute_reprojection_error(objpoints, left_imgpoints, right_imgpoints,
                                                       K_left, dist_left, K_right, dist_right, R, T)
        print(f"  Mean reprojection error: {mean_error:.4f} pixels")

        # 保存结果
        calib_result = {
            'img_size': self.img_size,
            'K_left': K_left.tolist(),
            'dist_left': dist_left.tolist(),
            'K_right': K_right.tolist(),
            'dist_right': dist_right.tolist(),
            'R': R.tolist(),
            'T': T.tolist(),
            'rms_stereo': rms_stereo,
            'baseline_m': float(np.linalg.norm(T))
        }
        return calib_result

    def _compute_reprojection_error(self, objpoints, imgpoints_l, imgpoints_r,
                                    K_l, dist_l, K_r, dist_r, R, T):
        """计算平均重投影误差,这是评估标定精度的关键指标"""
        total_error = 0
        total_points = 0
        for i in range(len(objpoints)):
            # 将世界点投影到左图像
            imgpoints_l_proj, _ = cv2.projectPoints(objpoints[i], np.eye(3), np.zeros((3,1)), K_l, dist_l)
            error_l = cv2.norm(imgpoints_l[i], imgpoints_l_proj, cv2.NORM_L2) / len(imgpoints_l_proj)
            # 将世界点投影到右图像(需要考虑旋转和平移)
            imgpoints_r_proj, _ = cv2.projectPoints(objpoints[i], R, T, K_r, dist_r)
            error_r = cv2.norm(imgpoints_r[i], imgpoints_r_proj, cv2.NORM_L2) / len(imgpoints_r_proj)
            total_error += (error_l + error_r)
            total_points += 2
        return total_error / total_points

# 使用示例
if __name__ == '__main__':
    data_dir = Path('./stereo_calib_data')
    left_paths = sorted(data_dir.glob('left/*.jpg'))[:20]  # 使用前20对
    right_paths = sorted(data_dir.glob('right/*.jpg'))[:20]

    calibrator = StereoCalibrator(pattern_size=(9, 6), square_size=0.025)
    result = calibrator.calibrate(left_paths, right_paths)

    # 保存标定结果
    with open('stereo_calibration_result.json', 'w') as f:
        json.dump(result, f, indent=2)
    print("\nCalibration results saved to 'stereo_calibration_result.json'")

运行这段代码后,重点关注几个输出:

  • RMS误差:理想情况下应小于0.5像素。如果大于1,可能需要检查标定板图像质量或增加有效图像对数量。
  • 平移向量T:其模长就是两个相机光心之间的距离,即基线长度。这个值应该和你实际测量的两个相机间距大致相符,是验证标定结果合理性的重要依据。
  • 重投影误差:这是更严格的精度指标,同样越小越好。

3. 立体校正与匹配:从二维图像中“抠”出深度

拿到准确的标定参数后,下一步就是处理实际拍摄的场景图。立体校正的目的是“摆正”两张图,让匹配搜索从二维降为一维,极大简化问题。立体匹配则是核心算法,目的是为每个像素找到其在另一张图中的对应点,计算视差。

3.1 立体校正:让图像行对齐

校正会生成两个映射表(map),后续只需用cv2.remap函数即可快速校正任何新图像。

# stereo_rectify.py
import cv2
import numpy as np
import json

class StereoRectifier:
    def __init__(self, calib_file='stereo_calibration_result.json'):
        with open(calib_file, 'r') as f:
            calib = json.load(f)
        self.img_size = tuple(calib['img_size'])
        self.K_left = np.array(calib['K_left'])
        self.dist_left = np.array(calib['dist_left'])
        self.K_right = np.array(calib['K_right'])
        self.dist_right = np.array(calib['dist_right'])
        self.R = np.array(calib['R'])
        self.T = np.array(calib['T'])
        self._compute_rectification_maps()

    def _compute_rectification_maps(self, alpha=0.0):
        """
        计算立体校正映射
        :param alpha: 缩放参数,0表示裁剪无效区域,1保留所有原图像素(可能有黑边)。
                      推荐-1,让OpenCV自动优化。
        """
        # 立体校正
        R1, R2, P1, P2, Q, roi1, roi2 = cv2.stereoRectify(
            self.K_left, self.dist_left,
            self.K_right, self.dist_right,
            self.img_size, self.R, self.T,
            alpha=alpha, flags=cv2.CALIB_ZERO_DISPARITY
        )
        # 计算映射
        self.map_left_x, self.map_left_y = cv2.initUndistortRectifyMap(
            self.K_left, self.dist_left, R1, P1, self.img_size, cv2.CV_32FC1)
        self.map_right_x, self.map_right_y = cv2.initUndistortRectifyMap(
            self.K_right, self.dist_right, R2, P2, self.img_size, cv2.CV_32FC1)
        self.Q = Q  # 视差转深度矩阵,用于后续3D重建
        self.roi_left = roi1
        self.roi_right = roi2

    def rectify(self, left_img, right_img):
        """校正一对图像"""
        left_rect = cv2.remap(left_img, self.map_left_x, self.map_left_y, cv2.INTER_LINEAR)
        right_rect = cv2.remap(right_img, self.map_right_x, self.map_right_y, cv2.INTER_LINEAR)
        return left_rect, right_rect

    def draw_epilines(self, left_rect, right_rect, num_lines=20):
        """绘制极线,可视化校正效果(校正后应为水平线)"""
        left_gray = cv2.cvtColor(left_rect, cv2.COLOR_BGR2GRAY)
        right_gray = cv2.cvtColor(right_rect, cv2.COLOR_BGR2GRAY)
        # 使用ORB等特征检测器找点
        orb = cv2.ORB_create()
        kp_left, des_left = orb.detectAndCompute(left_gray, None)
        # 匹配
        bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)
        kp_right, des_right = orb.detectAndCompute(right_gray, None)
        matches = bf.match(des_left, des_right)
        matches = sorted(matches, key=lambda x: x.distance)[:num_lines]

        # 画线
        left_vis = left_rect.copy()
        right_vis = right_rect.copy()
        for m in matches:
            pt_left = tuple(map(int, kp_left[m.queryIdx].pt))
            pt_right = tuple(map(int, kp_right[m.trainIdx].pt))
            color = tuple(np.random.randint(0, 255, 3).tolist())
            cv2.circle(left_vis, pt_left, 5, color, -1)
            cv2.circle(right_vis, pt_right, 5, color, -1)
            # 在右图对应行画水平线
            cv2.line(right_vis, (0, pt_right[1]), (right_vis.shape[1], pt_right[1]), color, 1)
        return cv2.hconcat([left_vis, right_vis])

# 使用示例
if __name__ == '__main__':
    rectifier = StereoRectifier()
    left_img = cv2.imread('scene_left.jpg')
    right_img = cv2.imread('scene_right.jpg')
    left_rect, right_rect = rectifier.rectify(left_img, right_img)
    # 可视化校正结果
    vis = rectifier.draw_epilines(left_rect, right_rect)
    cv2.imwrite('rectified_epilines.jpg', vis)
    print("Rectification complete. Check 'rectified_epilines.jpg'.")
    # 校正后,左右图中对应的特征点应该基本在同一水平线上。

3.2 立体匹配:算法选择与参数调优“心法”

立体匹配是双目视觉的灵魂,也是最耗计算资源的部分。OpenCV主要提供了两种算法:BM(Block Matching) 和 SGBM(Semi-Global Block Matching)。简单来说,BM快但精度低、噪声大;SGBM慢一些,但效果通常好得多,是实际应用的首选。

调参是个经验活,下面这个SGBM参数表是我经过大量测试总结的,可以作为你的起点:

参数含义推荐初始值/范围调整策略
minDisparity最小视差0如果物体都在远处,可设为正数。
numDisparities视差搜索范围必须是16的整数倍,如64, 128, 256越大,能探测的深度范围越广,但计算量也越大。根据基线长度和最近物体距离估算。
blockSize匹配块大小奇数,如3, 5, 7, 15小值对细节敏感但噪声大;大值平滑但边缘模糊。纹理丰富区域用小值,平滑区域用大值。
P1惩罚系数18*3*blockSize**2控制视差平滑度的参数。P1是相邻像素视差变化为1时的惩罚。
P2惩罚系数232*3*blockSize**2控制视差平滑度的参数。P2是相邻像素视差变化大于1时的惩罚。P2通常远大于P1。
disp12MaxDiff左右一致性检查最大容差1值越小,检查越严格,能消除一些错误匹配。
uniquenessRatio唯一性比率5-15值越大,对匹配唯一性要求越高,能过滤模糊匹配。
speckleWindowSize斑点滤波器窗口大小50-200过滤小的孤立噪点群。设为0禁用。
speckleRange斑点滤波器视差变化阈值1-2窗口内视差变化小于此值的区域被视为同一连通域。

下面是一个封装好的视差计算类,包含了BM和SGBM,并提供了视差图后处理(滤波)功能。

# disparity_engine.py
import cv2
import numpy as np

class DisparityEngine:
    def __init__(self, method='sgbm', **kwargs):
        self.method = method
        if method == 'bm':
            self.stereo = self._create_bm_matcher(**kwargs)
        elif method == 'sgbm':
            self.stereo = self._create_sgbm_matcher(**kwargs)
        else:
            raise ValueError("Method must be 'bm' or 'sgbm'")

    def _create_bm_matcher(self, num_disparities=128, block_size=15):
        """创建BM匹配器"""
        stereo = cv2.StereoBM_create(numDisparities=num_disparities, blockSize=block_size)
        # 可以设置一些预处理滤波器(OpenCV 4.x+)
        # stereo.setPreFilterType(1)
        # stereo.setPreFilterSize(5)
        # stereo.setPreFilterCap(31)
        return stereo

    def _create_sgbm_matcher(self, min_disparity=0, num_disparities=128, block_size=5,
                             p1=None, p2=None, disp12_max_diff=1,
                             uniqueness_ratio=10, speckle_window_size=100,
                             speckle_range=2, mode=cv2.STEREO_SGBM_MODE_SGBM_3WAY):
        """创建SGBM匹配器,参数设置更灵活"""
        if p1 is None:
            p1 = 8 * 3 * block_size ** 2
        if p2 is None:
            p2 = 32 * 3 * block_size ** 2

        stereo = cv2.StereoSGBM_create(
            minDisparity=min_disparity,
            numDisparities=num_disparities,
            blockSize=block_size,
            P1=p1,
            P2=p2,
            disp12MaxDiff=disp12_max_diff,
            uniquenessRatio=uniqueness_ratio,
            speckleWindowSize=speckle_window_size,
            speckleRange=speckle_range,
            mode=mode
        )
        return stereo

    def compute(self, left_img, right_img, apply_filter=True):
        """
        计算视差图
        :param apply_filter: 是否应用WLS滤波器进行后处理
        """
        if len(left_img.shape) == 3:
            left_gray = cv2.cvtColor(left_img, cv2.COLOR_BGR2GRAY)
            right_gray = cv2.cvtColor(right_img, cv2.COLOR_BGR2GRAY)
        else:
            left_gray = left_img
            right_gray = right_img

        # 计算原始视差
        if self.method == 'bm':
            disparity = self.stereo.compute(left_gray, right_gray)
        else:  # sgbm
            disparity = self.stereo.compute(left_gray, right_gray).astype(np.float32) / 16.0

        # 可选:WLS滤波,能显著平滑视差图并保持边缘
        if apply_filter and self.method == 'sgbm':
            # 创建右视差图(用于左右一致性检查)
            right_matcher = cv2.ximgproc.createRightMatcher(self.stereo)
            disparity_right = right_matcher.compute(right_gray, left_gray).astype(np.float32) / 16.0
            # 创建WLS滤波器
            wls_filter = cv2.ximgproc.createDisparityWLSFilter(self.stereo)
            wls_filter.setLambda(8000.0)  # 平滑度权重
            wls_filter.setSigmaColor(1.5) # 颜色相似度权重
            disparity = wls_filter.filter(disparity, left_img, disparity_map_right=disparity_right)

        # 将无效视差(如匹配失败)设为0
        disparity[disparity < 0] = 0

        return disparity

    def visualize_disparity(self, disparity, colormap=cv2.COLORMAP_JET):
        """将视差图转换为伪彩色图,便于观察"""
        disp_vis = disparity.copy()
        # 归一化到0-255范围
        if self.method == 'sgbm':
            # SGBM视差已经是浮点数,需要先归一化
            disp_min, disp_max = disparity.min(), disparity.max()
            if disp_max > disp_min:
                disp_vis = (255 * (disparity - disp_min) / (disp_max - disp_min)).astype(np.uint8)
            else:
                disp_vis = np.zeros_like(disparity, dtype=np.uint8)
        else:
            # BM视差是整数,通常需要除以16(但compute里已处理)
            disp_vis = np.uint8(disparity)

        disp_color = cv2.applyColorMap(disp_vis, colormap)
        return disp_color

# 使用示例
if __name__ == '__main__':
    # 加载校正后的图像
    left_rect = cv2.imread('rectified_left.jpg')
    right_rect = cv2.imread('rectified_right.jpg')

    # 尝试SGBM
    engine_sgbm = DisparityEngine(method='sgbm', num_disparities=128, block_size=5)
    disparity_sgbm = engine_sgbm.compute(left_rect, right_rect, apply_filter=True)
    disparity_color_sgbm = engine_sgbm.visualize_disparity(disparity_sgbm)

    # 尝试BM(对比用)
    engine_bm = DisparityEngine(method='bm', num_disparities=128, block_size=15)
    disparity_bm = engine_bm.compute(left_rect, right_rect, apply_filter=False)
    disparity_color_bm = engine_bm.visualize_disparity(disparity_bm)

    # 并排显示
    comparison = np.hstack([disparity_color_bm, disparity_color_sgbm])
    cv2.imwrite('disparity_comparison.jpg', comparison)
    print("Disparity maps computed. 'disparity_comparison.jpg' shows BM (left) vs SGBM+WLS (right).")

运行后,你会看到BM生成的视差图噪点很多,而SGBM结合WLS滤波后的结果则平滑、连贯得多。在实际项目中,我几乎总是使用SGBM。

4. 三维重建与应用:从视差到真实世界

得到视差图后,我们离最终目标——三维点云,只有一步之遥。这一步在数学上是直接的,但如何让点云更干净、更有用,则需要一些技巧。

4.1 生成与优化点云

直接根据视差图和Q矩阵重投影得到的点云往往包含大量噪点(来自误匹配、图像边缘、无纹理区域)。我们需要进行过滤。

# pointcloud_builder.py
import cv2
import numpy as np
import open3d as o3d

class PointCloudBuilder:
    def __init__(self, Q_matrix):
        """
        :param Q_matrix: 从立体校正得到的重投影矩阵
        """
        self.Q = Q_matrix

    def reproject_to_3d(self, disparity, left_img):
        """
        将视差图重投影为3D点云
        :return: 点云坐标 (N, 3), 颜色 (N, 3)
        """
        # 使用reprojectImageTo3D函数进行重投影
        points_3d = cv2.reprojectImageTo3D(disparity, self.Q)
        # 创建掩码,过滤无效视差(通常视差为0或极小值为无效)
        mask = disparity > disparity.min() + 1  # 简单的阈值过滤
        # 应用掩码
        points = points_3d[mask]
        colors = left_img[mask]
        # 转换颜色顺序 BGR -> RGB (如果使用Open3D可视化)
        colors_rgb = colors[:, :, ::-1] / 255.0
        return points, colors_rgb

    def filter_pointcloud(self, points, colors, 
                          z_range=(0.1, 10.0), 
                          outlier_std_ratio=2.0):
        """
        过滤点云:去除太近/太远的点,并移除统计离群点
        """
        # 1. 基于深度(Z)的过滤
        z = points[:, 2]
        depth_mask = (z > z_range[0]) & (z < z_range[1])
        points = points[depth_mask]
        colors = colors[depth_mask]

        if len(points) == 0:
            return points, colors

        # 2. 统计离群点移除 (移除远离点云主体的孤立点)
        pcd = o3d.geometry.PointCloud()
        pcd.points = o3d.utility.Vector3dVector(points)
        pcd.colors = o3d.utility.Vector3dVector(colors)

        # 使用统计离群点移除
        cl, ind = pcd.remove_statistical_outlier(nb_neighbors=20, std_ratio=outlier_std_ratio)
        filtered_pcd = pcd.select_by_index(ind)

        return np.asarray(filtered_pcd.points), np.asarray(filtered_pcd.colors)

    def create_open3d_visualizer(self, points, colors, window_name="3D Point Cloud"):
        """创建并返回一个Open3D可视化窗口对象"""
        pcd = o3d.geometry.PointCloud()
        pcd.points = o3d.utility.Vector3dVector(points)
        pcd.colors = o3d.utility.Vector3dVector(colors)
        # 可选:下采样,使可视化更流畅
        if len(points) > 50000:
            pcd = pcd.voxel_down_sample(voxel_size=0.01)
        # 计算法线,用于渲染着色(非必须,但看起来更立体)
        pcd.estimate_normals(search_param=o3d.geometry.KDTreeSearchParamHybrid(radius=0.1, max_nn=30))
        return pcd

# 主流程示例
if __name__ == '__main__':
    # 加载数据
    left_img = cv2.imread('rectified_left.jpg')
    disparity = np.load('disparity_sgbm.npy')  # 假设之前保存了视差图
    # 加载Q矩阵 (从标定结果或rectifier对象获取)
    # 这里假设我们从rectifier对象获取
    from stereo_rectify import StereoRectifier
    rectifier = StereoRectifier()
    Q = rectifier.Q

    # 重建点云
    builder = PointCloudBuilder(Q)
    raw_points, raw_colors = builder.reproject_to_3d(disparity, left_img)
    print(f"Raw point cloud size: {len(raw_points)}")

    # 过滤点云
    filtered_points, filtered_colors = builder.filter_pointcloud(
        raw_points, raw_colors, z_range=(0.3, 5.0))
    print(f"Filtered point cloud size: {len(filtered_points)}")

    # 可视化
    pcd = builder.create_open3d_visualizer(filtered_points, filtered_colors)
    o3d.visualization.draw_geometries([pcd], window_name="Reconstructed Scene")

    # 保存点云为PLY格式,可用于MeshLab、CloudCompare等软件进一步处理
    o3d.io.write_point_cloud("reconstructed_scene.ply", pcd)
    print("Point cloud saved as 'reconstructed_scene.ply'")

4.2 实际应用场景与性能考量

一个能跑通的demo只是开始。要让双目视觉系统在实际项目中发挥作用,你需要考虑更多。

1. 实时性优化 如果你的应用需要实时处理(如机器人SLAM、交互式AR),纯Python+OpenCV的SGBM可能难以达到高帧率。可以考虑以下策略:

  • 降低分辨率:将输入图像缩放至原图的1/2或1/4,能极大提升速度,但会损失深度精度和有效距离。
  • 调整视差范围:精确估算场景的最近和最远距离,减少numDisparities。
  • 使用硬件加速:OpenCV部分函数支持OpenCL或CUDA。可以尝试编译支持GPU的OpenCV版本,或探索专用硬件(如Intel RealSense SDK、NVIDIA的VisionWorks库)。
  • 算法替代:对于特定场景(如室内结构化环境),可以考虑更快的局部匹配算法,或使用基于深度学习的轻量级立体匹配网络(如AnyNet、StereoNet)。

2. 精度提升技巧

  • 高分辨率标定:使用更高分辨率的图像进行标定,并确保标定板覆盖整个视野。
  • 亚像素优化:在立体匹配后,对视差图进行亚像素级优化(cv2.ximgproc.disparityRefinement)。
  • 多帧融合:对于静态场景,可以采集多帧图像,计算视差后取中值或均值,能有效抑制随机噪声。
  • 光照鲁棒性:确保左右相机曝光一致。如果环境光变化大,可以考虑使用归一化互相关(ZNCC)等对光照变化不敏感的匹配代价函数,但OpenCV的SGBM默认使用BT代价,对此有一定容忍度。

3. 常见问题与排查清单 当你发现三维重建结果不理想时,可以按以下清单排查:

问题现象可能原因排查与解决思路
点云整体扭曲或拉伸相机标定不准,特别是基线长度T测量不准。重新标定,确保标定板多角度、全覆盖。检查标定重投影误差。
点云在物体边缘出现“膨胀”或“拖尾”立体匹配在遮挡区域或深度不连续处出错。尝试调整SGBM的P1、P2平滑惩罚参数。使用WLS后处理滤波器。
点云噪声大,像“雪花”图像纹理弱、噪声多,或匹配算法参数不当。增加blockSize。尝试speckleWindowSize和speckleRange滤波。确保输入图像清晰、纹理丰富。
重建的物体表面凹凸不平镜头畸变校正不彻底,或相机同步不好导致运动模糊。检查标定时的畸变系数是否合理。确保拍摄时相机与场景无相对运动。
视差图有大片黑色区域(无效点)该区域在另一视图中被遮挡,或纹理重复导致匹配失败。这是双目视觉的固有难题。可尝试用图像修复(inpainting)填补,或通过后处理插值。

最后,分享一个我最近在室内机器人项目中的小经验:对于地面平坦的室内场景,在得到点云后,可以先用RANSAC算法拟合一个地平面,然后把地面点云滤除。这样剩下的就是墙壁、家具等障碍物,直接用于机器人路径规划,效果非常直接。实现这个功能的代码片段如下:

def segment_ground_plane(points, colors, distance_threshold=0.02, max_iterations=1000):
    """使用RANSAC从点云中分割地面"""
    pcd = o3d.geometry.PointCloud()
    pcd.points = o3d.utility.Vector3dVector(points)
    pcd.colors = o3d.utility.Vector3dVector(colors)

    # 使用RANSAC拟合平面
    plane_model, inliers = pcd.segment_plane(distance_threshold=distance_threshold,
                                             ransac_n=3,
                                             num_iterations=max_iterations)
    [a, b, c, d] = plane_model
    print(f"Plane equation: {a:.2f}x + {b:.2f}y + {c:.2f}z + {d:.2f} = 0")
    print(f"Number of inliers (ground points): {len(inliers)}")

    # 分割点云
    ground = pcd.select_by_index(inliers)
    obstacles = pcd.select_by_index(inliers, invert=True)

    # 给地面点云上色(绿色)以便可视化
    ground.paint_uniform_color([0.0, 1.0, 0.0]) 
    return np.asarray(ground.points), np.asarray(obstacles.points), np.asarray(obstacles.colors)

把这个函数接入到前面的点云处理流程中,你就能得到一个分离了地面和障碍物的点云,离真正的应用又近了一步。双目视觉的乐趣就在于,从两个二维的图像矩阵中,通过一系列计算,竟然能还原出我们所在的三维世界。这个过程虽然繁琐,但当看到第一个粗糙但可辨的点云出现时,那种成就感是无可替代的。希望这份指南能帮你少走些弯路,更快地体验到这种乐趣。如果在实践过程中遇到具体问题,不妨从标定数据和质量这个根源再仔细检查一遍,很多时候问题就出在那里。

双目视觉实战:如何用OpenCV和Python实现简易3D建模(附完整代码) 本文详细介绍了如何使用OpenCV和Python实现双目视觉技术进行简易3D建模,涵盖从摄像头标定到3D点云生成的全流程。通过实战案例和完整代码,帮助开发者快速掌握双目视觉和三维建模的核心技术,适用于计算机视觉入门和创意验证。 阅读详情

相关推荐

Python+OpenCV实战:用手机搭建简易双目视觉系统(附完整代码)

本文详细介绍了如何利用两部普通智能手机和Python的OpenCV库,低成本搭建一个简易的双目视觉系统。内容涵盖从硬件选择、相机标定、立体校正到深度图生成与三维重建的完整流程,并提供了完整的实战代码。重点解决了手机摄像头标定、图像同步等实际问题,帮助初学者快速入门双目视觉与三维重建技术。

river的博客 189

用Python和OpenCV从零搭建一个完整的双目视觉系统

完整项目可参考GitHub上的开源双目视觉项目,如。

Made In SQL 1464

5个实战案例教你用Python+OpenCV搞定双目视觉测距(附完整代码)

本文通过5个实战案例详细讲解如何使用Python和OpenCV实现双目视觉测距技术,涵盖摄像头标定、视差估计、深度计算等核心步骤,并提供完整代码。特别介绍双目匹配和视差估计等关键技术,适用于机器人导航、工业检测等场景。

weixin_28222493的博客 190

Python+OpenCV实战:用手机搭建双目视觉系统(附完整代码)

本文详细介绍了如何使用Python和OpenCV搭建低成本的双目视觉系统,包括硬件准备、相机标定、图像校正和三维重建等关键步骤。通过模拟人类双眼视差原理,从两部智能手机拍摄的图像中提取三维信息,实现三维重建。附完整代码示例,帮助开发者快速掌握双目视觉技术。

nokia的博客 507

基于opencv的双目测量系统详细设计与具体代码实现

基于 OpenCV 的双目测量系统详细设计与具体代码实现 1. 背景介绍 1.1 问题的由来 在三维视觉领域,如何获取物体的深度信息一直是一个重要的研究方向。传统的单目视觉系统只能获取二维图像信息,无法直接获取物体的深度

AI架构师小马 1232

基于Python与OpenCV的双目视觉测距:从原理到实战完整指南

计算机视觉中的立体视觉技术,其核心原理源于三角测量法,通过模拟人眼视差来感知三维空间信息。该技术的关键在于利用两个相机从不同视角捕获图像,通过计算对应像素点的位置差异(即视差),并结合相机标定获得的焦距与基线距离,最终解算出物体的深度。这项技术的工程价值在于,它提供了一种低成本、非接触式的三维感知方案,广泛应用于机器人导航、自动驾驶、三维重建、体积测量及增强现实等领域。本文以双目视觉测距为主题,详细拆解了基于Python和OpenCV的完整实现流程,涵盖了相机标定、立体校正、立体匹配(如SGBM算法)等核心

weixin_34079177的博客 338

用Python和OpenCV从零搭建一个完整的双目视觉系统(一)

在本系列后续的文章中,我们将对上述每一个阶段进行深入的技术剖析与代码实现。如何进行一次高精度的相机标定,并解读其结果。如何使用SGBM 算法并调优参数,以获得高质量的视差图。如何最终将二维像素数据,变成一个可以在屏幕上自由旋转的、可视化的三维点云。准备好相应的 Python 开发环境。下一篇文章正式进入编码阶段,从搭建一个专业、可扩展的项目框架开始。

qq_36497369的博客 1373

金蝶云ERP远程办公指南:零公网IP实现安全内网穿透访问

本文详细介绍了如何利用内网穿透技术,在无需公网IP和复杂路由器设置的情况下,安全远程访问内网部署的金蝶云ERP系统。通过使用cpolar工具,逐步指导用户创建加密隧道、配置固定域名,并实现管理中心与数据中心的远程登录,有效解决了企业员工在外出或居家办公时的核心业务系统访问难题。

weixin_29229261的博客 264

用Python和OpenCV从零搭建一个完整的双目视觉系统(三)

本系列文章旨在系统性地阐述如何利用 Python 与 OpenCV 库,从零开始构建一个完整的双目立体视觉系统。在上一篇文章中,我们为项目设计了清晰的架构。现在,我们将深入第一个,也是整个双目视觉系统的模块——。如果说双目视觉系统是一座高楼,那么标定就是它的地基。一个不准确、不稳定的标定结果,会使后续所有的计算(立体匹配、三维重建)都失去意义,最终导致整个系统的崩塌。无论你的匹配算法多么先进,都无法弥补一个错误的地基。

qq_36497369的博客 1854

ESP32S3与ILI9341 LCD屏LVGL移植实战:从零构建嵌入式GUI界面

本文详细介绍了如何在ESP32S3平台上移植LVGL图形库并驱动ILI9341 LCD屏幕,从硬件选型、开发环境配置到显示驱动实现和内存优化。通过实战案例讲解移植过程中的关键步骤和常见问题解决方案,帮助开发者快速构建嵌入式GUI界面。

h9j8k7l6m5n的博客 1090

读书笔记:深度学习入门-基于python的理论与实现(俗称鱼书)

终于从头到尾把鱼书看完啦,真的是非常入门,非常查漏补缺,对新手很友好,对有一定基础的人也非常不错。看完不复习等于没看,所以今天开始复盘,顺便写这篇读书笔记。希望分享给没看过这本书或者看过这本书的同僚们参考,欢迎大家指教。我将从第三章神经网络开始复习,前两章内容书里已经非常基础and清楚了。(会持续更新滴).........

博客而已 1万+

球体的双目视觉定位(matlab,附代码)

球体的双目视觉定位(matlab,附代码) 标签(空格分隔): 机器视觉 引言 双目视觉定位是我们的一个课程设计,最近刚做完,拿出来与大家分享一下,实验的目的是在拍摄的照片中识别球体,并求出该球体到相机的实际距离吗,我们要求需要用matlab,但是matlab调用双目摄像头(一个USB口)却老是只能调用双目摄像头中的一个,但是利用Python的OpenCV库却可以同时调用两个,因此我们...

weixin_30901729的博客 3836

华为昇腾310P3+deepseek蒸馏版性能调优:FP16精度优化全记录

本文详细介绍了在华为昇腾310P3硬件环境下优化DeepSeek蒸馏版模型FP16精度的关键技术路径。通过分层量化策略、内存分配优化和并行计算配置,显著提升模型推理性能,实现83%的并发处理能力提升,同时保持98.7%的原始模型准确率。

weixin_29074409的博客 340

用Python和OpenCV从零搭建一个完整的双目视觉系统(四)

本系列文章旨在系统性地阐述如何利用 Python 与 OpenCV 库,从零开始构建一个完整的双目立体视觉系统。

qq_36497369的博客 1978

软件测试面试题汇总

软件测试面试题汇总 测试技术面试题 ........................................................................................................................................................................ 5 1、什么是兼容

晴天的专栏 2万+

9、自动定理证明的发展历程与关键技术

本文全面介绍了自动定理证明的发展历程、关键技术及其实际应用,包括消解原理、参数化、支撑集策略等核心方法,并探讨了算法优化、数据结构优化及并行计算等提升效率的手段。同时,文章展望了未来发展方向,如深度学习在定理证明中的应用、交互式定理证明以及高阶逻辑的研究前景。

ll5678的博客 501

用Python和OpenCV从零搭建一个完整的双目视觉系统(二)

通过这样一种模块化的架构设计,我们将一个复杂的大问题,分解成了一系列职责单一、易于管理的小问题。每个模块都可以被独立地开发、测试和改进。在下一篇文章中,我们将正式深入第一个核心算法模块——,剖析相机标定的每一个技术细节和代码实现。

qq_36497369的博客 1248

若依开源系统多数据源整合clickhouse数据库详细步骤

若依开源系统多数据源整合clickhouse数据库详细步骤

java之书-资源 892

双目视觉目标追踪及三维坐标获取—python(代码)

通过双目相机实现了物体的目标追踪及三维坐标获取—python(代码)

weixin_51972728的博客 3万+

【项目实战】自动化测试软件VectorCAST介绍

VectorCAST是一款先进的自动化测试软件。VectorCAST是一款功能强大、易于使用的自动化测试软件。VectorCAST适用于各种嵌入式系统的测试。VectorCAST为嵌入式系统的测试提供了一种高效、可靠的解决方案。VectorCAST提供了一整套完整的测试工具,包括测试管理、代码覆盖、缺陷定位、报告生成等,以帮助开发人员提高测试效率,确保软件质量。VectorCAST支持。

奶爸的编程之路,也就一周冷个三天 2003

霍尼韦尔SoftMaster使用说明

霍尼韦尔SoftMaster使用说明

Python基于OpenCV的双目视觉深度图算法(源码&教程)

Python基于OpenCV的双目视觉深度图算法(源码&教程)

cheng2333333的博客 3311

Camera2Demo

在Camera2 的基础上进行封装,使得Camera2一堆初始化逻辑与Activity分离,降低耦合度,完成预览拍照并保存的操作

用Python和OpenCV从零搭建一个完整的双目视觉系统(五)

本系列文章旨在系统性地阐述如何利用 Python 与 OpenCV 库,从零开始构建一个完整的双目立体视觉系统。

qq_36497369的博客 1745

锂电池充电器电路设计 里面有电路

锂电池充电器电路设计 里面有电路 3.7V升压到5V电路设计。

Devexpress16.2.6破解工具

Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具

用Python和OpenCV从零搭建一个完整的双目视觉系统(六 最终篇)

本系列文章旨在系统性地阐述如何利用 Python 与 OpenCV 库,从零开始构建一个完整的双目立体视觉系统。

qq_36497369的博客 1891

基于python与OpenCV的双目视觉匹配测距系统设计与实现

本文基于Python与OpenCV技术,设计并实现了一个双目视觉匹配测距系统。双目视觉系统在三维重建和距离测量方面具有广泛应用。首先,通过双目相机采集左右眼图像,并进行图像去畸变和校正处理。然后,利用OpenCV库中的立体匹配算法,对左右眼图像进行特征提取和匹配,得到视差图。接下来,根据视差和基线长度构建视差-距离模型,实现图像距离到实际距离的转换。本文基于双目匹配算法改进了SGBM算法,提高了匹配效果和测距的准确性。

No_Name_Cao_Ni_Mei的博客 673
上一篇: RK3576开发板ROS2部署实战:从Buildroot配置到Hello World Demo
下一篇: 高德地图API实战:5分钟搞定两点距离计算(附完整代码示例)
mac99
博客等级 码龄17年 114粉丝 1444原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值