张正友标定法详解:从原理到OpenCV实操的相机标定指南
2026/9/9 8:56:45 网站建设 项目流程

简介:一份关于张正友标定法的MATLAB实现学习资料包,面向计算机视觉初学者和研究者,覆盖摄像机标定核心流程:棋盘格角点检测、单应矩阵估计、内外参数求解及畸变校正。通过配套脚本与数据文件,可对照经典论文逐步复现标定过程,适合用于三维重建、机器人导航等任务的前置学习。资源共27个文件,以MATLAB脚本(.m)、文本数据(.txt)、MATLAB自动保存文件(.asv)和文献原文(.caj)为主,压缩包仅85KB,轻量但完整。其中.m文件提供标定算法实现,.txt数据用于验证中间结果,.caj为基于单平面模板的摄像机定标研究原文,便于理论对照。已有497人浏览学习,是快速入门张正友标定法的实用参考。读者可获得可直接运行的实验脚本、分步数据文件和原始论文,省去从零搭建代码的麻烦,同时结合包内说明文档理解标定参数含义与优化技巧。

1. 为什么OpenCV和MATLAB里都有“张正友标定法”

做机器视觉或者三维重建的人,几乎都绕不开相机标定。早年我在学校第一次跑MATLAB的Camera Calibrator工具箱时,界面提示用的是“Zhang's Method”,当时没太在意,只觉得这个工具很神奇——把棋盘格在镜头前转几圈,就能算出相机的内参和畸变系数。后来真正入行做视觉定位项目,被各种重投影误差折磨得焦头烂额,才回过头来认真研究这位“张正友”到底做了什么,为什么他的方法能成为行业默认配置。

张正友标定法,本质上是一套介于传统标定和自标定之间的折中方案。说它传统,是因为它依然需要一块高精度的平面棋盘格作为参照物;说它自标定,是因为它不要求棋盘格和相机之间有任何预先知道的运动关系,你只需要在不同角度、不同距离下拍摄十几张照片,算法就能自动解出相机内参、外参以及畸变参数。

这套方法的厉害之处在于两点:一是操作门槛极低,普通人拿个打印的棋盘格就能完成标定;二是数学上非常优雅,先通过单应性矩阵做线性求解,再用最大似然估计做非线性优化,整个过程有严格的几何推导支撑。不管是OpenCV里的cv::calibrateCamera,还是MATLAB里的Camera Calibrator,底层核心思路都源自这里。

这篇文章我打算从原理、实操到踩坑,把张正友标定法完整拆一遍。如果你正在做视觉测量、机器人手眼标定、AR叠加、三维重建,或者只是想在OpenCV里把畸变矫正干净,这篇内容都值得你花时间看完。

2. 张正友标定法的核心思路与数学模型

2.1 相机的成像模型:从世界坐标到像素坐标

聊标定之前,必须先把相机的成像模型捋清楚。一个三维空间点,从世界坐标系到图像像素坐标系,要经历四次坐标变换。

世界坐标系里的点先通过旋转矩阵R和平移向量t,变换到相机坐标系,这一步得到的是相机看这个点的三维位置。接着,相机坐标系下的点通过针孔成像模型投影到归一化平面,x' = Xc/Zc, y' = Yc/Zc,这一步把三维点压成了二维坐标。然后理想的无畸变图像坐标再经过镜头畸变偏移,变成真实的物理图像坐标。最后一步,物理坐标通过像素坐标系的内参矩阵,换算成像素坐标。

把前两步和后一步合并,可以得到经典的投影方程:s * m = A * [R|t] * M,其中m是像素齐次坐标,M是世界齐次坐标,A是内参矩阵,R和t是外参,s是尺度因子。张正友标定法的整个推导,都是围绕这个方程展开的。

2.2 为什么选用棋盘格:角点检测的稳定性

标定板不是随便选的。选用棋盘格,核心原因是它的角点具有极高的定位稳定性。一个黑白交界的矩形角点,在图像里是一个鞍点,无论从哪个方向看,它都存在且位置唯一。相比之下,圆形标定板的圆心在透视投影下会发生偏移,除非你专门做椭圆拟合补偿,不然精度天然吃亏。

