4、棋盘格与角点检测:棋盘格设计原则、OpenCV角点检测函数、亚像素角点优化

棋盘格,说白了就是张正友标定法的“尺子”。

你想想看,我们要从一堆照片里算出相机的内参和外参,总得有个已知尺寸的参照物吧?棋盘格就是干这个的。它的角点——那些黑白方块的交界处——就是我们用来建立世界坐标和像素坐标对应关系的锚点。

这一章,我们就来聊聊怎么设计好这把“尺子”,以及怎么用OpenCV又快又准地把这些角点找出来。

4.1 棋盘格设计原则:别在第一步就翻车

棋盘格的设计,看似简单,其实坑不少。我在项目里见过有人随便打印一张棋盘格就开始标定,结果标出来的参数一塌糊涂。嗯,这里面的门道,我跟你捋一捋。

4.1.1 核心参数:内角点 vs 方格数

首先,你得搞清楚一个概念:我们检测的是内角点,不是棋盘格的所有格子角。

比如一个 9×6 的棋盘格,指的是内角点有 9 列、6 行。对应的方格数其实是 10×7。OpenCV 的 findChessboardCorners 函数,第一个参数传的就是内角点的尺寸 (9,6),千万别搞反了。

经验公式:
内角点数 = (方格列数 - 1) × (方格行数 - 1)
例如:10×7 的方格 → 内角点为 9×6

4.1.2 方格尺寸:要精度,就别偷懒

方格的实际物理尺寸(比如 30mm × 30mm)必须精确测量。我见过有人直接用“A4纸打印的,大概是25mm吧”这种说法……千万别这么干。

  • 用游标卡尺量:量一整排方格的宽度,除以方格数,取平均值。多量几次。
  • 单位统一:标定程序里填的尺寸单位是毫米,那所有数据都得是毫米。
  • 尺寸别太小:我个人建议方格边长在 20mm ~ 50mm 之间。太小了,相机离远点就看不清角点了。

4.1.3 棋盘格材质:平面度是命门

棋盘格必须贴在平整的硬板上。我曾经偷懒用普通打印纸贴在纸板上,结果标定出来的畸变系数全是错的。为什么?因为纸板一弯,棋盘格就不是一个平面了,张正友标定法的“平面假设”直接失效。

避坑指南:
不要用软纸、不要用带弯曲的亚克力板、不要贴在有纹理的墙面上。
最好的方案:用高精度打印机打印在相纸上,然后贴在铝合金板或玻璃板上。

4.2 OpenCV 角点检测:findChessboardCorners 实战

棋盘格准备好了,接下来就是让 OpenCV 帮我们找角点。这个函数我用了不下上千次,它的脾气我基本摸透了。

4.2.1 函数签名与参数

ret, corners = cv2.findChessboardCorners(image, patternSize, flags=None)
参数 说明 我的建议
image 输入的棋盘格图像,必须是8位灰度图或彩色图 建议先转灰度,速度更快
patternSize 内角点的行列数,如 (9,6) 注意是列×行,不是行×列
flags 自适应阈值、滤波等选项 默认 None 就够用,复杂场景再加
ret 布尔值,True 表示找到角点 一定要检查这个返回值
corners 检测到的角点坐标,形状为 (N,1,2) N 必须等于 patternSize 的乘积

4.2.2 标准调用流程

我习惯的写法是这样的,每一步都有它的道理:

import cv2
import numpy as np

