第三节:相机标定实战——用OpenCV搞定棋盘格标定

好,咱们直接进入正题。

相机标定,说白了就是给内窥镜的“眼睛”做一次精准的视力检查。你得知道它看东西时,画面是怎么扭曲的,颜色是怎么偏移的。这一节,我就带你手把手用OpenCV完成棋盘格标定,拿到内参和畸变系数这两个核心参数。

3.1 为什么非得用棋盘格?

你可能要问:标定板那么多,为什么大家都用棋盘格?

原因其实很简单——棋盘格的角点检测算法非常成熟,OpenCV里直接封装好了。你想想看,黑白相间的格子,角点位置明确,亚像素精度也能做到很高。我在项目中试过圆点标定板,精度确实也不错,但棋盘格胜在稳定、好上手。

我的小建议: 打印棋盘格时,尽量用哑光纸。反光太强的话,角点检测容易翻车。我曾经吃过这个亏,后来换了哑光纸,问题就解决了。

3.2 准备工作

你需要准备:

  • 一张打印好的棋盘格标定板(建议 9×6 内角点,格子大小 20mm×20mm)
  • 内窥镜或普通摄像头
  • Python 3.8+ 和 OpenCV 4.x

嗯,这里要注意:棋盘格的内角点数量,指的是内部交叉点的个数。比如 9×6 的棋盘,内部角点就是 8×5。别搞反了,我第一次标定时就数错了,结果跑了半天数据全废了。

3.3 核心代码实现

咱们直接上代码。我会把每一步拆开讲清楚。

import cv2
import numpy as np
import glob

# 1. 准备棋盘格参数
chessboard_size = (8, 5)  # 内角点数量
square_size = 20.0        # 格子边长,单位mm

# 2. 生成世界坐标系中的点
objp = np.zeros((chessboard_size[0] * chessboard_size[1], 3), np.float32)
objp[:, :2] = np.mgrid[0:chessboard_size[0], 0:chessboard_size[1]].T.reshape(-1, 2)
objp *= square_size

# 3. 存储所有图像的点
objpoints = []  # 世界坐标系点
imgpoints = []  # 图像坐标系点

# 4. 读取标定图像
images = glob.glob('calib_images/*.jpg')

for fname in images:
    img = cv2.imread(fname)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    
    # 5. 查找棋盘格角点
    ret, corners = cv2.findChessboardCorners(gray, chessboard_size, None)
    
    if ret:
        objpoints.append(objp)
        
        # 6. 亚像素精确化
        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)
        
        # 7. 可视化角点(可选)
        cv2.drawChessboardCorners(img, chessboard_size, corners2, ret)
        cv2.imshow('Corners', img)
        cv2.waitKey(100)

cv2.destroyAllWindows()

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

print("内参矩阵:\n", mtx)
print("畸变系数:\n", dist)

3.4 代码逐段解析

这段代码我用了很多次,每次用都会微调。咱们挑几个关键点说说。

3.4.1 世界坐标点的生成

np.mgrid 这行代码,很多人第一次看会懵。其实它就是生成一个网格,每个交叉点对应一个 (x, y, 0) 坐标。z 轴设为0,因为标定板是平面的。我习惯把格子尺寸乘上去,这样内参里的焦距单位就是毫米,方便后续计算。

3.4.2 角点检测与亚像素优化

cv2.findChessboardCorners 返回的角点坐标是整数像素级别的。但实际应用中,我们需要亚像素精度。所以后面用 cv2.cornerSubPix 做一次精细化。这个步骤不能省,否则标定结果会偏。

注意: 标定图像至少需要 10-15 张。而且每张图的棋盘格姿态要不同——倾斜、旋转、远近都要有。我见过有人拍了20张全是正面朝前的,标定出来畸变系数全是0,那肯定不对。

3.5 标定结果解读

标定完成后,你会得到两个核心数据:

参数 含义 典型值(示例)
内参矩阵 mtx 包含焦距 fx, fy 和主点 cx, cy [[fx, 0, cx], [0, fy, cy], [0, 0, 1]]
畸变系数 dist 包含 k1, k2, p1, p2, k3 [k1, k2, p1, p2, k3]

内参矩阵里的 fx 和 fy,理想情况下应该很接近。如果差太多,说明镜头可能有装配误差。畸变系数里,k1 和 k2 是径向畸变,p1 和 p2 是切向畸变。内窥镜的径向畸变通常很大,k1 可能是负的,表示桶形畸变。

经验之谈: 我遇到过一台内窥镜,k1 值达到 -0.45,画面边缘的直线都弯成弧线了。标定后校正效果非常明显,医生看了直说“这画面终于正常了”。

3.6 保存与加载标定结果

标定一次,终身受用。我习惯把结果存成 .npz 文件,方便后续加载。

# 保存
np.savez('calibration_data.npz', mtx=mtx, dist=dist, rvecs=rvecs, tvecs=tvecs)

# 加载
data = np.load('calibration_data.npz')
mtx = data['mtx']
dist = data['dist']

这样下次直接用,不用重新标定。不过要注意,如果换了镜头或者调整了焦距,必须重新标定。

3.7 常见问题与避坑

  • 角点检测失败: 检查棋盘格是否完整出现在画面中,光照是否均匀。我建议用漫射光源,避免强光直射。
  • 标定误差大: 重投影误差超过 0.5 像素就算偏大了。这时候要检查图像数量是否足够,姿态是否多样。
  • 畸变校正后图像边缘有黑边: 这是正常的,因为校正后图像需要重新映射。可以用 cv2.getOptimalNewCameraMatrix 调整。
避坑指南: 我曾经用手机拍标定板,结果自动对焦导致每张图的焦距都不一样。标定出来的内参乱七八糟。后来我固定了焦距,问题就解决了。内窥镜一般没有自动对焦,但也要确认一下。

3.8 小结

这一节我们完成了棋盘格标定的完整流程。你拿到了内参和畸变系数,这是后续畸变校正和色彩还原的基础。下一节,我会带你用这些参数做实际的图像校正,看看效果到底怎么样。

记住:标定不是一次性的工作。每次设备有变动,都要重新标定。这是保证精度的底线。