棋盘格的另一个优势是检测算法非常成熟。OpenCV里findChessboardCorners一把梭,亚像素角点再用cornerSubPix精修,整个过程鲁棒性极强。哪怕光照不均匀、有部分遮挡,只要格子数量够、对比度还行,基本都能检出来。

从数学上讲,棋盘格提供了一个平面上的大量共面特征点,而平面特征点正是张正友标定法能够简化的关键——它让每张照片的单应性矩阵都能被稳定估计出来,从而把内参和外参分离求解。

2.3 单应性矩阵:连接世界平面和图像平面的桥梁

张正友标定法最精妙的一步,是把标定板平面设定为世界坐标系的Z=0平面。这样,世界坐标系里的每个角点坐标都变成了[X, Y, 0, 1]T,投影方程里的旋转矩阵第三列r3乘上0之后直接消失,整个方程就降维成了一个3x3的单应性矩阵H。

这个H矩阵是内参矩阵和外参前两列的乘积组合:H = A * [r1, r2, t]。单应性矩阵描述了世界平面上的点和图像平面上的点之间的一一映射关系。每拍一张棋盘格照片,只要角点检测足够准,就可以通过DLT算法(直接线性变换)求出一组H。

这里有个非常重要的性质:旋转矩阵的列向量之间是单位正交的,即r1^T * r2 = 0,而且r1和r2的模长都是1。这两个约束条件,就是张正友从单张照片的内参约束中提取出内参矩阵信息的钥匙。

2.4 内参求解:从约束方程到闭式解

对每一张照片,H矩阵已经知道了,那么把H写成[h1, h2, h3],结合上面的正交约束,可以得到两个关于内参矩阵A的基本方程:

h1^T * A^{-T} * A^{-1} * h2 = 0 h1^T * A^{-T} * A^{-1} * h1 = h2^T * A^{-T} * A^{-1} * h2

令B = A^{-T} * A^{-1},这个B是一个对称矩阵,展开之后有6个未知数。由于B是对称的,可以把它重写成一个6维向量b,于是上面两个方程就转化成了关于b的线性方程组。每张照片提供两个方程,理论上只要拍摄3张以上不同角度的棋盘格照片,就能线性解出b。

解出b之后,再对B做Cholesky分解,就能恢复出内参矩阵A的各个元素,包括焦距fx、fy,主点cx、cy,以及倾斜因子。此时每张照片的外参也顺带解出来了,因为H已知、A已知,r1 = A^{-1} * h1,r2 = A^{-1} * h2,r3 = r1 x r2,t = A^{-1} * h3,再做一个SVD正交化修正即可。

2.5 畸变模型与非线性优化

上述线性求解是在理想无畸变假设下完成的。实际镜头,尤其是广角和廉价镜头,畸变非常明显。张正友采用的是经典的Brown畸变模型,通常用k1、k2(径向畸变)和p1、p2(切向畸变)四个参数来描述。

径向畸变是沿着镜头半径方向的变形,表现为画面边缘的桶形或枕形失真,k1控制主要畸变量,k2控制高阶畸变。切向畸变则是镜头与成像平面不完全平行导致的,实际中占比很小,但在高精度测量场景里必须考虑。

线性解出的内参和外参,加上初始化为0的畸变系数,一起作为初值,再用Levenberg-Marquardt算法做非线性最小二乘优化。优化的目标函数是所有角点的重投影误差平方和最小化:

min Σ || m_ij - m_hat(A, k1, k2, p1, p2, R_i, t_i, M_j) ||^2

这里的m_hat表示把世界坐标点M_j按照当前内外参和畸变系数投影到图像上的预测位置。整个优化过程迭代下去,直到误差收敛。这就是张正友标定法完整的两步走:线性求解初值,非线性优化精修。

2.6 与其他标定法对比:为什么张正友是“最佳平衡点”

传统标定法的代表是Tsai两步法。它要求标定板放在精确已知的物理位置,用高精度的机械平台控制移动,对标定环境要求极高,适合实验室条件,但工业现场很难满足。

