【ZED】深入解析ZED相机标定:从张氏标定法到实战优化

【ZED】从零开始使用ZED相机(三):相机标定(张氏标定法) 引言 关于ZED相机的安装和配置可以先参考【ZED】从零开始使用ZED相机(一):windows下的安装配置与测试 关于ZED相机的基本使用可以参考【ZED】从零开始使用ZED相机(二):打开相机+捕获图像 接下来介绍基于ZED相机的SDK实现相机的标定,标定的原理基于张氏标定法,算法这里不单独介绍,推荐讲解的链接立体视觉入门指南(3):相机标定之张式标定法【超详细值得收藏】 1 获取棋盘格图像 首先打印棋盘格图案,笔者打印的是7x9的棋盘格,内角点数为6x8 笔者将ZED可能调用的函数封装成一个类,初始 阅读详情

1. 为什么你的ZED相机需要标定?从“感觉不准”到“精准测量”

如果你刚拿到ZED相机,兴冲冲地用它来测距或者做三维重建,结果发现测出来的距离和实际差了好几厘米,或者拼接出来的模型歪歪扭扭,别急着怀疑硬件坏了。十有八九,问题出在相机标定这一步没做好。我刚开始玩ZED的时候也踩过这个坑,以为出厂设置就万事大吉,结果在做一个机械臂抓取项目时,因为深度误差导致定位失败,白白折腾了好几天。

简单来说,相机标定就是给相机做一次“体检”和“身份认证”。它要解决两个核心问题:第一,确定相机自身的“性格参数”(内参);第二,确定相机在空间中的“位置和姿态”(外参)。你可以把相机想象成人的眼睛。每个人的眼球形状、晶状体焦距都略有不同(这就是内参),而且你观察世界时,头是正着看还是歪着看,站得近还是站得远(这就是外参),都会影响你判断物体的位置和大小。相机标定,就是通过一套数学方法,把这些“个性”和“状态”精确地测量出来。

对于ZED这类双目立体相机,标定尤为重要。因为它依赖左右两个镜头看到的画面差异(视差)来计算深度。如果两个镜头的内参不准,或者它们之间的相对位置(外参)测量有误,那么计算出的视差就会出错,深度信息自然也就不准了。这就像你用两只眼睛看东西,如果一只眼睛近视度数没配准,或者两眼斜视,你判断距离的能力就会大打折扣。

所以,无论你是做机器人导航、三维扫描、体积测量还是AR/VR应用,精准的标定都是后续所有工作的基石。没有准确的标定,再高级的算法也是“巧妇难为无米之炊”。接下来,我们就从最经典、最实用的张氏标定法开始,一步步拆解ZED相机的标定全过程。

2. 张氏标定法:化繁为简的“棋盘格魔法”

提到相机标定,就绕不开张正友博士在1998年提出的“张氏标定法”。这个方法之所以经典,是因为它用一个简单巧妙的思路,解决了复杂的参数求解问题,让高精度的相机标定走出了实验室,变成了普通开发者也能轻松上手的技术。

它的核心道具是一张棋盘格图案。你肯定见过,就是那种黑白相间的方格图。这个方法妙在哪呢?它不需要你知道这个棋盘格在真实世界里精确的物理尺寸(比如每个格子必须是10.000毫米),只需要你知道它有多少行、多少列(比如内角点是6x8)。棋盘格提供了一个已知的、规则的三维点阵(所有角点都在一个平面上,且间距均匀),相机从不同角度去拍摄它,就能建立起大量的二维图像点与三维空间点的对应关系。

整个标定的数学过程,可以理解为层层递进的四步:

  1. 求解单应性矩阵(Homography):这是第一步,也是最关键的一步。对于每一张拍摄的棋盘格图片,我们都要计算一个单应性矩阵。这个矩阵描述的是棋盘格平面(真实世界中的那个平面)和相机成像平面(照片)之间的映射关系。你可以把它想象成一个“透视变换”的公式,能把棋盘格从三维空间“拍扁”到二维照片上。通过多张不同角度的图片,我们就能得到多个这样的映射关系。

  2. 求解相机内参矩阵(Intrinsics):有了多个单应性矩阵,我们就可以利用一些约束条件(比如旋转矩阵的正交性),反推出相机的内部参数。这个内参矩阵通常长这样:

    [fx,  s, cx]
    [0,  fy, cy]
    [0,  0,  1]
    

    其中,fx和fy是焦距(以像素为单位),(cx, cy)是图像的主点(通常接近图像中心),s是倾斜系数(现代相机通常为0)。对于ZED相机,左右两个镜头需要分别标定出各自的内参矩阵。

  3. 求解相机外参矩阵(Extrinsics):内参知道了,单应性矩阵也知道了,那么对于每一张图片,相机相对于那个棋盘格的位置和朝向(即外参矩阵 [R|t],R是旋转,t是平移)也就能算出来了。这告诉我们,拍摄每张照片时,相机是从哪个角度、哪个位置看棋盘格的。

  4. 求解畸变系数并全局优化:镜头不是完美的,尤其是广角镜头,拍出来的图像边缘的直线会变弯,这就是畸变。张氏标定法最后一步就是估算径向畸变系数(通常是k1, k2,严重的还会用k3),然后用最大似然估计或Levenberg-Marquardt算法,把内参、外参、畸变系数放在一起,进行一轮全局的精细优化,使得所有棋盘格角点的投影位置和实际检测到的图像位置之间的总误差最小。

