# Camera Calibration

> OpenCV4 相机标定技能 - 单目/双目标定、畸变校正、立体匹配、深度估计、3D 重建

- Skill: `miuav/camera-calibration` (Agent Skill)
- Install (CLI): `npx skillmds@latest add miuav/camera-calibration`
- Raw SKILL.md: https://api.skillmd.com/api/skills/miuav/camera-calibration/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: miuav (https://skillmd.com/u/miuav)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/miuav/camera-calibration

---


# OpenCV4 Camera Calibration Skill

> 相机标定与立体视觉完整指南

---

## 何时使用

当需要以下帮助时使用此技能：
- 单目相机标定（内参）
- 双目相机标定（内外参）
- 畸变校正
- 立体校正（极线校正）
- 立体匹配和深度估计
- 3D 重建

---

## 快速参考

### 标定板生成

```python
import cv2
import numpy as np
import glob

# 生成棋盘格标定板
def generate_chessboard(size=(9, 6), square_size=1.0, save_path='chessboard.png'):
    objp = np.zeros((size[0]*size[1], 3), np.float32)
    objp[:, :2] = np.mgrid[0:size[0], 0:size[1]].T.reshape(-1, 2) * square_size
    
    fig, ax = plt.subplots(figsize=(8, 6))
    ax.set_xlim(0, size[0])
    ax.set_ylim(0, size[1])
    ax.set_aspect('equal')
    ax.set_facecolor('white')
    
    for i in range(size[0] + 1):
        ax.axvline(i, color='black', linewidth=1)
    for i in range(size[1] + 1):
        ax.axhline(i, color='black', linewidth=1)
    
    plt.axis('off')
    plt.savefig(save_path, dpi=150, bbox_inches='tight', 
                facecolor='white', edgecolor='none')
    plt.close()

# 或使用 OpenCV 打印标定板
# 国际象棋：9x6 角点，80mm 格子
# 圆形标定板：4x11 圆点阵列
```

### 单目相机标定

```python
import cv2
import numpy as np
import glob

# 标定板参数
objp = np.zeros((6*9, 3), np.float32)
objp[:, :2] = np.mgrid[0:9, 0:6].T.reshape(-1, 2)
objp *= 25  # 格子大小 mm

# 存储所有图像的点
objpoints = []  # 3D 点
imgpoints = []  # 2D 点

images = glob.glob('calibration/*.jpg')
img_size = None

for fname in images:
    img = cv2.imread(fname)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    img_size = gray.shape[::-1]
    
    # 查找棋盘格角点
    ret, corners = cv2.findChessboardCorners(gray, (9, 6), None)
    
    if ret:
        objpoints.append(objp)
        # 亚像素精度优化
        criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
        corners2 = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria)
        imgpoints.append(corners2)
        
        # 可视化
        cv2.drawChessboardCorners(img, (9, 6), corners2, ret)
        cv2.imshow('Corners', img)
        cv2.waitKey(500)

cv2.destroyAllWindows()

# 标定
ret, camera_matrix, dist_coeffs, rvecs, tvecs = cv2.calibrateCamera(
    objpoints, imgpoints, img_size, None, None)

print(f"Camera Matrix:\n{camera_matrix}")
print(f"Distortion Coefficients:\n{dist_coeffs}")

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

### 畸变校正

```python
# 加载标定结果
data = np.load('calibration.npz')
camera_matrix = data['camera_matrix']
dist_coeffs = data['dist_coeffs']

# 读取图像
img = cv2.imread('test.jpg')
h, w = img.shape[:2]

# 方法1：矫正映射
new_camera_matrix, roi = cv2.getOptimalNewCameraMatrix(
    camera_matrix, dist_coeffs, (w, h), 1, (w, h))
mapx, mapy = cv2.initUndistortRectifyMap(
    camera_matrix, dist_coeffs, None, new_camera_matrix, (w, h), 5)
undistorted = cv2.remap(img, mapx, mapy, cv2.INTER_LINEAR)

# 方法2：一步校正
undistorted = cv2.undistort(img, camera_matrix, dist_coeffs, None, new_camera_matrix)

# 裁剪 ROI 区域
x, y, w, h = roi
undistorted = undistorted[y:y+h, x:x+w]
```

### 双目相机标定

```python
import cv2
import numpy as np
import glob

# 左右相机标定板点
objp = np.zeros((6*9, 3), np.float32)
objp[:, :2] = np.mgrid[0:9, 0:6].T.reshape(-1, 2) * 25

objpoints_l, imgpoints_l = [], []
objpoints_r, imgpoints_r = [], []

# 读取左右图像
images_l = sorted(glob.glob('stereo/left/*.jpg'))
images_r = sorted(glob.glob('stereo/right/*.jpg'))

