简介:这是一份围绕张正友标定法整理的MATLAB实现资料包,面向计算机视觉初学者、课程设计与科研人员,解决摄像机标定中相机内参、外参与畸变系数的估计问题。压缩包共27个文件、仅85KB,以10个.m脚本为主体,涵盖单应矩阵计算、坐标归一化、旋转测试等关键环节;另有9个txt数据与说明文件、7个.asv自动保存备份,以及1篇CAJ格式原始论文《基于单平面模板的摄像机定标研究》。目前已有502人学习下载,配套脚本与数据可让读者从棋盘格角点检测开始,逐步推演内参矩阵、外参矩阵与畸变校正的完整求解流程,对照论文公式即可快速复现实验并扩展至自己的图像数据上。体积小巧且目录结构清晰,适合直接打开调试,是理解张正友标定法底层原理的实用入门资料。
1. 张正友标定法到底是什么:一张棋盘格照片就能让相机"认识自己"
你有没有想过,手机拍照时,明明镜头是圆的,拍出来的直线却不会弯曲?自动驾驶汽车判断障碍物距离,靠的又是哪双"眼睛"?这一切背后,都藏着一个看似朴素却极其关键的步骤——相机标定。通俗地说,标定就是让相机"认识自己":它得知道自己镜头的焦距是多少、画面中心在哪、成像有没有变形。在众多标定方法里,张正友标定法因为"一张棋盘格就够了"的极致简单,成了计算机视觉领域应用最广的入门必修课。它由学者张正友在1998年提出,也叫"张氏标定法",既不需要昂贵的精密设备,又能达到不错的精度,像极了做菜时"不用高汤,用清水也能炖出一锅好汤"的智慧。
2. 相机为什么会"看歪":从针孔模型到畸变的麻烦
2.1 从针孔模型说起:相机是怎么"看见"世界的
想象一个暗盒,前面戳一个针尖大小的小孔,外面的光线穿过小孔,在盒子内部倒映出外面的景象——这就是针孔相机模型,也是所有相机标定理论的起点。在这个理想模型中,世界上的一个点、针孔中心、成像平面上的一个点,三者永远在一条直线上。这条"共线关系"就是标定要守住的基本盘。
但现实中的相机不是针孔。手机镜头由好几片透镜组成,光线穿过透镜会发生折射,导致世界的点、光心、成像点不再严格共线。于是我们需要给这个理想模型加一些"补丁"参数:内参(焦距、主点坐标、像素尺寸)描述相机自身的几何结构,外参(旋转矩阵和平移向量)描述相机在世界坐标系中的位置和朝向。张正友标定法要做的,就是通过拍摄已知尺寸的棋盘格,把这两类参数反解出来。
2.2 畸变的真面目:桶形、枕形和切向畸变
光线经过透镜时,边缘区域和中心区域的折射程度不同,画面就会"鼓起来"或"凹下去",这叫径向畸变——桶形畸变看起来像画面从中心向外膨胀,枕形畸变则像四角被向外拉扯。除此之外,镜头安装做不到绝对平行于成像平面,还会产生切向畸变,画面像被人从侧面轻轻拧了一把。
畸变参数一般用 k1、k2(径向)和 p1、p2(切向)来建模,有些标定算法还会引入 k3 来应对强畸变的广角镜头。张正友标定法正是把畸变系数也放进优化目标里,让相机在标定完成后,能把畸变校正回来。
2.3 为什么不能拿厂商参数直接用
你可能会想:相机出厂时不是给过参数吗?问题在于,镜头是批量生产的,每一颗镜头的焦距、畸变都存在细微差异;而且相机跌落、温度变化都会让参数漂移。更关键的是,我们的应用场景——比如机器人导航、三维重建、AR叠加——对空间关系的精度要求是毫米级的,厂商给的那组"平均参数"根本不够用。所以,每次安装好相机或者换了镜头,都得重新标定一次。
3. 张正友标定法的核心套路:拍照、找角点、算矩阵
3.1 第一步:打印一张棋盘格
张正友标定法最友好的地方,就是它只需要一张打印出来的棋盘格(黑白相间的方格图案),贴在硬纸板上,就算制作了标定板。为什么用棋盘格?因为黑白方格的交界处——角点——有极其明确的像素坐标,计算机可以自动、稳定地检测到它们。
棋盘格的方格边长必须精确测量(比如 30 毫米),因为它是整个标定过程中的"世界尺度";换句话说,所有计算出来的相机参数,都以这个边长作为真实长度的基准。测量不准,后面全白搭,这一步也成了很多同学第一次标定时最容易忽略的坑。
3.2 第二步:从多个角度拍摄 10~20 张照片
拿着棋盘格,让它以不同角度、不同远近、不同位置出现在画面里,拍摄 10 到 20 张照片。要求是:棋盘格必须完整出现在画面中,角度变化要丰富——左右倾斜、上下倾斜、前后移动都得有。为什么要这么麻烦?因为单个角度的照片只能给出一个二维平面到图像的映射关系,多个角度才能把相机内参的"真实形状"从不同侧面逼问出来。
拍摄的关键细节包括:光照均匀,避免反光;不能使用自动对焦,要锁定焦距(否则每张照片的焦距都在变,标定结果会互相矛盾);棋盘格尽量占画面的 1/3 以上,太小了角点检测容易出错。
3.3 第三步:让程序自动检测角点
拍摄完成后,进入算法部分。以 OpenCV 为例,最核心的调用是cv2.findChessboardCorners(),它会返回棋盘格所有内角点的像素坐标。这个函数的工作逻辑并不神秘:先对图像做二值化和形态学处理,找到候选的黑色方块,再用四边形匹配和亚像素细化把角点坐标精确定位到亚像素级别。
import cv2 import numpy as np # 棋盘格规格:内角点数量(比如 9x6 的格子,内角点是 8x5) pattern_size = (8, 5) # 每个格子的实际边长(单位:毫米) square_size = 30.0 # 准备世界坐标系中的棋盘格角点坐标(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) objp *= square_size # 遍历图片,检测角点并保存坐标 obj_points = [] # 世界坐标系中的点 img_points = [] # 图像坐标系中的点 for fname in image_files: img = cv2.imread(fname) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) found, corners = cv2.findChessboardCorners(gray, pattern_size, None) if found: # 亚像素级别精确定位 criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) corners2 = cv2.cornerSubPix(gray, corners, (5, 5), (-1, -1), criteria) obj_points.append(objp) img_points.append(corners2)这里有两个关键参数:pattern_size是棋盘格内角点的行列数,注意不是格子数;square_size是世界坐标系的"比例尺",不同单位(毫米、厘米、米)只影响平移向量的尺度,不影响内参。cornerSubPix做的是亚像素细化,让角点精度从像素级提升到亚像素级,这对最终的标定精度影响显著。
3.4 第四步:解出内参、外参和畸变系数
检测到足够的角点对后,核心算法分三步走:先用闭合解(直接线性变换法)粗略估计内外参数,再用最大似然估计做非线性优化,最后用最小二乘方法计算畸变系数。
张正友标定法的一个精妙之处在于,它利用"棋盘格平面在世界坐标系中 z 恒为 0"这一特点,把相机成像方程从一个复杂的 3D 到 2D 投影,简化成了一个平面到平面的单应性变换(Homography)。每个角度下的棋盘格都能算出一个单应性矩阵,积累足够多的单应性矩阵后,内参就能通过解线性方程组解出来,精细打磨则交给非线性优化。
# OpenCV 中一步完成标定 ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera( obj_points, img_points, gray.shape[::-1], None, None ) # mtx 是内参矩阵,dist 是畸变系数 print("内参矩阵:\n", mtx) print("畸变系数:", dist.ravel())calibrateCamera的入参是前面准备好的世界坐标点、图像坐标点和图像尺寸,返回的mtx是 3x3 的内参矩阵(含焦距和主点),dist是畸变系数向量,rvecs和tvecs则保存了每一张照片对应的外参。这一行代码背后,就是张正友标定法从单应性矩阵到参数分解的全部数学推导。
4. 深入标定结果的内部:如何读懂内参、外参和误差报告
4.1 内参矩阵里的四个数字意味着什么
内参矩阵看起来是一堆数字,其实只包含四个有物理意义的变量:fx、fy 是焦距在 x、y 方向的像素度量,cx、cy 是主点(光轴穿过成像平面的交点)的像素坐标。理想情况下,fx 和 fy 应当相等,主点应当位于画面中心,但由于制造误差,它们总会有些偏差。
内参矩阵 K = [fx 0 cx] [ 0 fy cy] [ 0 0 1]fx 可以直观理解为"相机焦距(毫米)× 像素密度(像素/毫米)",所以同一颗镜头,传感器像素越小,fx 数值越大。读懂这四个数,你就能判断相机是否存在明显的装配误差,以及三维重建时的尺度是否可靠。
4.2 重投影误差:标定结果好不好的"体检报告"
标定完成后,如何判断结果靠不靠谱?靠重投影误差(Reprojection Error)。原理很简单:用标定出来的参数,把世界坐标系中的棋盘格角点重新投影回图像上,计算投影点和实际检测到的角点之间的像素距离,这个距离的平均值就是重投影误差。
# 计算重投影误差 total_error = 0 for i in range(len(obj_points)): imgpoints2, _ = cv2.projectPoints(obj_points[i], rvecs[i], tvecs[i], mtx, dist) error = cv2.norm(img_points[i], imgpoints2, cv2.NORM_L2) / len(imgpoints2) total_error += error print("平均重投影误差:", total_error / len(obj_points))通常,重投影误差小于 0.1 像素就算标定质量优秀,0.1~0.3 像素可以用,超过 0.5 像素则说明有照片出了问题——可能是棋盘格反光、拍摄时抖动,或者某张照片的棋盘格角度太单一。不少新手拿到标定结果不看这个误差就直接用,结果三维重建时偏差好几厘米,回头查才发现是标定没做好。
4.3 图像去畸变:标定之后立刻能做的一件事
标定得到的畸变系数,最直接的应用就是去畸变。OpenCV 提供了两种方式:cv2.undistort()一步到位,但需要对整幅图像做重映射,计算量大;cv2.initUndistortRectifyMap()则是先算好映射表,之后每帧只需查表重映射,性能更高,适合实时视频流处理。
# 生成映射表,适合视频流实时去畸变 newcameramtx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist, (w, h), 1, (w, h)) mapx, mapy = cv2.initUndistortRectifyMap(mtx, dist, None, newcameramtx, (w, h), cv2.CV_32FC1) dst = cv2.remap(img, mapx, mapy, cv2.INTER_LINEAR)getOptimalNewCameraMatrix里那个1是自由缩放比例因子,设为 1 表示保持全部像素,设为 0 则会裁掉边缘畸变严重的区域,但分辨率损失会明显。实际项目中,如果对画面视野要求高,通常设 1 然后手动裁剪;如果边缘畸变本身干扰视觉,可以用 0。
4.4 标定结果为什么会有波动:照片数量和角度分布的影响
标定结果不是一次拍完就固定的,它对输入数据很敏感。照片数量少(少于 10 张)时,方程约束不足,解出来的内参往往在多次尝试之间跳来跳去;角度分布太集中(比如全是正对棋盘格拍摄),单应性矩阵之间的差异太小,内参的可辨识度就会下降。一个可行的经验法则是:拍摄 15 张以上照片,让棋盘格在画面中的倾角覆盖 -30° 到 +30° 的范围,并且远近位置拉开差距。标定完成后再拍一组不同角度的照片作为验证集,算一次重投影误差,如果验证误差远高于训练误差,说明标定过拟合了——侧面遮盖了某个角度范围的样本太稀缺。
5. 张正友标定法与其它标定方法的区别:三大流派怎么选
5.1 传统标定法 vs 张正友法:高精度与便捷性的取舍
传统标定法以使用精密加工的三维标定块为代表,把一块带有精确刻度图案的立方体放在相机前拍摄一次,就能完成标定。它的优点是只需拍摄一张照片、精度极高,缺点同样明显:标定块造价昂贵,动辄数千元,而且对摆放位置要求苛刻,一旦标定块的加工精度不够或者位置偏移,误差立刻放大。
张正友法把三维标定块换成了一张二维棋盘格,用多次拍摄替代单次拍摄。代价是精度稍逊于顶级的三维标定块标定,但成本低到可以忽略,操作难度也大幅下降。对大多数应用场景来说,张正友法的精度(重投影误差可做到 0.05~0.15 像素)已经足够满足视觉测量、定位和重建的需求。
5.2 自标定 vs 张正友法:没有标定板,但也要付出代价
自标定(Self-Calibration)是另一条路线:完全不需要标定物,只凭相机运动过程中多帧图像之间的对应关系来求解参数。它听起来很酷,但也有明显的短板——算法病态性强,对图像特征的数量和质量要求很高,在场景纹理不够丰富时,不仅精度不稳定,甚至可能完全无法收敛。
张正友法本质上是一个"介于传统与自标定之间的方法":它用棋盘格提供了可靠的约束,又不像三维标定块那样要求精密设备。这也是它的历史地位——它让标定从高端实验室的专用操作,变成了普通开发者也能随手完成的常规步骤。
5.3 OpenCV 标定与 MATLAB 标定工具箱的区别
实际动手时,OpenCV 和 MATLAB 是最常用的两个工具。OpenCV 的标定完全基于代码,灵活性高,适合嵌入到自动化流程里;MATLAB 的 Calibration Toolbox 提供图形界面,每一步都有可视化反馈(角点检测结果、误差分布图),更适合教学和一次性标定。
两者的算法内核都是张正友标定法,差别主要体现在参数输出的组织方式和错误诊断的便捷程度上。MATLAB 会输出每张照片的外参三维示意图,帮你看清棋盘格在空间中的姿态,这个功能在排查拍摄角度问题时特别实用;OpenCV 则需要自己写代码画出来。
6. 张正友标定法误区与易混概念:三个新手最容易栽的跟头
6.1 误区一:把世界坐标系的单位当成"米",结果重建出来全是歪的
这是最典型的翻车现场。很多同学把棋盘格边长填成 0.03(米),但测量时是用直尺量的毫米值 30,两者差了 1000 倍。外参的平移向量会跟着缩放,二维图像坐标不受影响,看起来标定误差很小,但输出给三维重建模块时,单位错乱导致重建的物体尺寸全部错误。
为什么混:因为棋盘格角点的世界坐标既可以写成毫米,也可以写成米,算法本身不关心单位,它只处理数值。怎么区分:牢记"你填什么单位,标定结果就以什么单位解释",并在注释里显式写明单位——内参不受影响,但外参平移量直接依赖这个选择。
6.2 误区二:自动对焦不关,每张照片的焦距都在变
拍摄时如果开了自动对焦,相机每次对焦都会微调镜头位置,等效焦距跟着变化。张正友标定法假设所有照片共享同一组内参,这个假设一旦被破坏,标定算法会把焦距的变化混入畸变系数甚至外参里,最终结果不稳定。
为什么混:手机相机默认自动对焦,很多人完全没意识到拍摄过程中焦距已经变了。怎么区分:拍摄前在相机 App 里长按锁定对焦,或者切换到专业模式手动设定焦距;工业相机则设置 fixed focus。判断方法很简单:标定多次,如果内参矩阵忽大忽小,八成就是对焦没锁住。
6.3 误区三:内参矩阵中的 fx 和 fy 直接当成物理焦距,其实它是"像素焦距"
fx 的单位是像素,不是毫米。如果要换算成物理焦距,需要 fx 除以传感器每毫米的像素数。这看起来是一个知识性细节,但确实有很多做三维重建的初学者把 fx 当作物理焦距用来计算视角大小,结果算出来的视场角偏得离谱。
为什么混:名称里带"f"容易被当成焦距真值。怎么区分:记住公式 f_mm = fx / (pixel_size_mm),或者直接用 fx 和图像宽度计算水平视场角:FOV = 2 * arctan(width / (2 * fx)),这样既不用换单位,也不容易混淆。
7. 什么时候该用张正友标定法:边界、坑位和一句建议
张正友标定法的适用边界其实很清楚。适合它的情况:单目相机标定、相机内参未知且不允许使用昂贵设备、需要把多个相机标定到统一坐标、或者只是想快速给相机做一个能用的去畸变配置。不适合它的情况:对精度有毫米级甚至亚毫米级要求且预算充足(这种情况应该上三维标定块),或者完全无法放置标定板的场景(这时只能用自标定,但要接受精度波动),再或者鱼眼镜头这类超广角强畸变镜头,标准针孔模型加上 k1/k2/k3 可能不够,需要专门的全向相机标定模型。
具体到三维重建项目里,我的建议是:把张正友标定法当成默认起点,然后用重投影误差作为验收标准。误差达标就放心用;误差超标,优先排查拍摄质量,而不是急着换算法。这套方法最让我服气的地方在于它的"性价比"——纸板和打印机换来的精度,放在二十年前是需要几万元设备才能做到的事。
作为一个做过几年视觉开发的普通工程师,我每次拿到新的摄像头,都会强制自己走一遍完整的标定流程:锁对焦、拍 15 张、检查重投影误差、保存映射表。有一次偷懒拿了上一个相机的标定结果直接换到新摄像头,结果测距偏差直接到了 8 厘米,从那以后我再也不敢跳步骤了。张正友标定法并不复杂,但它的价值恰恰藏在那些"看起来不重要"的细节里:单位、角点、对焦、误差报告,每一个都算数。希望这篇梳理能帮你把标定这块硬骨头啃下来,少走几段弯路,一次就把标定做明白。
本文还有配套的精品资源,点击获取