这个过程听起来复杂,但幸运的是,OpenCV这样的库已经把绝大多数步骤封装好了。我们作为使用者,核心任务就是:拍好棋盘格照片,调用正确的函数,然后解读结果。下面,我们就进入实战环节。

3. 实战第一步:准备高质量的标定图像

标定结果的好坏,七八成取决于你采集的图像质量。这一步做不好,后面算法再厉害也白搭。根据我的经验,很多新手标定误差大,问题都出在图像采集环节。

首先,制作或打印你的棋盘格。 我推荐使用不对称的圆圈网格,因为OpenCV检测它的角点(圆心)比检测传统棋盘格的交点更稳定、更精确。当然,传统的棋盘格也完全没问题。关键是要平整!不要贴在软趴趴的纸板上,最好用亚克力板或者裱在硬卡纸上。尺寸上,每个格子的大小没有绝对要求,但建议让棋盘格在图像中占据足够的面积,比如占画面宽度的1/3到1/2。我常用的是7x9的网格(内角点数为6x8),这个大小比较适中。

接下来,用ZED相机采集图像。 这里有几个至关重要的技巧:

  • 多角度、多位置:这是最重要的原则。不要只在一个位置拍,要像用手机拍一个物体一样,上下左右、远近倾斜、正对侧对,各个角度都拍一些。想象用棋盘格“包裹”住相机的视野。我一般会拍15到25张,太少的话参数估计可能不稳定,太多也没必要,反而增加计算量。
  • 保证清晰度:确保棋盘格在每一张图片里都是清晰的,没有运动模糊。光线要均匀,避免棋盘格上有强烈反光或阴影。ZED相机在室内普通光照下就能工作得很好。
  • 覆盖整个画面:特别要拍一些让棋盘格充满整个画面边缘的图片。因为畸变在图像边缘最明显,这些图片对于准确估计畸变系数至关重要。
  • 左右镜头同步:ZED是双目相机,我们需要同时获取左右镜头的图像进行分别标定。下面的代码示例展示了如何用ZED Python API捕获并保存左右图像。
import pyzed.sl as sl
import cv2
import numpy as np
import os
import time

class ZEDCalibrator:
    def __init__(self, resolution='HD1080', fps=30):
        self.zed = sl.Camera()
        self.init_params = sl.InitParameters()
        self.init_params.camera_resolution = getattr(sl.RESOLUTION, resolution)
        self.init_params.camera_fps = fps
        self.init_params.depth_mode = sl.DEPTH_MODE.NONE  # 标定时不需要深度模式

        # 打开相机
        err = self.zed.open(self.init_params)
        if err != sl.ERROR_CODE.SUCCESS:
            print(f"相机打开失败: {err}")
            exit(1)

        self.runtime = sl.RuntimeParameters()
        self.image_left = sl.Mat()
        self.image_right = sl.Mat()
        self.save_dir = ""

    def capture_calibration_images(self, num_target=20):
        """捕获并保存用于标定的左右图像"""
        # 创建带有时间戳的保存目录
        timestamp = time.strftime("%Y%m%d_%H%M%S")
        self.save_dir = f"./calibration_images_{timestamp}"
        os.makedirs(self.save_dir, exist_ok=True)
        left_dir = os.path.join(self.save_dir, "left")
        right_dir = os.path.join(self.save_dir, "right")
        os.makedirs(left_dir, exist_ok=True)
        os.makedirs(right_dir, exist_ok=True)

        print(f"图像将保存至: {self.save_dir}")
        print("按 's' 保存当前帧,按 'ESC' 退出采集。")

        captured_count = 0
        while captured_count < num_target:
            if self.zed.grab(self.runtime) == sl.ERROR_CODE.SUCCESS:
                # 获取左右图像
                self.zed.retrieve_image(self.image_left, sl.VIEW.LEFT)
                self.zed.retrieve_image(self.image_right, sl.VIEW.RIGHT)

                left_frame = self.image_left.get_data()
                right_frame = self.image_right.get_data()

                # 并排显示
                display_frame = np.hstack((left_frame, right_frame))
                cv2.imshow('ZED - 按S保存,ESC退出', cv2.resize(display_frame, (1280, 360)))

                key = cv2.waitKey(5)
                if key == 27:  # ESC
                    break
                elif key == ord('s') or key == ord('S'):
                    # 保存左右图像
                    left_path = os.path.join(left_dir, f"left_{captured_count:03d}.png")
                    right_path = os.path.join(right_dir, f"right_{captured_count:03d}.png")
                    cv2.imwrite(left_path, left_frame)
                    cv2.imwrite(right_path, right_frame)
                    print(f"已保存图像对: {captured_count:03d}")
                    captured_count += 1

        cv2.destroyAllWindows()
        self.zed.close()
        print(f"采集完成,共保存 {captured_count} 对图像。")
        return self.save_dir

if __name__ == "__main__":
    calibrator = ZEDCalibrator(resolution='HD1080')
    image_dir = calibrator.capture_calibration_images(num_target=20)

运行这段代码,你会看到一个并排显示左右镜头的窗口。拿着你的棋盘格在相机前移动,按‘s’键保存。记得要变换角度!保存好后,你会得到left和right两个文件夹,里面是成对的图像。这些就是标定的原材料。