自标定法(如基于Kruppa方程的方法)不需要标定板,直接通过相机运动过程中图像之间的对应关系求解内参,灵活性极高,但鲁棒性很差,容易陷入局部最优,精度不稳定,适合对精度要求不高的场景,比如三维重建的粗略初值。

张正友标定法正好卡在两者之间:需要标定板,但对标定板的运动没有任何限制,随便拍十几张就行;精度接近传统标定法,鲁棒性远优于自标定法;操作难度又低得多。所以无论是学术研究还是工业落地,它都成了事实上的标准方案。

3. 实操全流程:OpenCV从拍摄到标定

3.1 标定板准备与拍摄要点

标定板的精度直接决定标定结果的上限。建议先在专业打印店用高精度打印机输出棋盘格,贴在完全平整的硬板上。我自己用的是A3尺寸、9x6内角点、方格边长30mm的棋盘格,打印之后用游标卡尺实际测量每一个格子的边长,取平均值作为物理尺寸输入。

拍摄时注意几个要点:一是棋盘格要占图像面积的1/4到1/3,太小了角点检测不稳定;二是角度要丰富,左右倾斜、上下俯仰、旋转,各个姿态都要有;三是距离要有变化,近景和远景都要拍;四是光照要均匀,避免反光和阴影把角点吃掉了。

推荐的拍摄数量是15到20张。少于10张时,线性求解容易不稳定,非线性优化也容易过拟合;多于30张提升有限,反而增加检测失败的风险。我习惯每换一个场景重新标定,因为镜头对焦环变动、相机固件参数变动都会影响内参。

3.2 OpenCV代码实现与参数解释

直接上代码。以下是我在实际项目中使用的标定流程,基于Python和OpenCV。

import cv2 import numpy as np import glob # 棋盘格参数配置 pattern_size = (9, 6) # 内角点数量,不是格子数量 square_size = 30.0 # 方格边长,单位mm criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 1e-6) # 准备世界坐标系坐标 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 = [] # 图像像素坐标点 images = glob.glob('calib_images/*.jpg') for fname in images: img = cv2.imread(fname) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 自动检测棋盘格角点 ret, corners = cv2.findChessboardCorners(gray, pattern_size, None) if ret: objpoints.append(objp) # 亚像素精修 corners_sub = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) imgpoints.append(corners_sub) # 可视化标定结果 cv2.drawChessboardCorners(img, pattern_size, corners_sub, ret) cv2.imshow('corners', img) cv2.waitKey(100) cv2.destroyAllWindows() # 核心标定函数 ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera( objpoints, imgpoints, gray.shape[::-1], None, None ) print("内参矩阵:\n", mtx) print("畸变系数:", dist.ravel()) print("重投影误差:", ret) # 保存结果 np.savez('calib_result.npz', mtx=mtx, dist=dist, rvecs=rvecs, tvecs=tvecs)

这段代码里有几个细节值得强调。findChessboardCorners的pattern_size必须填内角点数量,也就是格子数减一。一个9x6的棋盘格,实际是横向9个内角点、纵向6个内角点,对应10x7的格子阵列。填错的话检测一定失败。

cornerSubPix的窗口大小一般设成(11,11)或者(15,15),太小了精修效果不明显,太大了容易跨过角点区域引入干扰。迭代次数30次、精度1e-6是经验值,实际使用完全够用。

calibrateCamera返回的ret值就是重投影误差的均方根值,单位是像素。这个数值是衡量标定质量最直接的指标,我一般要求它小于0.15像素才算合格,如果超过0.3像素,说明拍摄的照片或者角点检测有问题,需要排查。

3.3 标定结果验证:矫正图像与误差分析

标定完不能直接拿去用,必须先验证。最简单的验证方式是把标定的内参和畸变系数应用在测试图像上,观察矫正效果。

import cv2 import numpy as np # 加载标定结果 data = np.load('calib_result.npz') mtx = data['mtx'] dist = data['dist'] # 方式一:直接去畸变(适合内参变化不大的场景) img = cv2.imread('test.jpg') h, w = img.shape[:2] newcameramtx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist, (w, h), 1, (w, h)) dst = cv2.undistort(img, mtx, dist, None, newcameramtx) # 裁剪边界黑边 x, y, w, h = roi dst_crop = dst[y:y+h, x:x+w] cv2.imwrite('undistorted.jpg', dst_crop)

