相机内外参描述了相机从三维世界坐标到二维图像坐标的几何映射关系。

一、什么是相机内参和外参

相机内参(Intrinsic Parameters)

相机内参描述相机本身的光学特性,与相机在世界坐标系中的位置无关。

焦距参数

  • fx(水平焦距)
    • 表示相机在水平方向的焦距,单位为像素
    • 计算公式:fx = f * mx,其中 f 是物理焦距(mm),mx 是水平像素密度(pixel/mm)
    • 典型值:手机相机 800-2000 像素,专业相机 1000-5000 像素
    • 作用:控制水平方向的缩放,决定物体在图像中的宽度大小
  • fy(垂直焦距)
    • 表示相机在垂直方向的焦距,单位为像素
    • 计算公式:fy = f * my,其中 my 是垂直像素密度(pixel/mm)
    • 理想情况下 fx ≈ fy,但实际可能存在微小差异
    • 作用:控制垂直方向的缩放,决定物体在图像中的高度大小
# 焦距对投影的影响示例
# 物体距离相机 1000mm,实际高度 100mm
depth = 1000  # mm
object_height = 100  # mm

# 不同焦距下的投影高度
fx_values = [800, 1200, 1600, 2000]
for fx in fx_values:
    projected_height = (fx * object_height) / depth
    print(f"焦距 fx={fx}: 投影高度 = {projected_height:.1f} 像素")

主点参数

  • cx(主点X坐标)
    • 光轴与图像平面的交点的X坐标,单位为像素
    • 理想值:cx = image_width / 2
    • 实际值可能偏移,反映镜头装配误差
    • 作用:定义图像的光学中心,影响透视投影的中心位置
  • cy(主点Y坐标)
    • 光轴与图像平面的交点的Y坐标,单位为像素
    • 理想值:cy = image_height / 2
    • 偏移量通常在几个像素以内
    • 作用:与cx一起确定光学中心,影响图像的对称性
# 主点偏移的影响示例
import numpy as np

# 理想主点位置
ideal_cx = 640  # 1280x720 图像的中心
ideal_cy = 360

# 实际主点位置(有偏移)
actual_cx = 645  # 偏移 +5 像素
actual_cy = 355  # 偏移 -5 像素

# 偏移会导致投影点位置偏移
world_point = np.array([100, 200, 1000])  # 世界坐标点
fx, fy = 1000, 1000

# 理想投影
ideal_u = fx * world_point[0] / world_point[2] + ideal_cx
ideal_v = fy * world_point[1] / world_point[2] + ideal_cy

# 实际投影
actual_u = fx * world_point[0] / world_point[2] + actual_cx
actual_v = fy * world_point[1] / world_point[2] + actual_cy

print(f"理想投影: ({ideal_u:.1f}, {ideal_v:.1f})")
print(f"实际投影: ({actual_u:.1f}, {actual_v:.1f})")

畸变系数

径向畸变系数(由镜头曲率引起):

  • k1(一阶径向畸变)
    • 主要畸变系数,影响最大
    • k1 > 0:桶形畸变(图像中心向外凸)
    • k1 < 0:枕形畸变(图像中心向内凹)
    • 典型值:-0.5 到 +0.5
  • k2(二阶径向畸变)
    • 高阶校正项,补偿k1无法修正的畸变
    • 数值通常比k1小一个数量级
    • 典型值:-0.1 到 +0.1
  • k3(三阶径向畸变)
    • 极高阶项,仅在严重畸变时使用
    • 多数情况下接近0
    • 典型值:-0.01 到 +0.01

切向畸变系数(由镜头装配不平行引起):

  • p1(切向畸变系数1)
    • 校正由于镜头与传感器不平行导致的畸变
    • 影响图像的倾斜和不对称
    • 典型值:-0.01 到 +0.01
  • p2(切向畸变系数2)
    • 与p1配合校正切向畸变
    • 通常数值较小
    • 典型值:-0.01 到 +0.01