4. 核心操作:使用OpenCV进行单目与双目标定

图像准备好后,我们就可以开始实际的标定计算了。这里我强烈推荐使用 OpenCV 库,它提供了非常完善且久经考验的标定函数。我们分两步走:先分别标定左右两个单目相机,再标定它们的立体关系(双目标定)。

4.1 单目相机标定(以左目为例)

单目标定的目标是得到每个镜头的内参矩阵和畸变系数。OpenCV的cv2.calibrateCamera函数一站式搞定。

import cv2
import numpy as np
import glob

def calibrate_mono_camera(image_dir, pattern_size=(6, 8), square_size=0.025):
    """
    单目相机标定
    :param image_dir: 存放标定图像的目录
    :param pattern_size: 棋盘格内角点数量 (列数, 行数)
    :param square_size: 每个棋盘格方块的物理尺寸(单位:米)
    """
    # 1. 准备物体坐标点 (0,0,0), (1,0,0), (2,0,0) ....,(5,7,0)
    objp = np.zeros((pattern_size[0] * pattern_size[1], 3), np.float32)
    objp[:, :2] = np.mgrid[0:pattern_size[0], 0:pattern_size[1]].T.reshape(-1, 2)
    objp *= square_size  # 乘以实际物理尺寸

    # 用于存储所有图像的对象点和图像点
    objpoints = []  # 真实世界中的3D点
    imgpoints = []  # 图像中的2D点

    # 查找所有图像
    images = glob.glob(f'{image_dir}/*.png')
    print(f"找到 {len(images)} 张图像用于标定。")

    for fname in images:
        img = cv2.imread(fname)
        gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

        # 查找棋盘格角点
        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)

            objpoints.append(objp)
            imgpoints.append(corners_refined)

            # 可视化角点(可选)
            cv2.drawChessboardCorners(img, pattern_size, corners_refined, ret)
            cv2.imshow('Detected Corners', cv2.resize(img, (800, 600)))
            cv2.waitKey(300)  # 显示300毫秒
        else:
            print(f"警告: 在图像 {fname} 中未找到棋盘格角点。")

    cv2.destroyAllWindows()

    if len(objpoints) < 10:
        print("错误:成功检测到角点的图像太少,请检查图像质量或棋盘格方向。")
        return None

    # 2. 进行相机标定!
    print("开始计算相机参数...")
    ret, camera_matrix, dist_coeffs, rvecs, tvecs = cv2.calibrateCamera(
        objpoints, imgpoints, gray.shape[::-1], None, None
    )

    # 3. 计算重投影误差,评估标定质量
    mean_error = 0
    for i in range(len(objpoints)):
        imgpoints_reprojected, _ = cv2.projectPoints(objpoints[i], rvecs[i], tvecs[i], camera_matrix, dist_coeffs)
        error = cv2.norm(imgpoints[i], imgpoints_reprojected, cv2.NORM_L2) / len(imgpoints_reprojected)
        mean_error += error
    mean_error /= len(objpoints)

    print(f"\n=== 左目相机标定结果 ===")
    print(f"标定状态: {'成功' if ret else '失败'}")
    print(f"相机内参矩阵 (K):\n{camera_matrix}")
    print(f"畸变系数 (k1, k2, p1, p2, k3...):\n{dist_coeffs.ravel()}")
    print(f"平均重投影误差: {mean_error:.6f} 像素")
    print("=" * 40)

    # 误差越小越好,通常小于0.5像素就算很不错了,0.1-0.3是优秀水平。
    if mean_error > 0.8:
        print("警告:重投影误差偏大,建议检查标定图像质量或重新采集。")

    return camera_matrix, dist_coeffs, mean_error

# 使用示例
left_camera_matrix, left_dist_coeffs, left_error = calibrate_mono_camera('./calibration_images_20231027_143000/left', square_size=0.025)
right_camera_matrix, right_dist_coeffs, right_error = calibrate_mono_camera('./calibration_images_20231027_143000/right', square_size=0.025)

关键点解读:

  • pattern_size=(6,8):这里指的是内角点(internal corners)的数量,也就是黑白格子相交的内部点。如果你打印的是7x9的格子,内角点就是6x8。
  • square_size=0.025:这是每个棋盘格方块的真实物理边长,单位是米(这里是2.5厘米)。这个值必须准确测量,它决定了你标定出的焦距等参数的真实物理尺度。
  • 重投影误差:这是衡量标定质量的核心指标。它表示的是,用我们标定出的参数,把三维点再投影回图像上,计算出的位置与实际检测到的角点位置之间的平均像素距离。这个值越小越好。一般来说,误差小于0.5像素就算合格,如果能做到0.2像素以下,那标定质量就非常高了。如果误差大于1像素,你就需要回头检查图像质量了。

4.2 双目立体标定

单目标定确定了每个镜头“独善其身”的参数。双目标定则要解决两个镜头“并肩作战”时的关系,核心是求出右相机相对于左相机的旋转矩阵R和平移向量T。这个T的范数(长度)就是我们的基线距离,是双目测距的基石。

