用Python和OpenCV从零搭建一个完整的双目视觉系统(三)
本系列文章旨在系统性地阐述如何利用 Python 与 OpenCV 库,从零开始构建一个完整的双目立体视觉系统。在上一篇文章中,我们为项目设计了清晰的架构。现在,我们将深入第一个,也是整个双目视觉系统的模块——。如果说双目视觉系统是一座高楼,那么标定就是它的地基。一个不准确、不稳定的标定结果,会使后续所有的计算(立体匹配、三维重建)都失去意义,最终导致整个系统的崩塌。无论你的匹配算法多么先进,都无
本系列文章旨在系统性地阐述如何利用 Python 与 OpenCV 库,从零开始构建一个完整的双目立体视觉系统。
本项目github地址:https://github.com/present-cjn/stereo-vision-python.git
在上一篇文章中,我们为项目设计了清晰的架构。现在,我们将深入第一个,也是整个双目视觉系统最关键的模块——相机标定 (Camera Calibration)。
如果说双目视觉系统是一座高楼,那么标定就是它的地基。一个不准确、不稳定的标定结果,会使后续所有的计算(立体匹配、三维重建)都失去意义,最终导致整个系统的崩塌。无论你的匹配算法多么先进,都无法弥补一个错误的地基。
本文将深入探讨相机标定的核心原理,详细对比“一步标定法”与“两步标定法”的优劣,并逐行解析我们项目中健壮的标定代码实现。
1. 标定的“为什么”:理解相机的内在与外在
相机标定的根本目的,是精确地计算出相机的内参 (Intrinsics) 和 外参 (Extrinsics)。
内参 (Intrinsic Parameters)
内参描述了相机自身的光学特性,它将三维世界中的点投影到二维图像平面上。这与相机被如何放置无关,是相机出厂时就固有的属性。
- 相机矩阵
K:
[[ fx, 0, cx ],
[ 0, fy, cy ],
[ 0, 0, 1 ]]
-
- 焦距 (
fx,fy): 以像素为单位,决定了相机的视野范围(FOV)。 - 主点 (
cx,cy): 相机光轴与成像平面的交点,通常非常接近图像的中心。
- 焦距 (
- 畸变系数
D: 由于镜头制造工艺的物理限制,图像会产生变形。畸变主要分为两种:
-
- 径向畸变: 导致直线在图像边缘处变弯曲(桶形或枕形畸变)。
- 切向畸变: 由镜头与成像平面不完全平行导致。
外参 (Extrinsic Parameters)
对于双目系统,外参描述了右相机相对于左相机坐标系的空间位置关系。
- 旋转矩阵
R(3x3): 描述了右相机相对于左相机的旋转姿态。 - 平移向量
T(3x1): 描述了右相机光学中心相对于左相机光学中心的位移。T的模长通常被称为基线距 (Baseline),是深度计算的关键参数。
2. “一步法” vs. “两步法”:稳定性的抉择
在OpenCV中,获取这些参数主要有两种策略。
一步标定法 (One-Step Calibration)
- 做法: 直接调用
cv2.stereoCalibrate函数,让它一次性地、同时地去求解两个相机的内参、畸变以及它们之间的外参,总共20多个未知参数。 - 缺点: 因为优化空间巨大且参数高度耦合,用一次性求解容易陷入某个“局部最优解”中。这导致标定结果非常不稳定,对初始值和输入数据的质量极其敏感。
两步标定法 (Two-Step Calibration) - 本项目采用的方案
- 做法: 遵循“分而治之”的思想。
-
- 第一步:单目标定: 分别对左右相机调用
cv2.calibrateCamera函数。这是一个相对简单的优化问题,可以非常精确地计算出每个相机各自的内参K和畸变D。 - 第二步:双目标定: 再次调用
cv2.stereoCalibrate,但这次将第一步得到的精确内参作为高质量的初始猜测值,并使用cv2.CALIB_USE_INTRINSIC_GUESS标志。
- 第一步:单目标定: 分别对左右相机调用
- 优点: 极大地降低了优化问题的复杂度,使得求解过程更稳定、结果更精确、对数据顺序不那么敏感。
3. 代码实现深度解析
现在,我们深入 calibration/calibrator.py 文件,看看这些理论是如何通过代码实现的。
__init__:准备“标准答案卡”
class StereoCalibrator:
def __init__(self, chessboard_size: tuple, square_size: float):
self.chessboard_size = chessboard_size
self.square_size = square_size
# 准备 objectPoints,这是棋盘格角点的三维物理坐标
width, height = self.chessboard_size
self.objp = np.zeros((width * height, 3), np.float32)
# 使用 np.mgrid 生成网格点,确保 (y, x) 顺序
grid_points = np.mgrid[0:height, 0:width].T.reshape(-1, 2)
self.objp[:, :2] = grid_points
self.objp *= self.square_size
- 核心:
self.objp是我们提供给算法的“标准答案”。我们在这里创建了一个完美的、尺寸精确的虚拟棋盘格。np.mgrid[0:height, 0:width]的顺序至关重要,它确保了我们生成的3D点顺序能与findChessboardCorners的输出顺序匹配。
_find_corners_in_all_images:寻找角点
def _find_corners_in_all_images(self, image_source):
# ... (glob 和 natural_sort_key 加载图像对) ...
for left_path, right_path in image_pairs:
# ... (读取图像,并健壮地转换为灰度图) ...
# 为左图增加亮度归一化标志,提升在不同光照下的检测成功率
find_flags_l = cv2.CALIB_CB_NORMALIZE_IMAGE
ret_left, corners_left = cv2.findChessboardCorners(gray_left, self.chessboard_size, flags=find_flags_l)
ret_right, corners_right = cv2.findChessboardCorners(gray_right, self.chessboard_size, None)
# 只有当左右图像都成功找到了角点,才进行后续处理
if ret_left and ret_right:
# 将角点精确到亚像素级
corners_left_sub = cv2.cornerSubPix(gray_left, corners_left, (3, 3), (-1, -1), config.SUBPIX_CRITERIA)
corners_right_sub = cv2.cornerSubPix(gray_right, corners_right, (3, 3), (-1, -1), config.SUBPIX_CRITERIA)
object_points.append(self.objp)
image_points_left.append(corners_left_sub)
image_points_right.append(corners_right_sub)
else:
# 打印被跳过的图像对,便于调试
print(f" - Skipped pair: ... (Left found: {ret_left}, Right found: {ret_right})")
# ...
return object_points, image_points_left, image_points_right, image_size
- 核心: 这个函数负责从每对图片中提取角点的2D像素坐标。
if ret_left and ret_right:这个“守门员”逻辑,自动剔除了质量不佳的图像对,是保证最终标定质量的关键。
_calibrate_single_camera:执行单目标定
@staticmethod
def _calibrate_single_camera(obj_points, img_points, img_size, camera_name: str):
print(f"\nPerforming monocular calibration for {camera_name} camera...")
ret, K, D, rvecs, tvecs = cv2.calibrateCamera(
obj_points, img_points, img_size, None, None,
criteria=config.MONO_CALIB_CRITERIA
)
# 效果评估:要求重投影误差必须小于1.0
assert ret < 1.0, f"{camera_name} camera reprojection error is too high: {ret}"
print(f" - {camera_name} camera calibrated with reprojection error: {ret}")
return K, D, ret
- 核心: 这是两步法的第一步。它接收3D物理点和2D图像点,计算出单个相机的内参
K和畸变D。我们用assert ret < 1.0来强制要求一个高质量的标定结果。
_calibrate_stereo_relationship:执行双目标定
@staticmethod
def _calibrate_stereo_relationship(obj_points, img_points_l, img_points_r, K1, D1, K2, D2, img_size):
print("\nPerforming stereo calibration to find the relationship between cameras...")
# 核心标志:告诉函数使用我们提供的K,D作为高质量的初始猜测值
flags = config.STEREO_CALIB_FLAGS # cv2.CALIB_USE_INTRINSIC_GUESS
ret, K1, D1, K2, D2, R, T, E, F = cv2.stereoCalibrate(
obj_points, img_points_l, img_points_r,
K1, D1, # 传入单目标定的结果作为初始值
K2, D2,
img_size,
flags=flags,
criteria=config.STEREO_CALIB_CRITERIA
)
assert ret < 1.0, f"Stereo calibration reprojection error is too high: {ret}"
# ...
return stereo_params
- 核心: 这是两步法的第二步。最关键的参数是
flags=cv2.CALIB_USE_INTRINSIC_GUESS,它告诉算法:“在优化R和T的同时,你可以微调一下我给你的K和D,以达到全局最优。”
如何使用 StereoCalibrator 类
我们已经设计了一个功能强大且独立的 StereoCalibrator 类。那么,在一个简单的脚本中,我们该如何调用它来完成标定呢?下面是一个最简化的概念性示例:
# conceptual_usage_example.py
import os
from calibration.calibrator import StereoCalibrator
from utils import file_utils
# 1. 定义你的标定板参数
chessboard_size = (11, 8)
square_size_mm = 12.0
image_directory = "data/calibration_images/"
output_file = "output/stereo_params.yml"
# 2. 创建标定器实例
calibrator = StereoCalibrator(
chessboard_size=chessboard_size,
square_size=square_size_mm
)
# 3. 执行标定
# 在实际项目中,我们通过更灵活的方式传入图片路径
stereo_params = calibrator.run(image_directory)
# 4. 检查并保存结果
if stereo_params:
print("标定成功!正在保存参数...")
# 确保输出目录存在
os.makedirs("output", exist_ok=True)
file_utils.save_stereo_params(output_file, stereo_params)
else:
print("标定失败。")
在我们的完整项目中,上述所有步骤,包括参数的传递和文件的保存,都已经被优雅地封装在了 main.py 的命令行界面中。你不需要自己编写上面的脚本,只需要在克隆了我们的完整代码后,运行一个简单的命令即可开始标定:
# 用项目中的测试图片
python main.py calibrate
或者也可以用自己的采集的图片,按项目要求修改图片名放到[项目路径/data/calibration_images]
# 用自己给定的图片,例如一个11x8角点,格子边长12mm的标定板
python main.py calibrate --corners 11,8 --size 12
用项目中的测试图片运行成功后会打印如下信息

4. 如何解读标定结果
当你成功运行标定后,会得到一个 .yml 文件。解读这份标定结果文件的能力至关重要:
- 重投影误差 (reprojection_error): 最重要的指标。它应该远小于1.0,越小越好。
- 内参矩阵 (K1, K2): 两个相机的
fx和fy应该各自相近,并且两个相机的K矩阵整体也应该非常相似。 - 畸变系数 (D1, D2): 对于普通镜头,这些值通常都比较小。
- 旋转矩阵 R: 对于一个被精确平行放置的双目系统,
R应该非常接近一个单位矩阵。 - 平移向量 T:
-
Tx: 应该是负值,其绝对值就是你的基线距(单位mm),应该与你的物理测量值大致相符。Ty,Tz: 应该都非常接近于0,表明两个相机在垂直和前后方向上对齐得很好。
火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。
更多推荐
所有评论(0)