getOptimalNewCameraMatrix里的alpha参数很关键。alpha=0表示裁剪掉所有无效像素,输出图像尺寸可能变小,但画面完整保留;alpha=1表示保留所有原始像素,但边缘会出现黑边。实际项目中我一般设成0.5到1之间,再根据ROI裁剪。

更严格的验证方式是计算所有标定图像的重投影误差,把误差大于阈值的图像剔除后重新标定。OpenCV的calibrateCamera本身就返回整体误差,但如果想看每张图的误差,需要手动把每个角点投影回去。

我常用的验证代码是:

total_error = 0 for i in range(len(objpoints)): imgpoints_proj, _ = cv2.projectPoints(objpoints[i], rvecs[i], tvecs[i], mtx, dist) error = cv2.norm(imgpoints[i], imgpoints_proj, cv2.NORM_L2) / len(imgpoints_proj) total_error += error print(f"图像 {i}: 重投影误差 {error:.4f} px") print(f"平均误差: {total_error / len(objpoints):.4f} px")

4. 张正友标定法的常见问题与避坑指南

4.1 采集照片的标准是什么

很多新手在采集标定照片时非常随意,导致标定结果时好时坏。我总结了几条硬性标准。

图像清晰度是第一位的。棋盘格边缘模糊、有运动模糊的图直接删掉。检测角点用的是梯度信息,模糊的角点位置会有几个像素的偏差,直接污染标定结果。

角度覆盖度要够。理想情况下,棋盘格法线方向和相机光轴之间的夹角要覆盖0到45度。如果所有照片都几乎是正对着拍的,那么内参求解时Z方向的可观测量严重不足,焦距fx、fy的估计会很不稳定。

照片之间要有足够的位置差异。如果你只是把棋盘格平移了几毫米拍了一张,两张图提供的信息几乎是重复的,对约束方程没有新的贡献。理论上3张不同角度的照片就能解出内参,但实际中10到20张不同位姿的照片才能让非线性优化稳定收敛。

4.2 角点检测失败的排查路径

findChessboardCorners返回False是最常见的问题。我遇到最多的情况有以下几类。

一是pattern_size填错了。这个问题在初学者里出现概率极高,因为OpenCV的文档里写的是“number of inner corners per row and column”,很多人误以为填格子数量。9x6的格子阵列,内角点是8x5,填错直接检测失败。

二是图像分辨率太大导致检测性能不佳。有些工业相机输出4096x3072的图像,直接用原始尺寸跑findChessboardCorners会非常慢,而且检测率下降。我一般先缩小到1/4或者1/2尺寸做检测,拿到粗略角点后再映射回原始尺寸,用cornerSubPix精修。

三是光照问题。强反光或者阴影遮挡会让黑白格对比度骤降,角点响应特征消失。解决办法是拍摄时用均匀漫射光源,尽量让棋盘格平面光照一致。

四是棋盘格部分出了画面。OpenCV的检测算法要求所有内角点都在视野内,任何一点缺失都会导致检测失败。拍摄时留出至少一个格子的安全边距。

4.3 重投影误差偏大的原因分析

标定完发现重投影误差大于0.3像素,不要急着调算法,先检查数据本身。

第一嫌疑是棋盘格板不平。普通打印纸贴在玻璃上还好,贴在亚克力板上如果有弯曲,角点的物理世界坐标就偏离了理想平面假设。换成铝板或玻璃板,背后用海绵垫平,问题通常就消失了。

第二嫌疑是标定过程中相机或镜头有变动。自动对焦的相机如果在拍摄过程中对焦距离变了,焦距就会变,这种情况下所有照片不能用同一组内参去拟合,直接导致误差飙升。解决办法是锁死自动对焦,或者用工业定焦镜头。