# 畸变系数影响示例
def apply_distortion(x, y, k1, k2, k3, p1, p2):
    """应用畸变模型"""
    r2 = x*x + y*y
    
    # 径向畸变
    radial_factor = 1 + k1*r2 + k2*r2*r2 + k3*r2*r2*r2
    
    # 切向畸变
    tangential_x = 2*p1*x*y + p2*(r2 + 2*x*x)
    tangential_y = p1*(r2 + 2*y*y) + 2*p2*x*y
    
    # 校正后的坐标
    x_corrected = x * radial_factor + tangential_x
    y_corrected = y * radial_factor + tangential_y
    
    return x_corrected, y_corrected

# 不同畸变系数的影响
distortions = [
    (0.2, 0.01, 0, 0, 0),    # 轻微桶形畸变
    (-0.1, 0.02, 0, 0, 0),   # 轻微枕形畸变
    (0.1, 0, 0, 0.01, 0.01), # 主要是切向畸变
]

for i, (k1, k2, k3, p1, p2) in enumerate(distortions):
    x_orig, y_orig = 100, 100  # 原始坐标
    x_dist, y_dist = apply_distortion(x_orig, y_orig, k1, k2, k3, p1, p2)
    print(f"畸变组{i+1}: ({x_orig}, {y_orig}) -> ({x_dist:.2f}, {y_dist:.2f})")

内参矩阵

K = [fx  0  cx]
    [0  fy  cy]
    [0  0   1 ]

矩阵元素含义

  • fx, fy:分别控制x和y方向的缩放
  • cx, cy:定义投影中心的偏移
  • 对角线形式表示x、y方向独立缩放
  • 右上角为0表示像素是矩形的(无倾斜)

不同相机类型的内参特点

相机类型 fx/fy 范围 cx/cy 偏移 主要畸变
手机相机 800-2000 <10 像素 中等径向畸变
工业相机 1000-5000 <5 像素 轻微畸变
鱼眼相机 200-800 <20 像素 强径向畸变
监控相机 500-1500 <15 像素 中等径向畸变

相机外参(Extrinsic Parameters)

相机外参描述相机相对于世界坐标系的位置和姿态。

旋转矩阵 R

旋转矩阵 R是3×3正交矩阵,描述相机坐标系相对于世界坐标系的旋转关系。

R = [r11 r12 r13]
    [r21 r22 r23]
    [r31 r32 r33]

矩阵性质

  • 正交矩阵:R^T * R = I(转置等于逆矩阵)
  • 行列式:det(R) = 1
  • 每行和每列都是单位向量

几何含义

  • 第1行 [r11, r12, r13]:世界坐标系X轴在相机坐标系中的表示
  • 第2行 [r21, r22, r23]:世界坐标系Y轴在相机坐标系中的表示
  • 第3行 [r31, r32, r33]:世界坐标系Z轴在相机坐标系中的表示
# 旋转矩阵示例:相机绕Y轴旋转45度
import numpy as np

def rotation_matrix_y(angle_deg):
    """绕Y轴旋转的旋转矩阵"""
    angle_rad = np.radians(angle_deg)
    cos_a = np.cos(angle_rad)
    sin_a = np.sin(angle_rad)
    
    R = np.array([
        [cos_a,  0, sin_a],
        [0,      1,     0],
        [-sin_a, 0, cos_a]
    ])
    return R

# 45度旋转
R_45 = rotation_matrix_y(45)
print("45度Y轴旋转矩阵:")
print(R_45)

# 验证正交性
print(f"R^T * R = \n{R_45.T @ R_45}")
print(f"行列式 = {np.linalg.det(R_45):.6f}")

欧拉角表示: 旋转矩阵也可以用欧拉角(roll, pitch, yaw)表示:

def euler_to_rotation_matrix(roll, pitch, yaw):
    """欧拉角转旋转矩阵"""
    # 角度转弧度
    r, p, y = np.radians([roll, pitch, yaw])
    
    # 分别绕X、Y、Z轴旋转
    Rx = np.array([
        [1, 0, 0],
        [0, np.cos(r), -np.sin(r)],
        [0, np.sin(r), np.cos(r)]
    ])
    
    Ry = np.array([
        [np.cos(p), 0, np.sin(p)],
        [0, 1, 0],
        [-np.sin(p), 0, np.cos(p)]
    ])
    
    Rz = np.array([
        [np.cos(y), -np.sin(y), 0],
        [np.sin(y), np.cos(y), 0],
        [0, 0, 1]
    ])
    
    # 组合旋转:R = Rz * Ry * Rx
    R = Rz @ Ry @ Rx
    return R

# 示例:相机姿态为 roll=10°, pitch=20°, yaw=30°
R_euler = euler_to_rotation_matrix(10, 20, 30)
print("欧拉角旋转矩阵:")
print(R_euler)

平移向量 t

平移向量 t是3×1向量,描述相机原点在世界坐标系中的位置。

t = [tx]
    [ty]
    [tz]

物理含义

  • tx:相机沿世界坐标系X轴的位移(米或毫米)
  • ty:相机沿世界坐标系Y轴的位移
  • tz:相机沿世界坐标系Z轴的位移

重要概念

  • t 不是相机的世界坐标位置
  • 相机的世界坐标位置 = -R^T * t
  • t 表示世界原点在相机坐标系中的位置
# 平移向量示例
def camera_position_from_extrinsics(R, t):
    """从外参计算相机在世界坐标系中的位置"""
    camera_position = -R.T @ t
    return camera_position

# 示例外参
R = np.array([
    [0.866, -0.5, 0],
    [0.5, 0.866, 0],
    [0, 0, 1]
])  # 绕Z轴旋转30度

t = np.array([100, 200, 300])  # 平移向量

# 计算相机实际位置
camera_pos = camera_position_from_extrinsics(R, t)
print(f"平移向量 t = {t}")
print(f"相机世界坐标位置 = {camera_pos}")

# 验证:世界原点在相机坐标系中的位置
world_origin_in_camera = R @ np.array([0, 0, 0]) + t
print(f"世界原点在相机坐标系中 = {world_origin_in_camera}")

外参矩阵

外参通常表示为齐次变换矩阵:

[R|t] = [r11 r12 r13 tx]
        [r21 r22 r23 ty]
        [r31 r32 r33 tz]
        [0   0   0   1]  (4x4 齐次形式)

坐标变换: 世界坐标点 Pw = [Xw, Yw, Zw]^T 转换为相机坐标 Pc = [Xc, Yc, Zc]^T

Pc = R * Pw + t
# 坐标变换示例
def world_to_camera(world_point, R, t):
    """世界坐标转相机坐标"""
    camera_point = R @ world_point + t
    return camera_point

# 示例
world_point = np.array([1000, 500, 2000])  # 世界坐标点 (mm)
R = np.eye(3)  # 无旋转
t = np.array([100, 200, 300])  # 平移 (mm)

camera_point = world_to_camera(world_point, R, t)
print(f"世界坐标: {world_point}")
print(f"相机坐标: {camera_point}")

外参的实际应用

多相机系统标定

在多相机系统中,外参描述各相机间的相对位置关系:

# 双目相机外参示例
def stereo_baseline_from_extrinsics(R1, t1, R2, t2):
    """从外参计算双目基线距离"""
    # 相机1和相机2的世界坐标位置
    pos1 = -R1.T @ t1
    pos2 = -R2.T @ t2
    
    # 基线距离
    baseline = np.linalg.norm(pos2 - pos1)
    return baseline, pos1, pos2

# 示例双目外参
R1 = np.eye(3)  # 左相机无旋转
t1 = np.array([0, 0, 0])  # 左相机在原点

R2 = np.eye(3)  # 右相机无旋转
t2 = np.array([120, 0, 0])  # 右相机沿X轴偏移120mm

baseline, pos1, pos2 = stereo_baseline_from_extrinsics(R1, t1, R2, t2)
print(f"双目基线距离: {baseline:.1f} mm")
print(f"左相机位置: {pos1}")
print(f"右相机位置: {pos2}")