def calibrate_stereo_camera(left_img_dir, right_img_dir, left_cam_matrix, left_dist, right_cam_matrix, right_dist, pattern_size=(6,8), square_size=0.025):
    """
    双目立体标定
    :return: R, T, E, F 以及左右相机的校正变换矩阵
    """
    # 准备物体点
    objp = np.zeros((pattern_size[0] * pattern_size[1], 3), np.float32)
    objp[:, :2] = np.mgrid[0:pattern_size[0], 0:pattern_size[1]].T.reshape(-1, 2)
    objp *= square_size

    left_objpoints = []
    left_imgpoints = []
    right_objpoints = []
    right_imgpoints = []

    left_images = sorted(glob.glob(f'{left_img_dir}/*.png'))
    right_images = sorted(glob.glob(f'{right_img_dir}/*.png'))

    paired_image_points = []
    # 需要确保左右图像对能成功配对并检测到角点
    for left_path, right_path in zip(left_images, right_images):
        img_l = cv2.imread(left_path)
        img_r = cv2.imread(right_path)
        gray_l = cv2.cvtColor(img_l, cv2.COLOR_BGR2GRAY)
        gray_r = cv2.cvtColor(img_r, cv2.COLOR_BGR2GRAY)

        ret_l, corners_l = cv2.findChessboardCorners(gray_l, pattern_size)
        ret_r, corners_r = cv2.findChessboardCorners(gray_r, pattern_size)

        if ret_l and ret_r:
            criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
            corners_l_refined = cv2.cornerSubPix(gray_l, corners_l, (11,11), (-1,-1), criteria)
            corners_r_refined = cv2.cornerSubPix(gray_r, corners_r, (11,11), (-1,-1), criteria)

            left_objpoints.append(objp)
            left_imgpoints.append(corners_l_refined)
            right_objpoints.append(objp) # 物体坐标是一样的
            right_imgpoints.append(corners_r_refined)

            paired_image_points.append((left_path, right_path))
        else:
            print(f"跳过未同时检测到角点的图像对: {left_path}, {right_path}")

    print(f"成功配对 {len(paired_image_points)} 对有效图像。")

    if len(paired_image_points) < 8:
        print("错误:有效图像对太少,无法进行可靠的双目标定。")
        return None

    # 进行立体标定
    flags = cv2.CALIB_FIX_INTRINSIC # 使用我们之前标定好的单目内参
    criteria_stereo = (cv2.TERM_CRITERIA_MAX_ITER + cv2.TERM_CRITERIA_EPS, 100, 1e-6)

    ret, _, _, _, _, R, T, E, F = cv2.stereoCalibrate(
        left_objpoints, left_imgpoints, right_imgpoints,
        left_cam_matrix, left_dist,
        right_cam_matrix, right_dist,
        gray_l.shape[::-1],
        criteria=criteria_stereo, flags=flags
    )

    print(f"\n=== 双目立体标定结果 ===")
    print(f"标定状态: {'成功' if ret else '失败'}")
    print(f"右相机相对于左相机的旋转矩阵 R:\n{R}")
    print(f"右相机相对于左相机的平移向量 T (单位: 米):\n{T.ravel()}")
    print(f"基线距离 (左右相机光心距离): {np.linalg.norm(T):.6f} 米")
    print(f"本质矩阵 E:\n{E}")
    print(f"基础矩阵 F:\n{F}")
    print("=" * 40)

    return R, T, E, F

# 使用示例,传入之前单目标定的结果
R, T, E, F = calibrate_stereo_camera(
    './calibration_images/left',
    './calibration_images/right',
    left_camera_matrix, left_dist_coeffs,
    right_camera_matrix, right_dist_coeffs,
    square_size=0.025
)

得到旋转矩阵R和平移向量T后,双目系统的核心几何关系就确定了。T向量的第一个分量(通常是x方向)的绝对值,就是两个相机光心之间的距离,即基线。这个值对于后续的立体匹配和深度计算至关重要。

5. 标定结果验证与深度图优化:让ZED“看得更准”

标定参数算出来了,但到底准不准?不能只看重投影误差,还得在实际应用里检验。最直接的验证方法就是看校正后的图像和深度图的质量。

5.1 立体校正与极线约束

双目相机原始拍出来的左右图,并不是严格行对齐的。立体校正的目的,就是把两个图像平面“掰”到同一个平面上,使得左右图像的对应点只在水平方向(x轴)上有差异(即视差),垂直方向(y轴)坐标相同。这极大简化了立体匹配的搜索过程。

def stereo_rectify_and_show(left_cam_matrix, left_dist, right_cam_matrix, right_dist, R, T, image_size):
    """
    计算立体校正映射,并展示校正效果
    """
    # 计算立体校正所需的变换矩阵
    R1, R2, P1, P2, Q, validPixROI1, validPixROI2 = cv2.stereoRectify(
        left_cam_matrix, left_dist,
        right_cam_matrix, right_dist,
        image_size, R, T,
        alpha=0  # alpha=0 表示校正后图像会被裁剪掉无效区域(黑边),-1则保留所有原图内容但会有黑边
    )

    # 计算用于remap的映射表
    left_map1, left_map2 = cv2.initUndistortRectifyMap(
        left_cam_matrix, left_dist, R1, P1, image_size, cv2.CV_16SC2
    )
    right_map1, right_map2 = cv2.initUndistortRectifyMap(
        right_cam_matrix, right_dist, R2, P2, image_size, cv2.CV_16SC2
    )

    print("立体校正映射计算完成。")
    print(f"重投影矩阵 Q:\n{Q}") # Q矩阵用于将视差图转换为深度图

    # 演示:加载一对图像并进行校正
    img_left = cv2.imread('./calibration_images/left/left_000.png')
    img_right = cv2.imread('./calibration_images/right/right_000.png')

    left_rectified = cv2.remap(img_left, left_map1, left_map2, cv2.INTER_LINEAR)
    right_rectified = cv2.remap(img_right, right_map1, right_map2, cv2.INTER_LINEAR)

    # 绘制极线进行验证:在左图画一条水平线,在右图对应位置也应该匹配到同一特征
    height, width = left_rectified.shape[:2]
    line_y = height // 2
    cv2.line(left_rectified, (0, line_y), (width, line_y), (0, 255, 0), 2)
    cv2.line(right_rectified, (0, line_y), (width, line_y), (0, 255, 0), 2)

    # 并排显示
    display = np.hstack((left_rectified, right_rectified))
    cv2.imshow('Stereo Rectified Images (Green line for epipolar check)', display)
    cv2.waitKey(0)
    cv2.destroyAllWindows()

    return left_map1, left_map2, right_map1, right_map2, Q

