资讯动态

OpenCV相机标定实战:张正友棋盘法从原理到避坑指南

发布时间:2026/9/9 12:01:15 来源:尧图企业网站定制
简介基于OpenCV实现的张正友相机标定完整工程包面向计算机视觉初学者、课程设计及毕业设计学生。压缩包共61个文件大小约17.07MB以C源码、Visual Studio工程文件、棋盘图、标定图及编译输出文件为主核心代码Calibration.cpp带有详细中文注释含14张bmp棋盘图和14张不同角度jpg标定图便于直接验证。工程实现从棋盘角点检测、内参与外参计算、旋转平移矩阵求解到标定效果评价和畸变矫正的完整链路一键运行即可得到标定结果文本并输出矫正后的棋盘图。已有2057人学习资源附带的工程结构清晰、依赖简单可帮助读者跳过繁琐的环境配置快速理解张正友标定法的代码实现与实验细节。 做视觉项目最绕不开的一个环节就是相机标定。我第一次做的时候网上搜到一堆“张正友标定法”的资料配着棋盘图代码看起来也不长但真跑起来问题一堆角点检测失败、重投影误差居高不下、棋盘图打印出来不平整导致数据全废……今天这篇就是把这些年踩过的坑一次说清楚。利用OpenCV的calibrateCamera配合棋盘图我们能在半小时内拿到相机的内参矩阵和畸变系数这套方法适合入门机器视觉的新手也适合老手快速校准新相机。相机标定的本质是给镜头“量体裁衣”。每颗镜头在出厂时都存在一定的畸变尤其是广角和低端镜头画面边缘的直线会明显弯曲。如果不做校正你后面做尺寸测量、二维码定位、机械臂抓取误差会迅速放大到不可接受的程度。OpenCV里的张正友标定法就是目前最通用、最省事的一套解决办法它只需要一张棋盘图、若干张不同角度的照片以及几十行代码就能把相机参数算出来。1. 先把原理说人话张正友标定法到底在标什么1.1 标定的数学本质其实是“解方程”很多人一看到标定就害怕觉得又是矩阵又是张量的。其实可以换个角度理解相机把一个三维点拍成二维像素点中间经过的变换是可以写成一个固定公式的。这个公式里有几个未知数包括焦距、主点光心在图像上的位置、以及畸变系数。标定做的事就是拿着若干张已知位置关系的棋盘格照片反推出这些未知数。具体来说OpenCV里的公式分为两部分。第一部分是理想的小孔成像模型三维世界坐标通过旋转和平移变换到相机坐标再用焦距投影到像素平面第二部分是畸变修正项OpenCV默认的畸变模型是k1、k2、p1、p2、k3五个系数前两个是径向畸变中间两个是切向畸变最后一个是高阶径向畸变。普通镜头用这五参数就够了鱼眼镜头则需要另外的模型。标定时我们输入的是棋盘角点的世界坐标和对应的像素坐标输出的是内参矩阵、畸变系数以及每张照片的外参相机相对棋盘的位置姿态。1.2 为什么标定板偏偏用棋盘图因为在计算机视觉里“角点”是最好检测的特征。黑白方格交替形成的角点在图像里的梯度响应很强而且多尺度下都稳定。OpenCV里一个findChessboardCorners函数就能把内角点全找出来不需要任何人工干预。如果把标定板换成圆点阵列或者二维码也能标定但检测算法的鲁棒性、亚像素精度、以及角点顺序的一致性都远不如棋盘格来得好处理。另外棋盘格是平面模板这意味着在构造世界坐标时可以把所有角点的Z坐标设置成0。这一个操作直接简化了求解过程张正友本人当年就是利用这个“平面约束”推导出初始解的。所以你在网上下载的标定代码里只要看到objp全设置为0的基本就是张正友法核心思想就是先用平面单应矩阵估算初值再用最大似然估计做非线性优化。1.3 从世界坐标到像素坐标就一条链路搞清楚这条链路后面的代码就不会瞎调参数了。一个棋盘角点先用它所在的棋盘平面定义世界坐标单位毫米通过外参矩阵由旋转向量rvec和平移向量tvec组成变换到相机坐标系再由内参矩阵投影到图像平面最后套上畸变模型就得到了我们实际看到的像素坐标。标定算法的逻辑就是反着走我们已知角点的世界坐标通过角点检测知道它的像素坐标于是反推出内参、外参和畸变系数。所以拍照时棋盘要出现在画面不同位置是因为不同位置的角点能提供不同的约束信息如果棋盘永远只出现在画面正中央求解出来的畸变系数就会非常不靠谱。理解了这条链路你会发现标定并非什么魔法它只是一个用足够多观测数据去拟合相机参数的过程。2. 准备工作棋盘图生成与环境搭建2.1 环境安装别在这一步卡太久我见过太多人卡在环境安装上其实OpenCV的Python环境配置很简单一条命令就搞定。我常用的安装命令如下pip install opencv-python opencv-contrib-python numpyopencv-python是核心库opencv-contrib-python则包含了一些扩展模块。如果你要做鱼眼标定或者其他高级功能contrib包是少不了的。numpy用于处理数组和矩阵标定过程中做数据拼接时非常依赖它。如果你用的是Anaconda也可以先用conda新建一个独立环境再pip安装避免和系统Python打架。装好之后用一句python -c import cv2; print(cv2.__version__)验证一下。能打印出版本号说明环境已经OK。注意OpenCV的版本迭代比较快Python API的核心函数名字基本没变过所以版本差异对本文的流程影响不大。2.2 用OpenCV直接生成棋盘图市面上的棋盘图下载链接很多但别人打印好的往往尺寸不合适还要担心分辨率不够。我习惯直接用OpenCV自己画一张代码很短也方便调整格子的数量和大小。import cv2 import numpy as np # 内角点数量9列 x 6行 pattern_size (9, 6) # 每个格子的边长单位毫米按打印后实测填写 square_size 25.0 # 四周留白单位像素 margin 80 # 每个格子在图片中的边长单位像素 cell 60 board_w pattern_size[0] 1 board_h pattern_size[1] 1 img_w margin * 2 board_w * cell img_h margin * 2 board_h * cell chessboard np.full((img_h, img_w), 255, dtypenp.uint8) for i in range(board_h): for j in range(board_w): if (i j) % 2 1: x margin j * cell y margin i * cell chessboard[y:y cell, x:x cell] 0 cv2.imwrite(chessboard_9x6.png, chessboard)这段代码的原理很简单就是按行列遍历遇到黑白方块的规律就用numpy数组填充色块。生成出来的图片直接A4打印优先用激光打印机而不是喷墨因为喷墨打印的黑色块边缘容易洇墨角点定位会跟着偏移。打印后用游标卡尺量一下格子实际边长往往和设计值有0.1到0.5毫米的偏差这个实测值要填到代码的square_size里很大程度上决定了标定结果的绝对精度。2.3 照片采集的几条硬性规范棋盘图做出来之后拍照是决定成败的一步。我见过不少朋友兴致勃勃地拍了一张照片然后问为什么标定出来的误差那么大。一张照片根本不够因为未知参数太多而你提供的方程数量不足。我建议至少拍15到20张。具体拍摄方法记住四个字多、变、清、全。多就是张数要够变就是姿态要变化包括左右倾斜、上下俯仰、远近移动、旋转角度不要所有照片都是正对着拍要“歪着拍、斜着拍、偏着拍”清就是画面不能模糊快门速度要足够高全就是棋盘完整出现在画面中任何一边被截断都不行。我自己的习惯是让棋盘占画面的三分之一以上太小了角点定位精度不够。最容易被忽略的一点是镜头焦距。如果你用的是可自动对焦的工业相机或手机拍摄时一定要固定对焦不要每张照片让镜头重新对焦。标定模型假设相机内参是固定的一旦焦距中途发生变化等于用一套参数去拟合两种镜头状态结果必然是一团糟。用定焦镜头或者手动模式锁住焦距是最稳妥的做法。3. 标定代码逐段拆解从检测角点到输出参数3.1 整体流程先说清楚完整流程可以分成五步遍历照片、灰度化、检测角点、亚像素精化、调用calibrateCamera。看起来简单但每一步都有值得注意的细节。我把整体代码先贴出来再逐步解释容易踩坑的地方。import cv2 import numpy as np import glob # 1. 设置标定板参数 pattern_size (9, 6) # 内角点数: 9列 x 6行 square_size_mm 25.0 # 打印后实测的格子边长单位毫米 images glob.glob(calib_images/*.png) # 2. 构造世界坐标系下的角点坐标 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) objp[:, :2] objp[:, :2] * square_size_mm obj_points [] # 每张图片对应的3D角点坐标 img_points [] # 每张图片对应的2D像素角点坐标 # 3. 亚像素精化的终止条件 criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) # 4. 遍历图片检测角点 for fname in images: img cv2.imread(fname) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, corners cv2.findChessboardCorners(gray, pattern_size, None) if not ret: print(f角点检测失败跳过: {fname}) continue corners_sub cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) obj_points.append(objp) img_points.append(corners_sub) # 5. 执行标定 ret, mtx, dist, rvecs, tvecs cv2.calibrateCamera( obj_points, img_points, gray.shape[::-1], None, None) print(f重投影误差RMS: {ret:.4f}) print(内参矩阵:\n, mtx) print(畸变系数:\n, dist.ravel()) # 6. 保存结果 np.savez(calibration_result.npz, mtxmtx, distdist, rmsret, image_sizegray.shape[::-1])3.2 findChessboardCorners的参数理解findChessboardCorners是标定的第一步也是最容易出问题的一步。第一个坑是pattern_size的填写它指的是内角点数量也就是棋盘格里黑白块交界处的点而不是棋盘格总数。比如你打印的棋盘是10×7个格子那么内角点就是9×6。如果填错函数直接检测失败。第二个坑是返回值的判断。OpenCV里的findChessboardCorners检测失败时返回的corners是None所以不能直接在后续调用cornerSubPix否则会报错。我的代码里先用ret判断失败就跳过这样可以避免少量照片不合格导致整个流程崩溃。第三个坑是flags参数比如CALIB_CB_ADAPTIVE_THRESH和CALIB_CB_NORMALIZE_IMAGE。这两个flag在光照不均的图片上确实有效但也会增加检测耗时。我建议先不加flags试一次如果检测失败再加自适应阈值。在多数正常光线下默认参数已经工作得很好了盲目加flag有时反而会因为过度处理把角点搞丢。3.3 cornerSubPix亚像素精化这一步不能省英里的counties数量只有几十个但它在标定里的作用非常关键。findChessboardCorners检测到的是整数像素坐标而真正的角点位置往往落在某个像素内部的亚像素级位置。如果不做精化角点坐标会带有几个像素的量化误差标定出来的畸变系数也会偏差更大。cornerSubPix的原理是在角点周围的小窗口内利用灰度梯度的正交关系迭代寻找最优点。winSize参数(11,11)表示搜索窗口大小criteria是迭代停止条件一般设置最大迭代30次、精度0.001就够用。这个函数处理起来非常快几十张图片也就几秒钟的事强烈建议不要跳过。3.4 calibrateCamera输出参数怎么理解calibrateCamera返回的东西有点多但每个都有明确的含义。ret是最直观的一个它表示重投影误差单位是像素计算方式是把标定得到的参数再代回去将世界坐标重投影到图像平面然后统计与检测角点之间的RMS距离。这个值越小越好一般低于0.1像素就可以认为标定结果很可靠0.1到0.3之间还能用超过0.5就需要检查采集流程了。mtx是3×3内参矩阵包含fx、fy以像素为单位的焦距和cx、cy主点坐标。在网络摄像头或普通镜头上cx、cy接近图像中心但不会完全等于中心因为装配过程很难做到绝对居中。dist是畸变系数按顺序排列为k1、k2、p1、p2、k3。rvecs和tvecs是每张照片的外参代表棋盘坐标系到相机坐标系的旋转和平移在做机械臂抓取、AR叠加这一类需要位姿估计的场景时后面这两组数据就是现成的初值。3.5 用结果验证标定质量拿到内参和畸变系数之后强烈建议把结果可视化出来用直觉判断是否合理。最简单的方法是调用undistort函数直接把原图校正一遍看看画面边缘的直线是否变直。如果校正后的图片在边缘出现严重扭曲或者拉伸说明标定大概率出了问题特别是畸变系数值异常大的时候比如k1超过1基本可以判断square_size填错或者角点检测有误。还可以计算单张图片的重投影误差把每张照片的rvec和tvec代入投影公式与检测角点对比。不过日常使用中直接用全局的RMS值判断就够了不需要写额外的误差统计代码。4. 我踩过的坑常见问题与排查清单4.1 角点检测失败十有八九是这几种原因角点检测失败是最打击新手的第一次看到“检测失败”四个字很容易怀疑自己的OpenCV装错了。其实多数情况就三类原因。第一类是pattern_size填错注意是内角点数量不是格子数量。第二类是光照问题比如反光、高光把角点区域糊成了一片白或者阴影把格子边界吃掉了这类场景可以用CALIB_CB_ADAPTIVE_THRESH缓解但最根本的办法是换个光照均匀的位置重新拍。第三类是不符合拍摄规范比如棋盘太平几乎和画面平行、棋盘被截断、图像严重模糊等这几种情况根本没法可靠检测直接删掉重拍就行。还有一个隐藏原因有些打印机打印出来的格子尺寸和设计值偏差很大导致棋盘格的长宽比例不对极端情况下也会检测失败所以有条件就尽量用清晰度高的打印机。4.2 重投影误差太大从哪里排查误差超过0.5像素时我一般依次排查五个方向。第一square_size毫米值是否填错这直接影响世界坐标尺度填错后误差会系统性地偏大。第二是否混用了不同焦距的照片如果中途变焦所有照片就都白拍了。第三棋盘图是否平整贴在硬板上纸面有折痕会导致角点位置在毫米级别上偏移。第四角点检测返回的顺序是否一致正常情况下objp的构造顺序和findChessboardCorners的返回顺序是匹配的如果你自己改了objp生成方式千万要注意行列顺序。第五照片数量是否过少或者姿态太单一至少留15张且分布在画面不同区域。按照这五步排查大多数误差问题都能解决。4.3 后续可以怎么扩展标定得到的mtx和dist在很多场景都能直接复用。如果想在不同分辨率下使用需要按比例重新缩放内参比如从1280×960换到640×480fx、fy、cx、cy都除以2即可。如果发现镜头在自动对焦模式下拍摄标定结果会不稳定建议换用定焦镜头或者锁焦模式。此外使用鱼眼镜头时请改用cv2.fisheye.calibrate它的畸变模型是针对鱼眼的普通模型的五参数在超广角下会失真。最后分享一个我自己的习惯每次标完定我会把标定结果和当时的图像分辨率一起存成npz文件文件名里带上镜头型号和日期。换镜头、调焦距的时候就回来看一眼旧参数心里大概有数新参数应该落在什么范围。这个习惯帮我排掉过好几个因为参数异常导致的定位故障也算是一个低成本的经验总结了。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价