1. 为什么你的ZED相机需要标定?从“感觉不准”到“精准测量”
如果你刚拿到ZED相机,兴冲冲地用它来测距或者做三维重建,结果发现测出来的距离和实际差了好几厘米,或者拼接出来的模型歪歪扭扭,别急着怀疑硬件坏了。十有八九,问题出在相机标定这一步没做好。我刚开始玩ZED的时候也踩过这个坑,以为出厂设置就万事大吉,结果在做一个机械臂抓取项目时,因为深度误差导致定位失败,白白折腾了好几天。
简单来说,相机标定就是给相机做一次“体检”和“身份认证”。它要解决两个核心问题:第一,确定相机自身的“性格参数”(内参);第二,确定相机在空间中的“位置和姿态”(外参)。你可以把相机想象成人的眼睛。每个人的眼球形状、晶状体焦距都略有不同(这就是内参),而且你观察世界时,头是正着看还是歪着看,站得近还是站得远(这就是外参),都会影响你判断物体的位置和大小。相机标定,就是通过一套数学方法,把这些“个性”和“状态”精确地测量出来。
对于ZED这类双目立体相机,标定尤为重要。因为它依赖左右两个镜头看到的画面差异(视差)来计算深度。如果两个镜头的内参不准,或者它们之间的相对位置(外参)测量有误,那么计算出的视差就会出错,深度信息自然也就不准了。这就像你用两只眼睛看东西,如果一只眼睛近视度数没配准,或者两眼斜视,你判断距离的能力就会大打折扣。
所以,无论你是做机器人导航、三维扫描、体积测量还是AR/VR应用,精准的标定都是后续所有工作的基石。没有准确的标定,再高级的算法也是“巧妇难为无米之炊”。接下来,我们就从最经典、最实用的张氏标定法开始,一步步拆解ZED相机的标定全过程。
2. 张氏标定法:化繁为简的“棋盘格魔法”
提到相机标定,就绕不开张正友博士在1998年提出的“张氏标定法”。这个方法之所以经典,是因为它用一个简单巧妙的思路,解决了复杂的参数求解问题,让高精度的相机标定走出了实验室,变成了普通开发者也能轻松上手的技术。
它的核心道具是一张棋盘格图案。你肯定见过,就是那种黑白相间的方格图。这个方法妙在哪呢?它不需要你知道这个棋盘格在真实世界里精确的物理尺寸(比如每个格子必须是10.000毫米),只需要你知道它有多少行、多少列(比如内角点是6x8)。棋盘格提供了一个已知的、规则的三维点阵(所有角点都在一个平面上,且间距均匀),相机从不同角度去拍摄它,就能建立起大量的二维图像点与三维空间点的对应关系。
整个标定的数学过程,可以理解为层层递进的四步:
-
求解单应性矩阵(Homography):这是第一步,也是最关键的一步。对于每一张拍摄的棋盘格图片,我们都要计算一个单应性矩阵。这个矩阵描述的是棋盘格平面(真实世界中的那个平面)和相机成像平面(照片)之间的映射关系。你可以把它想象成一个“透视变换”的公式,能把棋盘格从三维空间“拍扁”到二维照片上。通过多张不同角度的图片,我们就能得到多个这样的映射关系。
-
求解相机内参矩阵(Intrinsics):有了多个单应性矩阵,我们就可以利用一些约束条件(比如旋转矩阵的正交性),反推出相机的内部参数。这个内参矩阵通常长这样:
[fx, s, cx] [0, fy, cy] [0, 0, 1]其中,
fx和fy是焦距(以像素为单位),(cx, cy)是图像的主点(通常接近图像中心),s是倾斜系数(现代相机通常为0)。对于ZED相机,左右两个镜头需要分别标定出各自的内参矩阵。 -
求解相机外参矩阵(Extrinsics):内参知道了,单应性矩阵也知道了,那么对于每一张图片,相机相对于那个棋盘格的位置和朝向(即外参矩阵
[R|t],R是旋转,t是平移)也就能算出来了。这告诉我们,拍摄每张照片时,相机是从哪个角度、哪个位置看棋盘格的。 -
求解畸变系数并全局优化:镜头不是完美的,尤其是广角镜头,拍出来的图像边缘的直线会变弯,这就是畸变。张氏标定法最后一步就是估算径向畸变系数(通常是
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相机发挥出它应有的精准测量能力,为你的三维视觉项目打下最坚实的地基。
5717



被折叠的 条评论
为什么被折叠?



