强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

OpenCV 计算机视觉完全教程 / 第 14 章 — 相机标定与立体视觉

第 14 章 — 相机标定与立体视觉

14.1 相机模型

针孔相机模型

世界坐标 (X,Y,Z) → 相机坐标 → 图像坐标 → 像素坐标 (u,v)

像素坐标:
u = fx * X/Z + cx
v = fy * Y/Z + cy

内参矩阵 K:
┌              ┐
│ fx   0   cx │
│  0  fy   cy │
│  0   0    1 │
└              ┘

畸变参数: [k1, k2, p1, p2, k3]
  k1, k2, k3: 径向畸变
  p1, p2: 切向畸变

畸变类型

畸变形状说明
桶形畸变向内弯曲广角镜头常见
枕形畸变向外弯曲长焦镜头常见
切向畸变倾斜传感器不平行

14.2 相机标定

使用棋盘格图案进行相机标定。

import cv2
import numpy as np
import glob

def calibrate_camera(image_dir, pattern_size=(9, 6)):
    """
    相机标定

    参数:
        image_dir: 棋盘格图像目录
        pattern_size: 棋盘格内角点数 (列, 行)
    """
    # 3D 世界坐标(假设棋盘格在 z=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)
    # 实际尺寸(假设每个方格 25mm)
    square_size = 25.0
    objp *= square_size

    obj_points = []  # 3D 点
    img_points = []  # 2D 点

    images = glob.glob(f"{image_dir}/*.jpg")
    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
            )

            obj_points.append(objp)
            img_points.append(corners_refined)

            # 可视化
            cv2.drawChessboardCorners(img, pattern_size, corners_refined, ret)
            cv2.imshow("标定", img)
            cv2.waitKey(500)
            print(f"  ✓ {fname}")
        else:
            print(f"  ✗ {fname} (未检测到棋盘格)")

    cv2.destroyAllWindows()

    # 执行标定
    ret, camera_matrix, dist_coeffs, rvecs, tvecs = cv2.calibrateCamera(
        obj_points, img_points, gray.shape[::-1], None, None
    )

    print(f"\n=== 标定结果 ===")
    print(f"RMS 重投影误差: {ret:.4f} 像素")
    print(f"\n内参矩阵 K:\n{camera_matrix}")
    print(f"\n畸变系数: {dist_coeffs.ravel()}")

    return camera_matrix, dist_coeffs, rvecs, tvecs

# 使用
# K, dist, rvecs, tvecs = calibrate_camera("calibration_images/", (9, 6))
// C++ 相机标定
std::vector<std::vector<cv::Point3f>> obj_points;
std::vector<std::vector<cv::Point2f>> img_points;
cv::Size pattern_size(9, 6);

// ... 收集角点 ...

cv::Mat camera_matrix, dist_coeffs;
std::vector<cv::Mat> rvecs, tvecs;
double rms = cv::calibrateCamera(obj_points, img_points,
                                  image_size, camera_matrix,
                                  dist_coeffs, rvecs, tvecs);

标定参数参考

参数典型范围说明
fx, fy500-3000焦距(像素)
cx, cy图像中心主点坐标
k1-0.5 ~ 0.5径向畸变系数 1
k2-0.1 ~ 0.1径向畸变系数 2
p1, p2-0.01 ~ 0.01切向畸变系数
RMS 误差< 0.5 像素标定质量指标

14.3 畸变校正

import cv2
import numpy as np

def undistort_image(image, camera_matrix, dist_coeffs):
    """去畸变"""
    h, w = image.shape[:2]

    # 方法 1: 简单去畸变
    undistorted = cv2.undistort(image, camera_matrix, dist_coeffs)

    # 方法 2: 使用最优新内参
    new_mtx, roi = cv2.getOptimalNewCameraMatrix(
        camera_matrix, dist_coeffs, (w, h), alpha=0,
        newImageSize=(w, h)
    )
    # alpha=0: 裁剪所有黑边
    # alpha=1: 保留所有像素(可能有黑边)

    undistorted2 = cv2.undistort(image, camera_matrix, dist_coeffs,
                                  None, new_mtx)

    # 裁剪有效区域
    x, y, rw, rh = roi
    cropped = undistorted2[y:y+rh, x:x+rw]

    return cropped