机器人手眼标定

在机器人视觉中,外参描述相机相对于机器人坐标系的安装位置:

# 手眼标定示例
def hand_eye_transformation(robot_pose, camera_extrinsics):
    """计算物体在机器人坐标系中的位置"""
    R_cam, t_cam = camera_extrinsics
    R_robot, t_robot = robot_pose
    
    # 相机坐标系到机器人坐标系的变换
    # 这里简化处理,实际需要标定得出
    R_hand_eye = np.eye(3)
    t_hand_eye = np.array([50, 0, 100])  # 相机相对机器人末端的偏移
    
    return R_hand_eye, t_hand_eye

# 应用示例
print("机器人手眼标定参数获取...")

外参标定的精度要求

应用场景 位置精度 角度精度 备注
工业检测 ±1mm ±0.1° 高精度测量
机器人抓取 ±2mm ±0.5° 实时性要求
AR/VR ±5mm ±1° 用户体验优先
自动驾驶 ±10mm ±0.2° 远距离精度
三维重建 ±0.5mm ±0.05° 极高精度

二、什么时候需要标定,什么时候不需要

需要标定的场景

  1. 三维重建
    • 立体视觉、SLAM、SfM 等需要精确的空间测量
  2. 机器人视觉
    • 视觉导航、物体抓取、路径规划需要准确的深度信息
  3. 增强现实 (AR)
    • 虚拟物体与现实场景的精确对齐
  4. 工业检测
    • 尺寸测量、缺陷检测等需要毫米级精度
  5. 自动驾驶
    • 障碍物距离测算、车道线检测

不需要标定的场景

  1. 纯图像处理
    • 图像滤波、增强、风格转换等二维操作
  2. 目标检测/分类
    • 不需要空间信息的物体识别任务
  3. 文档扫描
    • 仅需要图像内容,不涉及实际尺寸

三、相机标定的数学原理

坐标系转换过程

世界坐标 → 相机坐标 → 图像坐标 → 像素坐标

世界坐标 (Xw, Yw, Zw)
    ↓ [R|t] 外参变换
相机坐标 (Xc, Yc, Zc)  
    ↓ 透视投影
图像坐标 (x, y)
    ↓ 内参变换
像素坐标 (u, v)

完整的相机模型

s[u] = [fx  0  cx] [r11 r12 r13 tx] [Xw]
 [v]   [0  fy  cy] [r21 r22 r23 ty] [Yw]
 [1]   [0  0   1 ] [r31 r32 r33 tz] [Zw]
                                    [1 ]

简化为:s·p = K·[R|t]·Pw

畸变校正

径向畸变(桶形/枕形):

x_corrected = x(1 + k1*r² + k2*r⁴ + k3*r⁶)
y_corrected = y(1 + k1*r² + k2*r⁴ + k3*r⁶)

切向畸变(镜头不平行):

x_corrected = x + [2*p1*x*y + p2*(r² + 2*x²)]
y_corrected = y + [p1*(r² + 2*y²) + 2*p2*x*y]

其中 r² = x² + y²

四、实际标定操作步骤

使用 OpenCV 进行标定

1. 准备标定板

最常用的是棋盘格标定板

  • 推荐 9×6 或 8×6 内角点
  • 方格边长建议 20-30mm
  • 打印到平整硬纸板上

2. 采集标定图像

import cv2
import numpy as np
import glob

# 设置棋盘格参数
pattern_size = (9, 6)  # 内角点数量
square_size = 25.0     # 方格边长(mm)

# 准备物理坐标点
pattern_points = np.zeros((np.prod(pattern_size), 3), np.float32)
pattern_points[:, :2] = np.mgrid[0:pattern_size[0], 0:pattern_size[1]].T.reshape(-1, 2)
pattern_points *= square_size

# 存储所有图像的角点
object_points = []  # 3D 点
image_points = []   # 2D 点