# 假设 image_size 是你的图像尺寸,例如 (1920, 1080)
image_size = (1920, 1080)
maps_and_Q = stereo_rectify_and_show(left_camera_matrix, left_dist_coeffs,
                                      right_camera_matrix, right_dist_coeffs,
                                      R, T, image_size)

运行后你会看到校正后的左右图,并且画了一条绿色的水平线。如果标定和校正都很准确,你会发现左右图中同一物体的特征点都在这条水平线上。这就是极线约束的直观体现,也是验证标定成功与否的“金标准”。

5.2 深度图质量对比

标定的最终目的是为了提升深度图的质量。我们可以用ZED SDK直接输出深度图,但更严谨的做法是,使用我们标定好的参数和OpenCV的立体匹配算法(如SGBM)自己计算一个深度图,与SDK的结果进行对比。

def compute_depth_with_custom_params(left_img, right_img, left_map1, left_map2, right_map1, right_map2, Q):
    """
    使用自定义标定参数和OpenCV SGBM算法计算深度图
    """
    # 1. 校正图像
    left_rect = cv2.remap(left_img, left_map1, left_map2, cv2.INTER_LINEAR)
    right_rect = cv2.remap(right_img, right_map1, right_map2, cv2.INTER_LINEAR)

    # 2. 转换为灰度图
    left_gray = cv2.cvtColor(left_rect, cv2.COLOR_BGR2GRAY)
    right_gray = cv2.cvtColor(right_rect, cv2.COLOR_BGR2GRAY)

    # 3. 创建SGBM立体匹配器
    window_size = 5
    min_disp = 0
    num_disp = 16*5  # 必须是16的整数倍
    stereo = cv2.StereoSGBM_create(
        minDisparity=min_disp,
        numDisparities=num_disp,
        blockSize=window_size,
        P1=8 * 3 * window_size ** 2,
        P2=32 * 3 * window_size ** 2,
        disp12MaxDiff=1,
        uniquenessRatio=15,
        speckleWindowSize=0,
        speckleRange=2,
        preFilterCap=63,
        mode=cv2.STEREO_SGBM_MODE_SGBM_3WAY
    )

    # 4. 计算视差图
    disparity_sgbm = stereo.compute(left_gray, right_gray).astype(np.float32) / 16.0

    # 5. 使用Q矩阵将视差图转换为深度图
    # 注意:Q矩阵来自stereoRectify,其中包含了基线等信息
    depth_map = cv2.reprojectImageTo3D(disparity_sgbm, Q)
    # depth_map是一个3通道图像,其中Z通道就是深度值(通常与世界坐标单位一致,如米)

    # 只取Z通道作为深度图
    depth = depth_map[:, :, 2]

    # 过滤无效值(视差为0或负数的点)
    depth[disparity_sgbm <= min_disp] = 0

    return disparity_sgbm, depth

# 加载一对新的测试图像(非标定集)
test_left = cv2.imread('./test_images/test_left.jpg')
test_right = cv2.imread('./test_images/test_right.jpg')

custom_disp, custom_depth = compute_depth_with_custom_params(
    test_left, test_right,
    maps_and_Q[0], maps_and_Q[1], maps_and_Q[2], maps_and_Q[3],
    maps_and_Q[4]
)

# 可视化
cv2.imshow('Custom Disparity Map', (custom_disp - custom_disp.min()) / (custom_disp.max() - custom_disp.min()))
cv2.imshow('Custom Depth Map', cv2.applyColorMap(cv2.convertScaleAbs(custom_depth, alpha=0.03), cv2.COLORMAP_JET))
cv2.waitKey(0)

将我们自己计算的深度图与ZED SDK直接输出的深度图进行对比。如果标定准确,两者在物体轮廓、深度连续性上应该高度一致,且测量已知距离的物体时误差很小。如果发现我们自己算的深度图噪声很大、物体断裂,或者SDK的深度图在边缘处扭曲严重,那很可能需要重新检查标定流程,特别是图像采集环节。

6. 避坑指南与高级优化技巧

踩过不少坑之后,我总结了一些提升标定精度和稳定性的经验:

1. 棋盘格的质量与平整度是重中之重。 打印的纸张要贴平,不能有褶皱。光照要均匀,避免反光。我后来直接定制了一块磨砂亚克力板的棋盘格,效果比纸质的好太多。

