棋盘格相机标定全流程:OpenCV内参畸变校正与误差排查
简介:面向计算机视觉初学者及需要落地相机标定工程的开发者,这份资料以张正友标定法为主线,提供可直接运行的OpenCV实现与配套采集数据,着力解决从原理理解到参数输出之间的实践断层。压缩包共107个文件,约95.67MB,主体为60张jpg标定图,另含cpp源码、VS2015工程与解决方案文件、棋盘格打印pdf、bat批处理脚本、mat结果数据及编译中间文件;手机与摄像头两组拍摄图可用于对比不同成像条件下的标定表现。工程注释较完整,支持一键运行,涵盖内参、外参、旋转与平移矩阵求解,并给出标定效果评价及原始棋盘图矫正,标定结果与Matlab较为接近。已有6820人学习下载,适合课程实验、项目预研和算法复核时作为可复现的参考样例。
1. 一张棋盘格照片背后的标定账:棋盘格相机标定能解决什么
相机标定这件事,很多人第一次翻车不是算法不会写,而是棋盘格打印比例错了、图片拍得太正,最后重投影误差看着不大,实际测距就是偏。这份资源把 OpenCV 实现、棋盘格打印 PDF 和测试图片放在一起,解决的是从“拍板子”到“拿到内参、畸变系数并完成校正”的完整闭环,适合做机器人抓取、AR 贴图、双目测距、工业视觉入门的人。它不追求标定场那套昂贵设备,核心就是棋盘格平面标定:打印一张板、拍十几张姿态、跑一遍角点检测和calibrateCamera,再把参数落到实际图像上验证。下面按我实际复现的顺序拆,先讲清楚内参和外参在解什么,再落到打印、拍摄、代码和排错。
2. 相机模型与标定量:内参、外参、畸变在解什么
2.1 针孔模型:像素坐标背后是一条三维射线
棋盘格标定不是“把图片变清晰”的滤镜,它解的是相机成像的几何参数。最常用的针孔模型把三维点从相机坐标系投到像素平面,核心是一个 3×3 内参矩阵:
K = [[fx, skew, cx], [0, fy, cy], [0, 0, 1]]fx、fy是焦距在像素尺度上的表达,cx、cy是主点,通常接近图像中心但不一定正好在中心。skew在大多数现代相机里接近 0,OpenCV 默认也不强制估计它。标定要拿到的就是这几个数,外加每张图片对应的旋转向量rvec和平移向量tvec。外参不是相机固有属性,它描述的是“这张棋盘格在拍摄那一刻相对相机的位置”,所以同一台相机拍不同姿态,内参应该稳定,外参每张都变。
很多人把fx和镜头焦距毫米数混在一起。镜头标称 6mm、8mm 是物理焦距,fx是像素焦距,二者差一个像元尺寸的换算关系。实际调试时不要拿镜头铭牌去反推fx,而是看标定结果是否稳定:同一台相机、同一分辨率、同一焦距下,多次标定的fx、fy应该接近,cx、cy不应该跑到图像外面。如果fx和fy相差很大,先怀疑图片姿态太单一或者棋盘格不是正方形网格,而不是急着改代码。
2.2 畸变模型:径向占主导,切向别忽略
针孔模型是直线投影,但真实镜头会让直线在图像边缘弯掉。OpenCV 的常用畸变模型是:
x_dist = x(1 + k1*r^2 + k2*r^4 + k3*r^6) + 2*p1*x*y + p2*(r^2 + 2*x^2) y_dist = y(1 + k1*r^2 + k2*r^4 + k3*r^6) + p1*(r^2 + 2*y^2) + 2*p2*x*yk1、k2、k3是径向畸变,广角镜头通常k1为负,表现为桶形畸变;长焦或某些镜头可能出现枕形。p1、p2是切向畸变,来自镜头和传感器不完全平行,量级一般比径向小,但在高精度测量里不能随手丢掉。普通工业镜头用 5 参数(k1,k2,p1,p2,k3)足够,鱼眼镜头不要硬套这个模型,OpenCV 有单独的cv2.fisheye模块,模型和初始化方式都不一样。
这里有一个选型判断:如果只是做 1080p 视频里贴个 AR 标签,5 参数畸变加一次undistort就够;如果要做亚像素级测量,必须看校正后边缘直线的弯曲程度,而不是只看 RMS。RMS 是重投影误差的均方根,它衡量的是“三维棋盘格角点投回图像后和检测角点的平均偏差”,单位是像素。RMS 小不代表畸变模型适合你的镜头,尤其画面边缘样本少的时候,RMS 会被中心区域拉低。
2.3 棋盘格平面标定:闭式初值加最大似然
棋盘格平面标定的思路不复杂:棋盘格是平面,世界坐标可以令Z=0,于是单应矩阵把平面点映射到像素点。每张图提供一个单应矩阵,多个姿态的单应矩阵给内参矩阵施加约束,先解出一个闭式初值,再用最大似然优化把所有参数一起调整,让重投影误差最小。OpenCV 的calibrateCamera做的就是这套流程,内部会先用初值跑优化,所以输入图片的质量比算法细节更决定结果。
为什么这套方法能成为入门首选?棋盘格容易打印,角点定义明确,OpenCV 的findChessboardCorners和cornerSubPix成熟,测试图片可以直接复现。圆点标定板在远距离、部分遮挡、大视场下可能更稳,但对打印精度和检测器要求更高;这份资源带的是棋盘格 PDF 和测试图片,先把棋盘格跑通,比一上来换标定板更划算。
2.4 标定前先盘点三个参数
动手前我会先建一张记录表,不然后面参数对不上,排错会变成玄学。至少要记录:图像分辨率、镜头焦距是否可变、棋盘格打印后的实际方格边长。分辨率决定cx、cy的量级;变焦镜头如果中途拧过,内参就变了,必须重新标;方格边长填错,外参平移量会整体缩放,重投影误差却可能看起来正常,这是最隐蔽的坑。
| 记录项 | 示例填写 | 影响 |
|---|---|---|
| 图像分辨率 | 1920×1080 | 主点范围和像素焦距量级 |
| 镜头类型 | 定焦 6mm | 变焦镜头需固定焦距后标定 |
| 棋盘格内角点 | 9×6 | 检测函数的 pattern size |
| 实测方格边长 | 25.0mm | 平移向量尺度和重投影误差 |
| 拍摄距离 | 0.4~0.8m | 棋盘格占比和姿态覆盖 |
这张表不是形式主义。后面代码里的PATTERN_SIZE和SQUARE_SIZE都从这里来,测试图片换一批、镜头换一个,记录表也要跟着换。标定结果一旦要交付给下游,记录表比一段没有上下文的K矩阵更有用。
3. 棋盘格PDF与测试图片:打印、拍摄、命名三步落地
3.1 打印PDF:比例、平整度与反光
棋盘格 PDF 最容易翻车的地方是打印比例。打印对话框里默认可能有“适应页面”“缩放以适合”,这会把方格边长改掉。正确做法是选实际大小或 100% 比例,纸张用 A4,打印后拿尺子量连续 10 个方格的总长度再除以 10,得到实际边长。不要直接相信 PDF 上标的 25mm,打印机进纸误差、纸张热胀冷缩都会让实际值差一点。这个实测值要填进代码的SQUARE_SIZE,单位统一用米,25mm 就写 0.025。
第二是平整度。打印纸用胶带贴在硬纸板或亚克力板上,四边和中间都要贴住,不能拱起来。棋盘格一旦弯曲,平面假设就被破坏,角点检测看着没问题,重投影误差会飘。第三是反光。亚光纸比铜版纸好,表面覆膜容易在灯光下形成高光,角点被吃掉。拍摄时让光源从两侧打,不要正对棋盘格打闪光灯。棋盘格如果脏了、折了,直接重新打印,不要试图用图像处理补,补出来的角点坐标不可信。
3.2 拍摄:姿态覆盖比数量更重要
我见过有人拍 50 张,全部是棋盘格正对镜头、只平移不旋转,标定结果依然不稳。原因是平面标定需要不同姿态提供约束,纯平移会让单应矩阵退化。合格的一组图片至少覆盖:棋盘格在画面中心、左上、右上、左下、右下;每张有 20°~45° 的倾斜;远近各来几张,让棋盘格占画面 1/3 到 2/3;绕光轴旋转几个角度,比如 0°、45°、90°。数量上 15~25 张通常够用,但姿态覆盖比数量重要。如果只能拍 8 张,也要保证四个角和中心都有。
拍摄时相机要固定焦距。手机自动对焦、变焦镜头中途拧动、工业相机换光圈,都会改变内参。手持拍摄可以,但快门速度要够,运动模糊会让cornerSubPix的亚像素结果偏。棋盘格不要过曝,黑白交界清晰但不溢出。每拍一张检查一下角点是否完整,边缘被裁掉的棋盘格不要用,内角点必须全部落在图像内。
3.3 测试图片:命名、筛选与目录结构
资源里的测试图片可以直接跑,但我建议按自己的拍摄批次重新组织目录,不要把不同镜头、不同分辨率的图片混在一个文件夹。下面这套目录和重命名命令是我常用的,先把原始图放raw/,筛过的图放test_images/,角点可视化放debug/:
mkdir -p raw test_images debug # 批量重命名,保留原始顺序,避免中文名和空格影响读取 i=0 for f in raw/*.jpg; do i=$((i+1)) cp "$f" "test_images/calib_$(printf '%03d' $i).jpg" done这段命令做两件事:建立三个目录,把原始图复制成calib_001.jpg这种无空格、无中文的连续文件名。printf '%03d'保证排序稳定,后面 Python 里用sorted(glob)时不会因为文件名顺序乱掉。复制而不是移动,是为了保留原始图,后面筛图时还能回头查。debug/用来存drawChessboardCorners的可视化结果,哪张图角点检测错了,一眼能看出来。
筛图的标准很简单:棋盘格完整、不模糊、不过曝、内角点数量对得上。同一批里如果有几张明显失焦,不要舍不得,删掉比硬标好。文件名里可以带距离和角度,比如calib_005_50cm_30deg.jpg,后面看单张误差时能对应到拍摄条件。测试图片不是越多越好,重复姿态的图片对优化贡献很小,反而可能把误差分布拉偏。
4. OpenCV标定全流程:从角点检测到calibrateCamera
4.1 角点检测与亚像素优化
先把最小可运行代码跑通。下面这段按 9×6 内角点、25mm 方格边长写,实际用 PDF 时以你手上棋盘格的内角点为准。注意PATTERN_SIZE是内角点数量,不是方格数量,9×6 内角点对应 10×7 个方格。
import cv2 import numpy as np from pathlib import Path # 内角点数量:列×行,必须和棋盘格 PDF 一致 PATTERN_SIZE = (9, 6) # 打印后实测的方格边长,单位米;25mm 写 0.025 SQUARE_SIZE = 0.025 IMG_DIR = Path("test_images") DEBUG_DIR = Path("debug") DEBUG_DIR.mkdir(exist_ok=True) # 亚像素迭代终止条件:30 次或步长小于 1e-3 criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 1e-3) # 世界坐标:Z=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) * SQUARE_SIZE objpoints, imgpoints = [], [] image_size = None for p in sorted(IMG_DIR.glob("*.jpg")): img = cv2.imread(str(p)) if img is None: print("读取失败", p.name) continue gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 自适应阈值 + 归一化,适合光照不均的测试图 found, corners = cv2.findChessboardCorners( gray, PATTERN_SIZE, flags=cv2.CALIB_CB_ADAPTIVE_THRESH + cv2.CALIB_CB_NORMALIZE_IMAGE ) if not found: print("未找到角点", p.name) continue # 亚像素优化窗口 11×11,搜索窗口 -1 表示按默认 corners = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) objpoints.append(objp.copy()) imgpoints.append(corners) image_size = gray.shape[::-1] vis = cv2.drawChessboardCorners(img.copy(), PATTERN_SIZE, corners, found) cv2.imwrite(str(DEBUG_DIR / f"corners_{p.name}"), vis) print("有效图片数", len(objpoints), "图像尺寸", image_size)这段代码的关键不是findChessboardCorners本身,而是顺序和参数。objp用mgrid生成规则网格,乘SQUARE_SIZE后单位是米;objpoints每张图复制一份,因为后面优化不会改它,但复制能避免引用问题。cornerSubPix必须在灰度图上做,窗口(11,11)对 1080p 棋盘格通常够,棋盘格占画面很小时可以适当放大到(15,15),但不要过大,否则会把邻近角点拉进来。image_size取gray.shape[::-1],得到(width, height),传给calibrateCamera的顺序不能反。
如果某张图found=False,先看debug/里有没有可视化。没有可视化说明检测阶段就失败了,常见原因是PATTERN_SIZE写成了方格数而不是内角点数,或者打印比例导致角点太密。不要急着加cv2.CALIB_CB_FAST_CHECK,那个标志会降低检测率,适合视频流粗筛,不适合静态标定图。
4.2 标定主循环:对象点、图像点与RMS
角点齐了之后,标定本身只有一行核心调用:
# 标定:输入世界坐标、图像坐标、图像尺寸 ret, K, dist, rvecs, tvecs = cv2.calibrateCamera( objpoints, imgpoints, image_size, None, None ) print("RMS 重投影误差", ret) print("内参 K=\n", K) print("畸变系数 dist=", dist.ravel()) # 逐张计算重投影误差,定位坏图 total_error = 0.0 per_view = [] for i in range(len(objpoints)): imgpts, _ = cv2.projectPoints( objpoints[i], rvecs[i], tvecs[i], K, dist ) err = cv2.norm(imgpoints[i], imgpts, cv2.NORM_L2) / len(imgpts) per_view.append((i, err)) total_error += err print("平均重投影误差", total_error / len(objpoints)) print("单张误差", per_view) # 保存参数,后续校正直接加载 np.savez("camera_calib.npz", K=K, dist=dist, rvecs=rvecs, tvecs=tvecs, rms=ret)calibrateCamera返回的ret是 RMS,单位像素,通常希望小于 1,做测量时尽量压到 0.5 以下,但不要为了追小误差把图片删到只剩中心几张。rvecs、tvecs是每张图的外参,projectPoints用它们把世界坐标投回图像,和检测到的imgpoints做 L2 距离,再除以角点数,得到单张平均误差。逐张看误差比只看总 RMS 有用:如果总 RMS 是 0.6,但某一张是 3.2,说明那张图有问题,可能是模糊、角点错位或者棋盘格不平。
np.savez把K、dist、外参和 RMS 一起存下来,文件名不要用中文。后面做校正、做测距、做双目,都从这个文件读,避免每次重新标。注意dist的形状可能是(1,5),用的时候dist.ravel()或直接传都可以,但自己写矩阵运算时记得展平。
4.3 畸变校正与参数落盘
标定完必须做一次校正验证,不然参数错了也看不出来。下面用getOptimalNewCameraMatrix加undistort:
import cv2 import numpy as np data = np.load("camera_calib.npz") K, dist = data["K"], data["dist"] img = cv2.imread("test_images/calib_001.jpg") h, w = img.shape[:2] # alpha=1 保留全部像素,边缘会有黑边;alpha=0 裁掉黑边但会丢视场 newK, roi = cv2.getOptimalNewCameraMatrix(K, dist, (w, h), 1, (w, h)) undist = cv2.undistort(img, K, dist, None, newK) # roi 是有效区域,需要裁切时再用 x, y, rw, rh = roi undist_crop = undist[y:y+rh, x:x+rw] cv2.imwrite("undist_full.jpg", undist) cv2.imwrite("undist_crop.jpg", undist_crop) print("newK=\n", newK) print("roi=", roi)alpha=1表示保留所有校正后的像素,适合检查边缘畸变;alpha=0会把黑边裁掉,适合直接出图,但会损失视场。getOptimalNewCameraMatrix返回的新内参newK要和undistort配套用,不能拿旧K去裁切。roi是有效区域,裁切时注意x,y可能为负?OpenCV 返回的 roi 一般是非负的,但不同版本行为略有差异,裁切前打印确认。校正后看直线:门框、桌边、棋盘格边线,如果边缘还是弯的,要么畸变模型不够,要么图片边缘样本太少。
5. 避坑与排查:重投影误差大、角点错位、参数发散
5.1 角点检测失败:先怀疑内角点数量和打印比例
现象:findChessboardCorners对大部分图片返回False,或者只在某几张正对图片上成功。原因通常不是算法坏了,而是PATTERN_SIZE填错。棋盘格 PDF 上数的是方格数,代码要的是内角点数,10×7 个方格对应 9×6 个内角点。另一个原因是打印缩放,方格太小导致角点粘连,或者打印太大超出画面。解决:先用drawChessboardCorners把检测结果画出来,拿一张图手动数内角点;打印时选 100% 比例,量实际边长;如果棋盘格占画面太小,靠近拍或者换更大纸张。不要用cv2.CALIB_CB_FAST_CHECK硬扛,静态图不需要。
5.2 重投影误差超过1像素:逐张看误差,不要只看RMS
现象:总 RMS 看着 0.8,但实际测距偏,或者某几张图误差明显高。原因可能是SQUARE_SIZE和实际打印边长不一致、图片有运动模糊、角点被高光吃掉、不同焦距的图片混在一起。解决:先量 10 个方格总长除以 10,更新SQUARE_SIZE;再打印单张误差,把误差大于 2 像素的图挑出来看debug/可视化;模糊图、过曝图直接剔除;确认拍摄过程中没有拧变焦环。不要为了让 RMS 变小反复调cornerSubPix窗口,窗口只影响亚像素精度,救不了错位的角点。
5.3 焦距主点离谱:姿态太正、图片太少
现象:fx和fy差很多,cx、cy跑到图像角落,或者多次标定结果跳变。原因:棋盘格全部正对镜头,单应矩阵约束退化;图片数量太少,优化没有足够观测;棋盘格只占画面中心一小块,边缘畸变没有样本。解决:补拍四个角和倾斜姿态,每张倾斜 20°~45°,让棋盘格覆盖画面不同区域;至少保留 12~15 张有效图;如果镜头是广角,边缘一定要有棋盘格。标定不是解方程越多越好,而是观测要覆盖待估参数的影响范围。
5.4 校正边缘拉丝:畸变模型和alpha用错
现象:undistort后图像边缘出现拉伸、黑边很大,或者裁切后视场小得没法用。原因:alpha参数选错,alpha=0会裁掉大量边缘;鱼眼镜头用了普通 5 参数模型,边缘怎么调都拉不直;newK和K混用。解决:先用alpha=1看全图,确认边缘畸变是否被拉直;鱼眼镜头换cv2.fisheye.calibrate和cv2.fisheye.undistortImage;裁切时用getOptimalNewCameraMatrix返回的roi,不要自己拍脑袋定边界。校正后拿一张带直线的图复测,直线弯不弯比参数表更直观。
6. 验收标定结果的三个动作:误差分布、留一验证与参数冻结
标定跑完不等于能用,我验收时只看三件事。第一,单张重投影误差分布:总 RMS 小于 1 只是及格线,把每张图的误差打印出来,超过 2 像素的图回到debug/看角点,别让一张坏图拉低整体可信度。第二,留一验证:把图片分成两组,各标一次,比较fx、fy、cx、cy和k1、k2的差异,差异大说明样本覆盖不够,补拍比调参有用。第三,参数冻结后复测:用camera_calib.npz里的K、dist去校正一张没参与标定的图,看门框、桌边、棋盘格边线是否直,再看getOptimalNewCameraMatrix的roi是否合理。
| 检查项 | 看什么 | 处理 |
|---|---|---|
| 单张误差 | 是否有孤立高误差图 | 剔除模糊、过曝、角点错位图 |
| 留一验证 | 两组内参是否接近 | 差异大就补拍边缘和倾斜姿态 |
| 校正复测 | 边缘直线是否拉直 | 鱼眼换模型,普通镜头查 alpha |
| 参数落盘 | npz 是否含 K、dist、RMS | 交付时带记录表和测试图 |
那张表我一般会贴在项目目录里,谁接手都能对上。从那以后我每次标定都强制走一遍“逐张误差、留一验证、冻结复测”,不再只看一行 RMS 就收工。希望帮到你。
本文还有配套的精品资源,点击获取