# 加载标定图像
images = glob.glob('calibration_images/*.jpg')

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)
        corners2 = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria)
        
        object_points.append(pattern_points)
        image_points.append(corners2)
        
        # 可视化角点
        cv2.drawChessboardCorners(img, pattern_size, corners2, ret)
        cv2.imshow('Corners', img)
        cv2.waitKey(500)

cv2.destroyAllWindows()

3. 执行标定

# 相机标定
ret, camera_matrix, dist_coeffs, rvecs, tvecs = cv2.calibrateCamera(
    object_points, image_points, gray.shape[::-1], None, None)

print("标定完成!")
print(f"重投影误差: {ret:.4f}")
print(f"相机内参矩阵:\n{camera_matrix}")
print(f"畸变系数: {dist_coeffs.ravel()}")

# 保存标定结果
np.savez('camera_calibration.npz', 
         camera_matrix=camera_matrix,
         dist_coeffs=dist_coeffs,
         rvecs=rvecs, 
         tvecs=tvecs)

4. 验证标定质量

# 计算重投影误差
total_error = 0
for i in range(len(object_points)):
    img_points2, _ = cv2.projectPoints(object_points[i], rvecs[i], tvecs[i], 
                                       camera_matrix, dist_coeffs)
    error = cv2.norm(image_points[i], img_points2, cv2.NORM_L2) / len(img_points2)
    total_error += error

mean_error = total_error / len(object_points)
print(f"平均重投影误差: {mean_error:.4f} 像素")

# 一般来说,误差小于0.5像素为优秀,小于1像素为良好

标定质量评估标准

重投影误差范围 标定质量 适用场景
< 0.3 像素 优秀 高精度测量、工业检测
0.3-0.5 像素 良好 三维重建、机器人视觉
0.5-1.0 像素 一般 AR/VR、普通视觉应用
> 1.0 像素 较差 需要重新标定

五、双目相机标定

双目相机需要额外标定立体外参,描述两个相机间的相对位置关系。

立体标定步骤

# 双目立体标定
def stereo_calibrate(object_points, imgpoints_left, imgpoints_right, 
                    camera_matrix_left, dist_coeffs_left,
                    camera_matrix_right, dist_coeffs_right, image_size):
    
    # 立体标定
    criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 100, 1e-5)
    
    ret, camera_matrix_left, dist_coeffs_left, camera_matrix_right, dist_coeffs_right, \
    R, t, E, F = cv2.stereoCalibrate(
        object_points, imgpoints_left, imgpoints_right,
        camera_matrix_left, dist_coeffs_left,
        camera_matrix_right, dist_coeffs_right,
        image_size, criteria=criteria, flags=cv2.CALIB_FIX_INTRINSIC)
    
    print(f"立体标定重投影误差: {ret:.4f}")
    print(f"旋转矩阵 R:\n{R}")
    print(f"平移向量 t: {t.ravel()}")
    
    return R, t, E, F

# 立体校正
R1, R2, P1, P2, Q, validPixROI1, validPixROI2 = cv2.stereoRectify(
    camera_matrix_left, dist_coeffs_left,
    camera_matrix_right, dist_coeffs_right,
    image_size, R, t)

# 生成校正映射
map1x, map1y = cv2.initUndistortRectifyMap(camera_matrix_left, dist_coeffs_left, 
                                           R1, P1, image_size, cv2.CV_32FC1)
map2x, map2y = cv2.initUndistortRectifyMap(camera_matrix_right, dist_coeffs_right, 
                                           R2, P2, image_size, cv2.CV_32FC1)

深度计算

def compute_depth(img_left, img_right, map1x, map1y, map2x, map2y, Q):
    # 立体校正
    img_left_rect = cv2.remap(img_left, map1x, map1y, cv2.INTER_LINEAR)
    img_right_rect = cv2.remap(img_right, map2x, map2y, cv2.INTER_LINEAR)
    
    # 立体匹配
    stereo = cv2.StereoBM_create(numDisparities=64, blockSize=15)
    disparity = stereo.compute(img_left_rect, img_right_rect)
    
    # 视差转深度
    depth = cv2.reprojectImageTo3D(disparity, Q)
    
    return depth, disparity