# 使用映射表(更快,适合视频流)
def create_undistort_maps(camera_matrix, dist_coeffs, image_size):
    w, h = image_size
    new_mtx, _ = cv2.getOptimalNewCameraMatrix(
        camera_matrix, dist_coeffs, (w, h), 0, (w, h)
    )
    map1, map2 = cv2.initUndistortRectifyMap(
        camera_matrix, dist_coeffs, None, new_mtx,
        (w, h), cv2.CV_32FC1
    )
    return map1, map2

def fast_undistort(image, map1, map2):
    return cv2.remap(image, map1, map2, cv2.INTER_LINEAR)

14.4 立体视觉

14.4.1 立体标定

import cv2
import numpy as np

def stereo_calibrate(obj_points, img_points_l, img_points_r,
                     image_size, K1, dist1, K2, dist2):
    """立体标定"""
    flags = cv2.CALIB_FIX_INTRINSIC
    criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER,
                100, 1e-6)

    ret, K1, dist1, K2, dist2, R, T, E, F = cv2.stereoCalibrate(
        obj_points, img_points_l, img_points_r,
        K1, dist1, K2, dist2, image_size,
        criteria=criteria, flags=flags
    )

    print(f"立体标定 RMS 误差: {ret:.4f}")
    print(f"旋转矩阵 R:\n{R}")
    print(f"平移向量 T:\n{T}")
    print(f"基线距离: {np.linalg.norm(T):.2f} mm")

    return R, T, E, F

14.4.2 立体校正

def stereo_rectify(K1, dist1, K2, dist2, image_size, R, T):
    """立体校正"""
    R1, R2, P1, P2, Q, roi1, roi2 = cv2.stereoRectify(
        K1, dist1, K2, dist2, image_size, R, T,
        alpha=0  # 裁剪黑边
    )

    # 生成映射表
    map1_l, map2_l = cv2.initUndistortRectifyMap(
        K1, dist1, R1, P1, image_size, cv2.CV_32FC1)
    map1_r, map2_r = cv2.initUndistortRectifyMap(
        K2, dist2, R2, P2, image_size, cv2.CV_32FC1)

    return (map1_l, map2_l), (map1_r, map2_r), Q

def rectify_images(img_left, img_right, maps_left, maps_right):
    """应用立体校正"""
    rect_l = cv2.remap(img_left, maps_left[0], maps_left[1],
                       cv2.INTER_LINEAR)
    rect_r = cv2.remap(img_right, maps_right[0], maps_right[1],
                       cv2.INTER_LINEAR)
    return rect_l, rect_r

14.5 深度图生成

import cv2
import numpy as np

def compute_depth_map(img_left, img_right, Q=None):
    """计算视差图/深度图"""
    # 转灰度
    gray_l = cv2.cvtColor(img_left, cv2.COLOR_BGR2GRAY)
    gray_r = cv2.cvtColor(img_right, cv2.COLOR_BGR2GRAY)

    # 方法 1: StereoBM(块匹配,速度快)
    stereo_bm = cv2.StereoBM_create(numDisparities=128, blockSize=15)
    disparity_bm = stereo_bm.compute(gray_l, gray_r)

    # 方法 2: StereoSGBM(半全局匹配,精度高)
    stereo_sgbm = cv2.StereoSGBM_create(
        minDisparity=0,
        numDisparities=128,        # 必须是 16 的倍数
        blockSize=5,
        P1=8 * 3 * 5**2,          # 惩罚项 1
        P2=32 * 3 * 5**2,         # 惩罚项 2
        disp12MaxDiff=1,
        uniquenessRatio=10,
        speckleWindowSize=100,
        speckleRange=32,
        preFilterCap=63,
        mode=cv2.STEREO_SGBM_MODE_SGBM_3WAY
    )
    disparity_sgbm = stereo_sgbm.compute(gray_l, gray_r)

    # 归一化显示
    disp_normalized = cv2.normalize(
        disparity_sgbm, None, 0, 255, cv2.NORM_MINMAX, cv2.CV_8U
    )

    # 如果有 Q 矩阵,重投影为 3D 点云
    if Q is not None:
        points_3D = cv2.reprojectImageTo3D(disparity_sgbm.astype(np.float32) / 16.0, Q)
        return disparity_sgbm, disp_normalized, points_3D

    return disparity_sgbm, disp_normalized, None

