4. 棋盘格与角点检测
棋盘格,说白了就是张正友标定法的“尺子”。
你想想看,我们要从一堆照片里算出相机的内参和外参,总得有个已知尺寸的参照物吧?棋盘格就是干这个的。它的角点——那些黑白方块的交界处——就是我们用来建立世界坐标和像素坐标对应关系的锚点。
这一章,我们就来聊聊怎么设计好这把“尺子”,以及怎么用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 本章知识结构图
下面这张图,把棋盘格角点检测的完整流程串起来了。从设计到优化,一步都不能少。
4.5 避坑总结
最后,我把这些年踩过的坑,浓缩成几条实用建议:
- 棋盘格别太小:在图像里至少占 1/4 面积,否则角点检测容易失败。
- 亚像素优化别省略:我见过有人直接用像素级角点去标定,结果重投影误差 0.5 个像素起步。加上亚像素优化,能压到 0.1 以内。
- 多拍几张不同角度的:张正友标定法需要至少 10~20 张不同姿态的棋盘格图像。别偷懒,拍个 15 张以上。
- 每次检测都检查结果:用
drawChessboardCorners把角点画出来看一眼。有时候 OpenCV 找到了角点,但顺序是乱的,或者有误检。
1. 打印棋盘格 → 2. 贴到硬板上 → 3. 拍 15~20 张照片 → 4. 批量跑 findChessboardCorners → 5. 亚像素优化 → 6. 保存角点坐标到文件。整个过程写成一个脚本,一键执行。