六、标定中的常见问题与解决方案

问题诊断表

问题现象 可能原因 解决方案
重投影误差过大 标定板不平整、角点检测不准 更换高质量标定板、增加标定图像数量
标定结果不稳定 图像数量不足、姿态变化不够 采集20-30张不同角度的图像
边缘畸变校正效果差 高阶畸变系数未考虑 使用更复杂的畸变模型
双目深度不准确 立体标定参数错误 检查双目间距、重新进行立体标定

标定技巧

  1. 标定板要求
    • 保持平整,避免弯曲变形
    • 选择合适尺寸(占画面1/3-1/2)
    • 确保良好的打印质量
  2. 图像采集
    • 覆盖整个视野范围
    • 包含不同距离和角度
    • 避免运动模糊和过曝
  3. 环境光照
    • 均匀充足的照明
    • 避免强烈阴影和反光
    • 保持光照一致性

七、实际应用案例

工业测量应用

def measure_object_size(img, camera_matrix, dist_coeffs, real_world_height):
    """
    基于已知高度的参考物体测量其他物体尺寸
    """
    # 畸变校正
    undistorted = cv2.undistort(img, camera_matrix, dist_coeffs)
    
    # 物体检测和轮廓提取(省略具体实现)
    contours = detect_objects(undistorted)
    
    measurements = []
    for contour in contours:
        # 计算轮廓外接矩形
        x, y, w, h = cv2.boundingRect(contour)
        
        # 基于参考高度计算实际尺寸
        pixel_per_mm = h / real_world_height
        actual_width = w / pixel_per_mm
        actual_height = h / pixel_per_mm
        
        measurements.append({
            'width_mm': actual_width,
            'height_mm': actual_height,
            'area_mm2': actual_width * actual_height
        })
    
    return measurements

SLAM应用中的位姿估计

def estimate_camera_pose(img, camera_matrix, dist_coeffs, known_points_3d):
    """
    基于已知3D点估计相机位姿
    """
    # 检测特征点
    keypoints, descriptors = detect_features(img)
    
    # 匹配到已知3D点
    matched_2d_points, matched_3d_points = match_to_known_points(
        keypoints, descriptors, known_points_3d)
    
    # PnP算法求解位姿
    success, rvec, tvec = cv2.solvePnP(
        matched_3d_points, matched_2d_points, 
        camera_matrix, dist_coeffs)
    
    if success:
        # 旋转向量转旋转矩阵
        R, _ = cv2.Rodrigues(rvec)
        
        # 计算相机在世界坐标系中的位置
        camera_position = -R.T @ tvec
        
        return R, tvec, camera_position
    
    return None, None, None

八、标定结果的存储和使用

标准存储格式

# 保存完整标定参数
calibration_data = {
    'camera_matrix': camera_matrix.tolist(),
    'dist_coeffs': dist_coeffs.tolist(),
    'image_size': [width, height],
    'reprojection_error': mean_error,
    'calibration_date': datetime.now().isoformat()
}

# JSON格式存储
import json
with open('camera_calibration.json', 'w') as f:
    json.dump(calibration_data, f, indent=2)

标定参数验证

def validate_calibration(camera_matrix, dist_coeffs, test_images):
    """
    验证标定参数的有效性
    """
    errors = []
    
    for img_path in test_images:
        img = cv2.imread(img_path)
        
        # 畸变校正前后对比
        undistorted = cv2.undistort(img, camera_matrix, dist_coeffs)
        
        # 检测棋盘格角点
        gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
        ret, corners = cv2.findChessboardCorners(gray, pattern_size)
        
        if ret:
            # 计算校正后的角点位置精度
            undist_gray = cv2.cvtColor(undistorted, cv2.COLOR_BGR2GRAY)
            ret2, corners2 = cv2.findChessboardCorners(undist_gray, pattern_size)
            
            if ret2:
                error = assess_corner_quality(corners2)
                errors.append(error)
    
    return np.mean(errors) if errors else float('inf')