StereoBM vs StereoSGBM

方法速度精度适用场景
StereoBM★★★★★★★★实时、低精度
StereoSGBM★★★☆☆★★★★★离线、高精度

14.6 ArUco 标记与 AR

import cv2
import numpy as np

def detect_aruco(image):
    """ArUco 标记检测"""
    # 选择字典
    aruco_dict = cv2.aruco.getPredefinedDictionary(cv2.aruco.DICT_6X6_250)
    detector = cv2.aruco.ArucoDetector(aruco_dict)

    # 检测
    corners, ids, rejected = detector.detectMarkers(image)

    if ids is not None:
        print(f"检测到 {len(ids)} 个标记: {ids.flatten()}")
        cv2.aruco.drawDetectedMarkers(image, corners, ids)

    return corners, ids

def estimate_pose(image, corners, ids, camera_matrix, dist_coeffs,
                  marker_length=0.05):
    """估计标记位姿"""
    rvecs, tvecs, _ = cv2.aruco.estimatePoseSingleMarkers(
        corners, marker_length, camera_matrix, dist_coeffs
    )

    result = image.copy()
    for i in range(len(ids)):
        cv2.drawFrameAxes(result, camera_matrix, dist_coeffs,
                          rvecs[i], tvecs[i], marker_length * 0.5)

        # 输出位姿信息
        t = tvecs[i][0]
        distance = np.linalg.norm(t)
        print(f"标记 {ids[i][0]}: 位置=({t[0]:.3f},{t[1]:.3f},{t[2]:.3f}), "
              f"距离={distance:.3f}m")

    return result, rvecs, tvecs

14.7 点云处理

import cv2
import numpy as np

def generate_point_cloud(disparity, Q, image, min_disp=10, num_disp=128):
    """从视差图生成彩色点云"""
    # 重投影到 3D
    points_3D = cv2.reprojectImageTo3D(
        disparity.astype(np.float32) / 16.0, Q
    )

    # 获取颜色
    colors = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

    # 过滤有效点
    mask = disparity > disparity.min()
    mask &= disparity < (min_disp + num_disp)

    points = points_3D[mask]
    colors = colors[mask]

    # 保存为 PLY 文件
    save_ply("pointcloud.ply", points, colors)
    return points, colors

def save_ply(filename, points, colors):
    """保存点云为 PLY 格式"""
    with open(filename, 'w') as f:
        f.write("ply\n")
        f.write("format ascii 1.0\n")
        f.write(f"element vertex {len(points)}\n")
        f.write("property float x\nproperty float y\nproperty float z\n")
        f.write("property uchar red\nproperty uchar green\nproperty uchar blue\n")
        f.write("end_header\n")
        for p, c in zip(points, colors):
            f.write(f"{p[0]:.4f} {p[1]:.4f} {p[2]:.4f} "
                    f"{c[0]} {c[1]} {c[2]}\n")

14.8 实战应用

场景技术说明
机器人导航双目深度避障、路径规划
工业测量标定 + 立体视觉三维尺寸检测
AR 应用ArUco + solvePnP虚实叠加
自动驾驶LiDAR + 相机标定多传感器融合
3D 扫描结构光/双目物体三维重建

14.9 扩展阅读

资源链接说明
OpenCV 标定教程docs.opencv.org/4.x/d4/d94/tutorial_camera_calibration官方标定教程
立体视觉docs.opencv.org/4.x/dd/d53/tutorial_py_depthmap深度图教程
ArUcodocs.opencv.org/4.x/da/d13/tutorial_aruco_detectionAR 标记
下一章第 15 章 — GPU 加速CUDA/UMat

本章小结: 掌握了相机标定、畸变校正、立体视觉、深度图生成和 ArUco 标记检测等 3D 视觉技术。