2. 采集图像的“姿势”要多样。 很多人只在一个距离上正对着棋盘格拍,这样标定出的畸变系数可能不准。一定要让棋盘格出现在图像的四个角落,并且有倾斜、旋转的姿态。可以参考这个模式:正对(3张)、左倾斜(3张)、右倾斜(3张)、上仰(3张)、下俯(3张)、近距离(2张)、远距离(2张)。

3. 善用OpenCV的cornerSubPix函数。 它通过亚像素优化,能让角点定位精度达到子像素级别,对标定精度有肉眼可见的提升。

4. 理解并监控重投影误差。 这是你标定质量的“体温计”。如果某一张图像的重投影误差远高于平均值,可以考虑把它从标定集中剔除。OpenCV的calibrateCamera函数会返回每幅图像的旋转和平移向量(rvecs, tvecs),你可以据此可视化每张图的相机位置,看看是否覆盖了足够的空间。

5. 双目标定的一致性检查。 标定出的基线距离T,应该和ZED相机官方给出的物理基线(ZED 2i大约是120mm)在同一个数量级。如果算出来差了一个数量级(比如1.2米),那肯定是哪里出错了。

6. 标定参数的保存与加载。 标定一次不容易,一定要把结果(两个相机的camera_matrix, dist_coeffs以及R, T, Q矩阵)用np.save或json保存下来。下次使用时直接加载,无需重复标定。

import json
import numpy as np

def save_calibration_params(filename, params_dict):
    """将标定参数保存为JSON文件(将numpy数组转换为列表)"""
    serializable_dict = {}
    for key, value in params_dict.items():
        if isinstance(value, np.ndarray):
            serializable_dict[key] = value.tolist()
        else:
            serializable_dict[key] = value
    with open(filename, 'w') as f:
        json.dump(serializable_dict, f, indent=4)
    print(f"参数已保存至 {filename}")

def load_calibration_params(filename):
    """从JSON文件加载标定参数(将列表转换回numpy数组)"""
    with open(filename, 'r') as f:
        loaded_dict = json.load(f)
    for key, value in loaded_dict.items():
        if isinstance(value, list):
            loaded_dict[key] = np.array(value)
    return loaded_dict

# 保存示例
calib_data = {
    'left_camera_matrix': left_camera_matrix,
    'left_dist_coeffs': left_dist_coeffs,
    'right_camera_matrix': right_camera_matrix,
    'right_dist_coeffs': right_dist_coeffs,
    'R': R,
    'T': T,
    'Q': Q, # 来自stereoRectify
    'left_map1': maps_and_Q[0], # 可选,保存映射表避免重复计算
    'left_map2': maps_and_Q[1],
    'right_map1': maps_and_Q[2],
    'right_map2': maps_and_Q[3],
    'image_size': [image_size[0], image_size[1]]
}
save_calibration_params('zed_stereo_calibration.json', calib_data)

最后,记住相机标定不是一劳永逸的。如果相机受到剧烈撞击,或者使用环境温度变化极大,都可能引起镜头微小的形变,导致参数漂移。对于高精度的应用,定期(例如每季度或每半年)重新标定一次是个好习惯。掌握了这套从理论到实践,从采集到验证的完整流程,你就能让手中的ZED相机发挥出它应有的精准测量能力,为你的三维视觉项目打下最坚实的地基。

ZED-Mini 标定完全指南(应该是最详细的吧) ZED-Mini 标定完全指南,包括双目标定,IMU标定以及相机-IMU联合标定 阅读详情

相关推荐

【ZED】从零开始使用ZED相机(五):Opencv+Python实现相机标定(单目)

引言 在这篇博客中【ZED】从零开始使用ZED相机(三):相机标定(张氏标定法)相机标定实现起来十分麻烦,且笔者测试下来标定的参数并不准确,而OpenCV中就是直接实现相机标定的库,调用起来十分方便,而且也比较准确。 1 cv2.calibrateCamera 实现单目相机校正 首先肯定是先采集好棋盘格凸显15-20张左右,放在一个文件夹下面如图(笔者采集的比较多,MATLAB计算的重投影误差太大时可以剔除掉一部分图象): # 类二:相机标定执行函数(单目校正) class Cam_calibrate()

qq_44703886的博客 5717

ZED双目相机标定跑通vins fusion

ros1code_utilsimu_utilsKalibrvins fusion(能够运行)

weixin_48852054的博客 3619

【ZED】深入解析ZED相机标定实战:张氏标定法的Python实现与优化

本文深入解析了ZED相机标定实战,重点介绍了张氏标定法的Python实现与优化。通过详细的步骤讲解和代码示例,帮助读者掌握相机标定的核心原理和实际操作技巧,包括内参矩阵求解、畸变系数估计以及标定结果优化与验证,适用于计算机视觉和机器人领域的开发者。

weixin_28743405的博客 394

zed2i相机内参标定

参考:参考以上连接先安装kalibr。

weixin_42703210的博客 2442

【ZED】深入解析ZED相机标定:张氏标定法的实战应用与优化技巧

本文深入解析ZED相机标定中的张氏标定法实战应用与优化技巧,涵盖棋盘格准备、图像采集、单应性矩阵计算、内参求解及非线性优化等关键步骤。通过实际案例和代码示例,展示如何提升标定精度,适用于机器人导航、三维重建等高精度场景。