# 1. 读取图像并转灰度
img = cv2.imread('chessboard.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 2. 定义棋盘格内角点尺寸
pattern_size = (9, 6)  # 9列,6行

# 3. 查找角点
ret, corners = cv2.findChessboardCorners(gray, pattern_size, None)

if ret:
    print(f"找到 {corners.shape[0]} 个角点")
    # 4. 绘制角点(调试用)
    cv2.drawChessboardCorners(img, pattern_size, corners, ret)
    cv2.imshow('Corners', img)
    cv2.waitKey(0)
else:
    print("未找到完整的棋盘格角点")

4.2.3 常见失败原因与对策

这个函数不是万能的。我遇到过好多次它返回 False,总结下来主要是这几个原因:

  • 光照不均匀:棋盘格一半亮一半暗。对策:用 cv2.adaptiveThreshold 预处理一下。
  • 棋盘格太小:在图像里只占了一小块。对策:让棋盘格占图像面积的 1/4 到 1/2。
  • 角度太倾斜:棋盘格平面与相机光轴夹角超过 45 度。对策:拍摄时保持夹角在 30 度以内。
  • 图像模糊:运动模糊或对焦不准。对策:保证快门速度足够,或者用 cv2.GaussianBlur 轻微去噪。
调试小技巧:
如果 findChessboardCorners 总是失败,可以试试设置 flags=cv2.CALIB_CB_ADAPTIVE_THRESH + cv2.CALIB_CB_NORMALIZE_IMAGE。这两个组合能解决大部分光照问题。

4.3 亚像素角点优化:从像素级到亚像素级

findChessboardCorners 找到的角点,精度是像素级的。说白了,就是整数坐标。但张正友标定法对精度的要求很高,像素级的误差会导致标定结果偏差很大。

这时候就需要亚像素角点优化了。它能将角点定位到小数点后几位,精度能提升一个数量级。

4.3.1 原理简述

亚像素角点检测的核心思想,是利用角点附近像素的梯度信息。在理想角点处,所有相邻像素的梯度方向都指向或背离该角点。通过迭代优化,找到那个让所有梯度向量与位置向量的点积之和为零的点。

嗯,听起来有点绕。你只要记住:它是在像素级角点周围的小窗口里,通过数学方法找到更精确的位置。

4.3.2 cornerSubPix 函数

cv2.cornerSubPix(image, corners, winSize, zeroZone, criteria)
参数 说明 常用值
image 灰度图 与 findChessboardCorners 输入一致
corners 初始角点坐标(像素级) findChessboardCorners 的输出
winSize 搜索窗口的半边长 (5,5) 或 (11,11)
zeroZone 避免奇异的死区,一般设为 (-1,-1) (-1,-1)
criteria 迭代终止条件 最大迭代次数 + 精度阈值

4.3.3 完整代码示例

# 接上一段代码,在找到角点之后
if ret:
    # 设置亚像素优化参数
    criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
    
    # 执行亚像素优化
    corners_sub = cv2.cornerSubPix(gray, corners, (11,11), (-1,-1), criteria)
    
    # 对比一下精度
    print("优化前第一个角点:", corners[0][0])
    print("优化后第一个角点:", corners_sub[0][0])

你会发现,优化后的坐标不再是整数了。比如从 (124, 256) 变成了 (124.352, 256.187)。这 0.3 个像素的差异,在标定结果上可能就是几个像素的重投影误差。

我的习惯:
每次标定前,我都会把亚像素优化的迭代次数设到 30 次,精度设到 0.001。虽然多花几毫秒,但标定结果的稳定性好很多。

4.4 本章知识结构图

下面这张图,把棋盘格角点检测的完整流程串起来了。从设计到优化,一步都不能少。

棋盘格角点检测完整流程 阶段一:棋盘格设计 • 内角点尺寸:如 (9,6) 对应 10×7 方格 • 方格尺寸:20~50mm,用游标卡尺精确测量 • 材质:相纸 + 铝合金/玻璃板,保证平面度 阶段二:图像采集与预处理 • 转灰度图 • 光照不均时用自适应阈值 • 保证棋盘格占图像 1/4 ~ 1/2 面积 阶段三:像素级角点检测 • cv2.findChessboardCorners(gray, patternSize) • 检查 ret 返回值 • 用 drawChessboardCorners 可视化验证 阶段四:亚像素角点优化 cv2.cornerSubPix(gray, corners, (11,11), (-1,-1), criteria) 输出 亚像素级角点坐标 用于后续标定

4.5 避坑总结

最后,我把这些年踩过的坑,浓缩成几条实用建议:

  • 棋盘格别太小:在图像里至少占 1/4 面积,否则角点检测容易失败。
  • 亚像素优化别省略:我见过有人直接用像素级角点去标定,结果重投影误差 0.5 个像素起步。加上亚像素优化,能压到 0.1 以内。
  • 多拍几张不同角度的:张正友标定法需要至少 10~20 张不同姿态的棋盘格图像。别偷懒,拍个 15 张以上。
  • 每次检测都检查结果:用 drawChessboardCorners 把角点画出来看一眼。有时候 OpenCV 找到了角点,但顺序是乱的,或者有误检。
我的工作流:
1. 打印棋盘格 → 2. 贴到硬板上 → 3. 拍 15~20 张照片 → 4. 批量跑 findChessboardCorners → 5. 亚像素优化 → 6. 保存角点坐标到文件。整个过程写成一个脚本,一键执行。