从零实现相机标定:Python+OpenCV实战指南

刚接触计算机视觉时,相机标定总让人望而生畏——那些矩阵参数、坐标系转换公式像天书一样难以理解。但当我第一次用自己拍摄的棋盘格图片成功计算出相机参数,并看到矫正后的图像时,那种"原来如此"的顿悟感至今难忘。本文将带你用Python和OpenCV一步步完成这个神奇的过程,把抽象的内外参概念转化为可运行的代码和可视化的结果。

1. 环境准备与工具搭建

工欲善其事,必先利其器。我们需要准备以下工具和环境:

  • 硬件准备:一个普通的USB摄像头或智能手机(建议分辨率至少720p)、一张打印的棋盘格标定板(建议A4尺寸以上)。棋盘格可以从OpenCV官网下载标准模板,通常使用8x6或9x7的方格布局。

  • 软件环境

    pip install opencv-python numpy matplotlib
    

    这三个库构成了我们实验的基础工具链:OpenCV提供计算机视觉算法,NumPy处理矩阵运算,Matplotlib用于可视化。

  • 棋盘格注意事项

    • 打印时确保方格是完美的正方形
    • 贴在平整的硬纸板或亚克力板上防止弯曲
    • 光照均匀,避免反光和阴影

2. 数据采集:获取标定图像集

好的标定始于高质量的数据采集。建议在不同角度和距离拍摄15-20张棋盘格照片,覆盖相机视野的各个区域:

import cv2

cap = cv2.VideoCapture(0)  # 如果是USB摄像头
img_count = 0

while img_count < 20:
    ret, frame = cap.read()
    cv2.imshow('Preview', frame)
    
    key = cv2.waitKey(1)
    if key == ord('s'):  # 按s键保存
        cv2.imwrite(f'calib_{img_count}.jpg', frame)
        img_count += 1
    elif key == 27:  # ESC退出
        break

cap.release()
cv2.destroyAllWindows()

采集技巧

  • 让棋盘格占据图像1/3到2/3的面积
  • 尝试不同倾斜角度(30-60度)
  • 包含棋盘格靠近图像边缘的样本
  • 避免运动模糊(快门速度足够快)

3. 角点检测与参数计算

有了图像集后,我们开始自动检测棋盘格角点并计算相机参数:

import numpy as np

# 设置棋盘格规格(内角点数量)
pattern_size = (8, 6)  # 比如8x6的棋盘格有7x5=35个内角点

# 准备对象点:(0,0,0), (1,0,0), (2,0,0), ..., (7,5,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)

# 存储对象点和图像点
obj_points = []  # 3D世界空间中的点
img_points = []  # 2D图像平面中的点

# 遍历所有图像
for fname in glob.glob('calib_*.jpg'):
    img = cv2.imread(fname)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    
    # 查找角点
    ret, corners = cv2.findChessboardCorners(gray, pattern_size, None)
    
    if ret:  # 如果找到角点
        obj_points.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)
        img_points.append(corners2)
        
        # 可视化(可选)
        cv2.drawChessboardCorners(img, pattern_size, corners2, ret)
        cv2.imshow('Corners', img)
        cv2.waitKey(500)

cv2.destroyAllWindows()

4. 相机标定与参数解析

现在我们可以进行实际的相机标定了:

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

print("相机矩阵:\n", mtx)
print("\n畸变系数:", dist.ravel())

参数解读

  1. 相机矩阵(内参矩阵)

    [[fx, 0, cx],
     [0, fy, cy],
     [0,  0,  1]]
    
    • fx, fy:x和y方向的焦距(像素单位)
    • cx, cy:主点坐标(通常接近图像中心)
  2. 畸变系数

    • k1, k2, k3:径向畸变系数
    • p1, p2:切向畸变系数
  3. 外参

    • rvecs:每张图像的旋转向量(可通过cv2.Rodrigues()转换为矩阵)
    • tvecs:每张图像的平移向量

5. 结果验证与图像矫正

理论需要实践验证,让我们用标定结果矫正一张测试图像:

# 读取测试图像
test_img = cv2.imread('test_image.jpg')
h, w = test_img.shape[:2]

# 优化相机矩阵和获取ROI
newcameramtx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist, (w,h), 1, (w,h))

# 方法1:使用cv2.undistort
dst = cv2.undistort(test_img, mtx, dist, None, newcameramtx)

# 方法2:使用remapping(更灵活)
mapx, mapy = cv2.initUndistortRectifyMap(mtx, dist, None, newcameramtx, (w,h), 5)
dst = cv2.remap(test_img, mapx, mapy, cv2.INTER_LINEAR)

# 裁剪图像(可选)
x, y, w, h = roi
dst = dst[y:y+h, x:x+w]

# 并排显示
cv2.imshow('Original vs Undistorted', np.hstack((test_img, dst)))
cv2.waitKey(0)
cv2.destroyAllWindows()

效果评估

  • 观察图像边缘的直线是否变得笔直
  • 比较矫正前后棋盘格的形变程度
  • 测量已知长度的物体在图像中的比例是否一致

6. 高级技巧与常见问题

在实际项目中,你可能会遇到以下情况:

提高标定精度的技巧

  • 使用更多样化的拍摄角度(建议至少15张高质量图像)
  • 确保棋盘格平整,打印精度高
  • 在标定时使用cv2.cornerSubPix提高角点定位精度
  • 考虑使用非对称圆形网格标定板(对遮挡更鲁棒)

典型问题排查

问题现象 可能原因 解决方案
找不到棋盘格 方格数设置错误 确认pattern_size是内角点数
角点检测不准确 图像模糊或反光 重新采集更清晰的图像
矫正后图像边缘扭曲 畸变模型不匹配 尝试更高阶的畸变系数
重投影误差大 标定板移动或变形 确保标定板刚性且固定

保存和加载标定参数

import pickle

# 保存标定结果
calib_data = {
    'camera_matrix': mtx,
    'dist_coeffs': dist,
    'rvecs': rvecs,
    'tvecs': tvecs
}

with open('calibration.pkl', 'wb') as f:
    pickle.dump(calib_data, f)

# 加载标定结果
with open('calibration.pkl', 'rb') as f:
    calib_data = pickle.load(f)
    mtx = calib_data['camera_matrix']
    dist = calib_data['dist_coeffs']

7. 实际应用案例

相机标定不只是学术练习,它在许多实际应用中至关重要:

增强现实(AR)

  • 将虚拟物体准确叠加到真实世界
  • 需要精确的相机参数来保证虚实对齐

三维重建

# 简化的视差计算示例
def compute_disparity(imgL, imgR):
    stereo = cv2.StereoSGBM_create(
        minDisparity=0,
        numDisparities=64,
        blockSize=11
    )
    disparity = stereo.compute(imgL, imgR)
    return disparity

机器人导航

  • 通过标定后的相机进行精确的距离测量
  • 视觉里程计中的位姿估计

工业检测

  • 消除镜头畸变对测量精度的影响
  • 实现像素到实际尺寸的准确转换

在最近的一个项目中,我们使用标定结果将无人机拍摄的图像拼接成高精度地图。最初由于忽略了对每个相机的单独标定,拼接处总是出现明显的错位。重新进行严格标定后,拼接精度提高了近70%。这让我深刻体会到,好的标定是计算机视觉应用的基石。

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