weixin_30664051的博客 394

【ZED】深入解析ZED相机标定:基于张氏标定法的实践指南

本文详细介绍了ZED相机基于张氏标定法的标定实践指南,涵盖从基础概念到具体实现的完整流程。通过棋盘格图像采集、单应性矩阵计算、内参求解等步骤,帮助读者掌握相机标定的核心技术,并提供了标定结果验证与优化的实用方法,确保获得高精度的标定参数。

weixin_29096377的博客 218

ZED相机标定实战:手把手教你用Python搞定张氏标定法(附完整代码)

本文提供了一份详尽的ZED相机标定实战指南,手把手教你使用Python实现经典的张氏标定法。内容涵盖从环境配置、棋盘格准备、图像采集最佳实践,到完整的代码实现、参数解读与精度评估。通过本教程,你将掌握获取高精度相机内参和畸变系数的完整流程,为后续的三维重建、SLAM等应用奠定坚实基础。

nice1的博客 221

【ZED】深入解析ZED相机双目标定:Opencv+Python实战与参数优化

本文深入解析了ZED相机双目标定的核心挑战与实战优化。通过OpenCV和Python,详细阐述了从高质量数据采集、角点检测预处理,到关键函数`stereoCalibrate`中`flags`参数的深度调优策略。文章提供了一个完整的可复现代码案例,指导读者获得高精度的相机内参、外参(旋转矩阵R与平移向量T)及畸变系数,并介绍了如何通过重投影误差和物理合理性(如基线距离)验证标定结果,为SLAM、三维重建等应用奠定坚实基础。

weixin_29038345的博客 107

ZED双目相机标定实战:OpenCV+Python完整代码解析(附MATLAB对比)

本文提供了ZED双目相机标定的完整实战指南,详细解析了基于OpenCV和Python的标定流程与核心代码。通过对比MATLAB的标定结果,验证了方法的准确性,并深入探讨了标定原理、误差分析与工程优化技巧,为三维视觉应用提供了一套可靠的相机标定解决方案。

sea99的博客 634

【ZED】从零开始使用ZED相机(六):Opencv+Python实现双目相机标定与立体校正

本文详细介绍了如何使用OpenCV和Python为ZED双目相机进行标定与立体校正。与单目标定不同,双目标定需同时获取左右摄像头的内参、畸变系数以及关键的相对位置参数(旋转矩阵R和平移向量T),并通过立体校正实现图像行对准,为后续立体匹配和深度计算奠定基础。文章提供了从图像采集、角点检测到参数计算与效果验证的完整代码实践。

weixin_29266647的博客 286

避坑指南:ZED相机双目标定常见问题与解决方案(OpenCV+Python版)

本文详细解析了ZED相机双目标定在OpenCV+Python环境下的完整流程与常见问题。重点探讨了棋盘格检测、stereoCalibrate函数深度配置、重投影误差分析及立体校正检查等关键环节,提供了经过验证的解决方案与代码示例,旨在帮助开发者高效完成相机标定,为后续三维视觉应用奠定基础。

weixin_29056781的博客 407

ZED相机标定避坑指南:从采集图像到参数优化的完整流程

本文提供了一份详尽的ZED相机单目标定实战指南。文章深入解析了从高精度棋盘格选择、多样化图像采集技巧,到使用OpenCV和Python进行核心标定、参数解读与结果验证的完整流程,并分享了高级优化与排错方法,旨在帮助开发者避开常见陷阱,获得精准可靠的内参和畸变系数。

s8t9u0v1w的博客 448

给STM32F4装上耳朵:用麦克风模块和Keil MDK实现你的第一个语音指令(附完整代码)

本文详细介绍了如何使用STM32F4开发板和麦克风模块实现简单的语音指令识别系统。通过Keil MDK开发环境配置、ADC音频采样、语音活动检测和模板匹配算法,开发者可以快速构建响应'开灯'、'关灯'等基础指令的语音交互方案。文章包含完整代码实现和调试技巧,特别适合STM32初学者入门语音识别领域。

weixin_29171087的博客 436

双面Zed:代码编辑器与深度相机的性能革命与ROS2标定实战

在开发者工具链中,代码编辑器和深度相机看似分属不同领域,却同样把性能优化做到极致。代码编辑器通过LSP与Tree-sitter双引擎协作,实现语法高亮与代码智能的毫秒级响应;双目深度相机则依靠视差计算与IMU融合,为机器提供像素级深度感知。这些技术的落地场景十分广泛:从日常编写Rust、Python程序,到在Ubuntu 24.04环境下开展ROS2联合Zed 2i相机标定,再到借助单目模式加速深度学习推理,处处体现着软硬件协同的效率价值。理解这些底层原理,无论对追求极致编码体验的开发者,还是对搭建机器人感

weixin_30205153的博客 395

FPGA面试题目笔记(一)——FPGA开发流程、亚稳态和竞争冒险、建立保持时间、异步FIFO深度等

同步异步电路、同步异步复位、时钟偏移和抖动、最小工作周期计算、触发器锁存器

H19981118的博客 1万+

015、架构_配置文件_管理节点(proxymanager.ini)