第三嫌疑是角点亚像素精修窗口参数不合适。如果棋盘格在图像中占比很小,角点区域只有几个像素,精修窗口太大反而会引入周围像素的干扰。此时考虑靠近拍摄,或者使用更高分辨率的相机。

4.4 不同标定条件对标定精度的影响

我专门做过一组对比实验,用同一台相机、同一个棋盘格,测试不同拍摄数量下的标定误差。

照片数量平均重投影误差标定结果稳定性
5张0.28 像素差,更换照片集结果浮动大
10张0.17 像素一般,基本可用
15张0.12 像素稳定
20张0.11 像素稳定
30张0.10 像素非常稳定

从数据可以明显看出,5张照片的结果非常不可靠,误差大而且解不稳定。15张之后基本收敛,继续增加照片数量收益有限。

另外我还测试过标定板平整度的影响。用普通纸打印直接平铺在桌面上,和贴到3mm铝板上做对比,前者的重投影误差大约是后者的2到3倍。这个差距在视觉测量项目里是致命的。

5. 进阶技巧:如何把标定精度再往上推一截

5.1 多分辨率拍摄与自适应角点检测

工业应用里,相机分辨率差异很大。对于高分辨率相机,直接对整幅图像做角点检测效率太低。我习惯把图像缩小到VGA级别做粗检测,再投影到原始尺度精修。

具体做法是:先对原始图像做高斯金字塔降采样,在低分辨率图上调用findChessboardCorners,拿到粗略角点坐标后乘以缩放系数映射回原始尺度,再用cornerSubPix精修。这样既保留了高分辨率图像的精度,又大幅提高了检测速度和成功率。

5.2 联合标定:单目内参与双目外参同时解算

如果你做的是双目视觉,不要分别对左右相机做单目标定,再单独计算外参。正确做法是先单目标定得到各自内参,然后用stereoCalibrate联合优化左右相机的相对位姿R和t,再用stereoRectify计算极线矫正映射。

联合优化的好处是,左右相机的内参和相对外参在整个系统层面达到最优,而不是各自最优后再拼装。对于需要做深度估计、三维重建的场景,这一步能显著降低视差图的系统性误差。

OpenCV里相关的核心调用是cv2.stereoCalibrate,输入左右相机的角点对和世界坐标,初始内参用单目标定结果,输出优化后的内参和相对外参。

5.3 标定结果的时效性管理

很多人标定一次用一年,这是大忌。镜头是机械结构,温度变化、震动、对焦环轻微转动都会改变内参。特别是温度,我实测过一台工业相机从室温20度工作到45度,焦距内参变化可以达到0.3%以上。

对于高精度应用,建议建立定期标定机制。我自己的项目里,热机半小时后做一次标定,然后每两三个小时用刚标定的内参评估重投影误差,如果误差超出阈值就触发重新标定。

另外,如果项目对精度要求极高,比如机器人抓取、手术导航这类场景,建议考虑用静止的标定板做在线动态标定,或者引入高精度激光测距辅助验证。

5.4 从像素精度到实际测量精度的换算

最后分享一个判断标定结果是否够用的经验公式。假设你的相机在1米距离处,像素精度大约是0.1毫米/像素。如果标定重投影误差是0.5像素,那么你在这个距离上的视觉测量误差至少是0.05毫米。这个误差会随着距离增加而线性放大。

所以不要只盯着重投影误差这个数字。你要反过来想,你的应用需要的物理测量精度是多少,然后反推允许的像素误差上限。如果预算只有1毫米,1米距离处允许的像素误差就是10像素,那标定做到0.5像素已经很富余了;如果要求0.1毫米,标定就必须做到0.05像素级别,这时候从拍摄条件到标定板材质,每一个环节都不能马虎。

我在实际使用中最大的体会是,张正友标定法本身已经非常成熟,真正决定标定质量上限的往往不是算法,而是你手里的棋盘格干不干净、平不平整,拍摄时有没有把各个姿态覆盖到位。把数据采集关把好了,标定的后续步骤基本就是水到渠成的事。这套方法从1998年提出到现在二十多年,依然是视觉工程师们最趁手的工具,学好它,你的视觉项目就赢在了起跑线上。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询