for fname_l, fname_r in zip(images_l, images_r):
    img_l = cv2.imread(fname_l)
    img_r = cv2.imread(fname_r)
    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, (9, 6), None)
    ret_r, corners_r = cv2.findChessboardCorners(gray_r, (9, 6), None)
    
    if ret_l and ret_r:
        objpoints_l.append(objp)
        objpoints_r.append(objp)
        # 亚像素优化
        corners_l = cv2.cornerSubPix(gray_l, corners_l, (11,11), (-1,-1),
                                       (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001))
        corners_r = cv2.cornerSubPix(gray_r, corners_r, (11,11), (-1,-1),
                                       (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001))
        imgpoints_l.append(corners_l)
        imgpoints_r.append(corners_r)

# 分别标定左右相机
ret_l, mtx_l, dist_l, _, _ = cv2.calibrateCamera(objpoints_l, imgpoints_l, gray_l.shape[::-1], None, None)
ret_r, mtx_r, dist_r, _, _ = cv2.calibrateCamera(objpoints_r, imgpoints_r, gray_r.shape[::-1], None, None)

# 双目标定
ret, mtx_l, dist_l, mtx_r, dist_r, R, T, E, F = cv2.stereoCalibrate(
    objpoints_l, imgpoints_l, imgpoints_r,
    mtx_l, dist_l, mtx_r, dist_r,
    gray_l.shape[::-1],
    flags=cv2.CALIB_FIX_INTRINSIC)

print(f"Rotation R:\n{R}")
print(f"Translation T:\n{T}")
```

### 立体校正和匹配

```python
# 立体校正
R1, R2, P1, P2, Q, _, _ = cv2.stereoRectify(
    mtx_l, dist_l, mtx_r, dist_r,
    gray_l.shape[::-1], R, T)

# 计算映射表
map1_l, map2_l = cv2.initUndistortRectifyMap(mtx_l, dist_l, R1, P1, gray_l.shape[::-1], cv2.CV_32FC2)
map1_r, map2_r = cv2.initUndistortRectifyMap(mtx_r, dist_r, R2, P2, gray_r.shape[::-1], cv2.CV_32FC2)

# 校正图像
rect_l = cv2.remap(img_l, map1_l, map2_l, cv2.INTER_LINEAR)
rect_r = cv2.remap(img_r, map1_r, map2_r, cv2.INTER_LINEAR)

# SGBM 立体匹配
stereo = cv2.StereoSGBM_create(minDisparity=0, numDisparities=64, blockSize=9)
disparity = stereo.compute(rect_l, rect_r)

# 转换为深度图
depth = (mtx_l[0,0] * T[0,0]) / disparity
```

---

## ROS2 集成

```python
# ROS2 相机标定
from rclpy.node import Node
from sensor_msgs.msg import Image
from cv_bridge import CvBridge
import cv2
import numpy as np

class CameraCalibrator(Node):
    def __init__(self):
        super().__init__('camera_calibrator')
        self.bridge = CvBridge()
        self.objpoints = []
        self.imgpoints = []
        self.objp = np.zeros((6*9, 3), np.float32)
        self.objp[:, :2] = np.mgrid[0:9, 0:6].T.reshape(-1, 2)
        
        self.subscription = self.create_subscription(
            Image, '/camera/image_raw', self.calibration_callback, 10)
    
    def calibration_callback(self, msg):
        img = self.bridge.imgmsg_to_cv2(msg, 'bgr8')
        gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
        ret, corners = cv2.findChessboardCorners(gray, (9, 6), None)
        
        if ret:
            corners2 = cv2.cornerSubPix(gray, corners, (11,11), (-1,-1),
                                         (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001))
            self.objpoints.append(self.objp)
            self.imgpoints.append(corners2)
            cv2.drawChessboardCorners(img, (9, 6), corners2, ret)
        
        cv2.imshow('Calibration', img)
        cv2.waitKey(1)
    
    def calibrate(self, img_size):
        ret, mtx, dist, _, _ = cv2.calibrateCamera(
            self.objpoints, self.imgpoints, img_size, None, None)
        return mtx, dist
```

---

## 最佳实践

1. **标定板准备**：
   - 打印在高精度纸上，或使用显示器显示
   - 确保标定板平整无褶皱
   - 格子尺寸精确（常用 25mm 或 30mm）

2. **标定图像采集**：
   - 至少 15-20 张图像
   - 覆盖整个视野（中心和边缘）
   - 不同角度和距离（俯仰、偏航、滚动）
   - 光照均匀，避免反光

3. **标定质量评估**：
   - 重投影误差 < 0.5 像素
   - 检查各图像的角点检测精度
   - 剔除误差大的图像重新标定

4. **双目标定**：
   - 确保左右相机同步采集
   - 两相机在同一平面上（简化标定）
   - 基线距离影响深度精度

---

## 相关技能

- [opencv-image-processing](./image-processing) - 图像处理基础
- [opencv-depth-estimation](./depth-estimation) - 深度估计