该配置文件中的参数在后台配置,两种生效方式:*所有参数重启生效。*部分参数可以使用工具dbtool动态生效。动态生效命令为:dbtool-pm-load-config。配置文件“proxymanager.ini”用于设置元数据管理模块的信息。文件路径为“~/etc”。配置文件“proxymanager.ini”是普通的文本文件。默认值proxymanager-alarmconfutf8.txt。配置错误或不配置,PM无法连接MDS,无法服务。获取元数据失败超时处理时间。异常时请求启动对等回滚。

Oracle/MySQL 到 国产信创数据库|迁移 · 调优 · 信创替换 331

ZED双目标定避坑指南:从棋盘格打印到参数优化的全流程解析

本文是一份详尽的ZED双目标定实战指南,重点解析了从棋盘格制备、图像同步采集到参数优化的全流程。文章深入探讨了张氏标定法的应用细节,并提供了大量代码示例和避坑技巧,旨在帮助开发者获得高精度的相机内外参,为后续的立体视觉应用奠定坚实基础。

herb5的博客 530

小龙虾OpenClaw卸载全攻略,彻底清理缓存和配置,干净无残留!

不想要的小龙虾OpenClaw怎么彻底卸载?🦞本文教你如何彻底的卸载OpenClaw⚡️,Windows/macOS/Linux通用,干净无残留🧹

XiaoqiangClub的博客 316

双目相机选型与标定实战:D435i、MYNT S2、ZED Mini深度对比

双目视觉是机器人感知的核心技术之一,通过左右图像视差计算三维信息,广泛应用于机械臂抓取、视觉SLAM与移动机器人避障等场景。根据原理不同,双目相机分为主动红外与被动可见光两类:主动双目在室内白墙、桌面等弱纹理环境下优势明显,被动双目则更适合户外强光条件。相机标定质量与深度图稳定性直接影响定位精度,而SDK生态、IMU时间戳同步及硬件成本则决定了项目开发效率。本文基于RealSense D435i、MYNT EYE S2与ZED Mini三款主流设备的实测对比,梳理了双目相机选型的关键指标、双目标定流程、手眼

weixin_34337381的博客 1127

置顶必读(1) | 《YOLOv26实战:从入门到深度优化》专栏导读与完整目录导航(持续更新中)

🏆 本文收录于 《YOLOv26实战:从入门到深度优化》 专栏。 本专栏系统梳理并持续复现 YOLOv26 官方特性、核心改进、多任务支持、部署优化与行业落地,内容坚持 持续更新 + 深度拆解 + 工程落地导向,不仅讲“怎么用”,更讲“为什么这样设计、如何真正落地到项目中”。

**My Coding Family** 9万+

Linux C/C++ or 嵌入式面试之《C/C++笔面试系列》(10) C++ 前置后置增减运算符重载形式与比较

C语言的++、–运算符在此不讨论。 文章目录1、前置后置增减运算符重载的形式2、前置和后置运算符的性能比较 1、前置后置增减运算符重载的形式 自增运算符“++”和自减运算符“–”分别包含两个版本。即运算符前置形式(如 ++x)和运算符后置形式(如 x++),这两者进行的操作是不一样的。因此,当我们在对这两个运算符进行重载时,就必须区分前置和后置形式。C++根据参数的个数来区分前置和后置形式。如果按照通常的方法来重载 ++ 运算符(即作为成员函数重载时不带参数,或者作为非成员函数重载时带有一个参数),那么重载

二进制君 5834

保姆级避坑指南:用Kalibr标定ZED相机与PX4飞控IMU外参(附完整YAML配置)

本文详细解析了使用Kalibr工具进行ZED相机与PX4飞控IMU外参标定的全流程,包括硬件配置、软件环境设置、参数文件优化、数据采集技巧及标定验证。通过精确的外参标定,可显著提升多传感器融合系统的性能,适用于无人机和移动机器人导航。

weixin_30654583的博客 620

DDD系列 - 第12讲 如何在DDD中实现Query服务(轻量化CQRS)

轻量化CQRS、整合Repository和DAO的优势。

罗小爬的技术宝书 268

Python+OpenCV双目标定实战:ZED相机标定全流程解析(附代码)

本文详细解析了使用Python和OpenCV进行ZED相机双目标定的全流程,包括基础原理、准备工作、代码实现及结果评估。通过实战案例和避坑指南,帮助开发者掌握双目相机标定技术,提升三维重建和深度估计的精度。附完整代码,适合计算机视觉从业者参考。

weixin_29065959的博客 188

1个PID回路如何连接2个执行器(算法和代码)

我们先看下位置型PID的控制框图:2、PID是基于error的控制 所以反馈信号跳动、干扰非常大的话,对PID的闭环控制影响是非常大的,所以对于反馈信号、一定要抗干扰和滤波处理。3、下图是增量型PID的控制框图:4、一个PID输出回路如何连接2个执行器?(有些系统会设置热阀和冷阀才能满足控制要求)。

RXXW_Dor的博客 1811

win10 跳过U8环境检测的IE WebControls.rar

win10 安装u8的时候 提示IE WebControls 不通过,这个工具可以欺骗u8的检查。 跳过U8环境检测的IE WebControls。提示导入失败后,重新检测通过。

上一篇: 金仓数据库V009R003C011与MySQL 8.0.12的5大关键语法差异对比(附实测代码)
下一篇: 从旋转到信号处理:欧拉公式如何成为傅立叶变换的幕后英雄?
Python
博客等级 码龄26年 1973粉丝 1567原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值