简介:基于OpenCV实现张正友相机标定的完整工程,面向计算机视觉初学者、高校学生及需要快速搭建标定环境的开发者,解决相机内参、外参与畸变参数求解,以及后续图像矫正问题。压缩包共92个文件、约10.26MB,除C++源码与Visual Studio工程文件外,还包含14张不同角度的jpg标定图、bmp棋盘图,以及编译生成的exe、pdb调试符号和中间文件,代码注释详细,一键即可运行。工程完整覆盖标定流程:读取棋盘格图像、检测角点、计算内参外参与旋转平移矩阵、输出标定结果,并通过反投影误差评价标定效果,最后利用获得的参数对原始棋盘图进行矫正。已有22719人学习下载,适合作为相机标定入门实践与算法学习的参考,也可将工程直接复用于其他视觉项目的标定环节,省去从零搭建的时间。 做相机标定,最绕不开的就是张正友棋盘格标定法。不管是做单目测距、双目视觉、三维重建,还是做畸变校正,这套流程都是计算机视觉工程师的“基本功”。标题里提到的“张正友相机标定Opencv实现”,说白了就是把这套经典算法用OpenCV的现成函数串起来,再配上棋盘图跑出内参和畸变系数。这篇博客我直接把我平时项目的完整程序和采图经验拿出来,附带踩坑记录,照着走一遍基本能出结果。
先说清楚这篇文章能给解决什么问题:如果你手里有一台相机(工业相机、单反、手机摄像头都行),想得到它的内参矩阵和畸变系数,或者做后续的测距、AR叠加,这篇文章直接给完整可运行的Python + OpenCV代码,以及生成棋盘图的脚本。你会明白标定的底层逻辑,知道每张照片为什么要那么拍,碰到角点检测不出来也知道从哪里查起。
1. 张正友标定法到底在做什么(原理速通)
很多新手刚接触张正友标定,第一反应是“代码我能跑通,但不知道它到底在算什么”。我先用最直白的话把原理捋一遍,后面看代码就一目了然。
1.1 相机成像模型里的几个坐标系
相机拍一张照片,本质上是把三维世界里的点投影到二维像素平面上。整个过程涉及三个坐标系:
- 世界坐标系:我们人为定义的三维参考系,棋盘格上的角点坐标就定义在这个系里。
- 相机坐标系:以相机光心为原点、光轴为Z轴的坐标系。从世界坐标到相机坐标,靠的是旋转矩阵R和平移向量t,也就是外参。
- 图像坐标系与像素坐标系:光心投影到成像平面,得到物理坐标,再经过像素缩放和主点平移,变成图像中的像素坐标。这个从三维相机坐标到二维像素坐标的映射,由内参矩阵K完成。
内参矩阵K长这样:
K = [fx, 0, cx; 0, fy, cy; 0, 0, 1]其中fx、fy分别是x、y方向上的等效焦距(单位是像素),cx、cy是主点坐标。张正友标定法要解决的核心问题,就是把这个K和畸变系数求出来。外参是每张照片各自不同的,属于标定过程的副产品。
1.2 为什么偏偏选棋盘格作为标定靶
我见过有人用圆点标定板,有人用ArUco码,但张正友方法最经典的标定靶就是黑白棋盘格。原因有几条:
第一,棋盘格的角点是天然的“特征点”。黑白格子交界处的内角点在图像里非常容易检测,而且角点的亚像素定位精度很高,比圆点中心在透视变形下的误差更稳定。
第二,棋盘格是一个平面,所有角点都落在同一个平面上。张正友方法的数学推导正是利用了“平面靶标”这个前提,通过单应性矩阵建立约束。平面靶标比三维靶标制作简单得多,打印一张纸就行。
第三,黑白相间的图案对光照变化、模糊、旋转都有很强的鲁棒性。OpenCV的findChessboardCorners函数对棋盘格的检测做了很多工程优化,只要别太夸张,基本都能找到角点。
1.3 张正友方法的数学思路:单应性矩阵
张正友方法的核心是“单应性矩阵H”。对于棋盘格平面上的三维点,可以把它看成Z=0平面下的点,这样三维到二维的投影关系就变成了一个3×3的单应性矩阵:
s * [u, v, 1]^T = K * [r1, r2, t] * [X, Y, 1]^T这里r1、r2是旋转矩阵的前两列。如果对每一张棋盘格图像,我们能精确提取出至少4个角点的二维像素坐标,并且知道它们对应的三维世界坐标(由棋盘格尺寸和角点数确定),就可以求解出该图像的单应性矩阵H。
张正友证明了,通过至少3张不同姿态的棋盘格图像,就能从多个单应性矩阵中恢复出内参矩阵K。实际工程中为了精度和稳定性,一般建议采集15~25张不同角度的图像。
畸变模型的加入是张正友方法的另一个关键点。它把径向畸变(参数k1、k2,高端镜头还会用到k3)和切向畸变(参数p1、p2)一起纳入非线性优化,用Levenberg-Marquardt算法对全部参数做全局优化。OpenCV里的calibrateCamera函数做的事情,就是把上述单应性求解作为初值,再用畸变模型做非线性优化,最终输出重投影误差最小的那组参数。
2. 标定前的准备工作(棋盘图与采集环境)
好多人拿到标定代码,随便打印一张棋盘格,拿起相机咔咔拍两张就开始跑,结果要么角点检测不到,要么标定出来的内参一看就不对。准备阶段花二十分钟,能让标定环节省两个小时。
2.1 棋盘图准备:直接生成与打印技巧
先用OpenCV生成一张高清棋盘图,这是最省事的方式。下面这段代码直接生成一个可用于打印的PNG棋盘图:
import cv2 import numpy as np # 参数 square_size = 150 # 每个方格的边长,单位像素 board_width = 10 # 方格列数 board_height = 7 # 方格行数 # 创建黑白棋盘 width_px = board_width * square_size height_px = board_height * square_size board = np.zeros((height_px, width_px), dtype=np.uint8) for i in range(board_height): for j in range(board_width): if (i + j) % 2 == 0: board[i * square_size:(i + 1) * square_size, j * square_size:(j + 1) * square_size] = 255 cv2.imwrite('chessboard_pattern.png', board)生成之后打印,注意几个细节:
- 内角点数量和格子数量别搞混。代码里的
board_width和board_height是“格子”数,而OpenCV标定函数需要的CHESSBOARD_WIDTH和CHESSBOARD_HEIGHT是“内角点”数。对10×7个格子的棋盘图,内角点数是(10-1)×(7-1),也就是9×6。这个数字填错了,findChessboardCorners永远检测不成功。 - 打印纸张尽量选哑光纸,避免反光。打印之后把棋盘图平整地贴在硬纸板或泡沫板上,不能有褶皱。我在项目中踩过一次坑:随手贴在快递纸箱上,纸箱表面不平,导致标定出来的fx和fy差了将近2%,后面做测距全偏了。
- 棋盘图在画面中占的面积别太小,建议占整张画面的四分之一到三分之一。
2.2 采样照片的要求与拍摄手法
标定图像的质量,直接决定标定结果的可靠性。这里有几个硬性要求:
- 相机固定,移动棋盘。相机装在三脚架上或者手持保持不动,将棋盘图在相机前方不同位置、不同角度摆放。千万别用手机围着棋盘拍一圈——那样外参会变化,但你没法精确建模相机运动,内参也容易算偏。
- 数量建议15~25张。太少画面覆盖不足,太多增加处理时间且容易引入重复姿态。我一般拍20张左右。
- 姿态要有多样性。棋盘要覆盖画面的九个方位区域:左上、居中、右下、边缘等;角度要有明显的倾斜,比如前后俯仰、左右旋转,让标定算法能解算出焦距和主点。如果所有照片棋盘都平行于成像面,内参中的fx、fy会退化,解出来极不稳定。
- 光照均匀,棋盘完整。画面中的棋盘格必须完整出现在视野内,任何一个角点都不能被遮挡。避免强光直射造成的反光区域,不然角点提取会在高光处出问题。
- 拍摄清晰,避免运动模糊。不要手持快门太长,尽量保证棋盘纹理锐利,因为亚像素角点检测依赖图像梯度信息,模糊会直接拉低精度。
2.3 环境搭建:OpenCV安装要点
Python环境安装OpenCV很简单:
pip install opencv-python opencv-contrib-python numpy这里需要注意一个容易踩的坑:opencv-python和opencv-contrib-python最好一起安装,版本要对齐,不然容易出现导入到某些模块(比如aruco、xfeatures2d)时找不到符号的问题。如果只需要基本标定功能,只装opencv-python也完全够用。
装完后跑一句验证:
import cv2 print(cv2.__version__)能打印出版本号,说明环境OK。
如果你是C++使用者,OpenCV的标定函数接口和Python几乎一一对应,核心思路完全一样,就是把vector<vector<Point3f>>和vector<vector<Point2f>>准备好,调用calibrateCamera。代码逻辑我后面会在Python版本里讲清楚,C++只是写法差异,没有思路差异。
3. 完整标定程序的实现(Python + OpenCV)
核心代码部分,我直接给一个完整可运行的程序。为了让流程清晰,我按“角点提取、执行标定、结果保存、畸变校正”四个模块来讲。
3.1 代码结构总览:从图片目录到标定结果
先设计好目录结构:把所有待标定图片放到calib_images/目录下,程序自动遍历该目录下所有jpg/jpg图片。
整体流程是:
- 设置棋盘格内角点行列数和格子物理尺寸
- 构建世界坐标系下的三维点坐标(所有棋盘格角点,Z=0)
- 遍历每张图片,检测角点,亚像素细化,收集对应关系
- 调用
calibrateCamera求解内参、畸变系数 - 输出并保存结果,用一张测试图验证畸变校正效果
下面这个代码是我实际项目中精简过的版本,直接可以跑:
import cv2 import numpy as np import glob import os # ========== 1. 参数设置 ========== CHESSBOARD_WIDTH = 9 # 内角点列数 CHESSBOARD_HEIGHT = 6 # 内角点行数 SQUARE_SIZE = 24.0 # 每个格子的物理尺寸,单位mm,按实际打印情况填写 criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) # ========== 2. 准备物体点 ========== objp = np.zeros((CHESSBOARD_HEIGHT * CHESSBOARD_WIDTH, 3), np.float32) objp[:, :2] = np.mgrid[0:CHESSBOARD_WIDTH, 0:CHESSBOARD_HEIGHT].T.reshape(-1, 2) objp *= SQUARE_SIZE # ========== 3. 遍历图片提取角点 ========== objpoints = [] # 世界坐标系下的三维点 imgpoints = [] # 对应的图像二维像素点 image_paths = sorted(glob.glob('calib_images/*.jpg')) if len(image_paths) == 0: raise FileNotFoundError('calib_images 目录下没有找到jpg图片') for fname in image_paths: img = cv2.imread(fname) if img is None: print(f'无法读取图片: {fname}') continue gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, corners = cv2.findChessboardCorners(gray, (CHESSBOARD_WIDTH, CHESSBOARD_HEIGHT), None) if ret: objpoints.append(objp) corners2 = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) imgpoints.append(corners2) # 可视化角点 vis_img = cv2.drawChessboardCorners(img.copy(), (CHESSBOARD_WIDTH, CHESSBOARD_HEIGHT), corners2, ret) cv2.imshow('Corners', vis_img) cv2.waitKey(100) else: print(f'角点检测失败: {fname}') cv2.destroyAllWindows() print(f'成功提取角点的图片数: {len(objpoints)} / {len(image_paths)}') if len(objpoints) < 5: raise RuntimeError('有效图片太少,至少需要5张以上才能标定') # ========== 4. 执行标定 ========== ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera( objpoints, imgpoints, gray.shape[::-1], None, None ) # ========== 5. 输出结果 ========== print(f'重投影误差 RMS: {ret:.4f} 像素') print('内参矩阵:') print(mtx) print('畸变系数 (k1, k2, p1, p2, k3):') print(dist.ravel()) # 保存结果 np.savez('camera_calib.npz', mtx=mtx, dist=dist, rvecs=rvecs, tvecs=tvecs, ret=ret) print('标定结果已保存到 camera_calib.npz')跑完之后,终端会打印重投影误差和内参结果。np.savez会把结果存成npy格式文件,下次直接用np.load读取,不需要重新标定。
3.2 核心函数的作用与参数解读
上面这段代码里,有几个细节值得单独展开说。
findChessboardCorners是角点检测的第一环。它返回两个值:ret(是否成功)和corners(检测到的角点坐标)。传入的参数是灰度图、内角点尺寸。有一个常见的疑惑:为什么传入的是内角点维度而不是格子维度?因为OpenCV关心的只是黑白交界的内部角点,边缘的角点不参与计算,填内角点数是接口明确规定的。
如果你发现有些光照不均、阴影较重的图片检测不出来,可以给findChessboardCorners加一些flags:
ret, corners = cv2.findChessboardCorners( gray, (CHESSBOARD_WIDTH, CHESSBOARD_HEIGHT), cv2.CALIB_CB_ADAPTIVE_THRESH + cv2.CALIB_CB_NORMALIZE_IMAGE + cv2.CALIB_CB_FILTER_QUADS )CALIB_CB_ADAPTIVE_THRESH表示使用自适应阈值,对光照不均的场景帮助很大;CALIB_CB_NORMALIZE_IMAGE会在检测前对图像做归一化;CALIB_CB_FILTER_QUADS会过滤掉形状不符合四边形约束的轮廓,减少误检。我建议默认就把这三个flag加上,代价很小但成功率会明显提升。
cornerSubPix的作用是把角点坐标从像素级精度提升到亚像素级。它的工作原理是在角点附近用梯度方向一致性做迭代优化。这一步对最终标定精度的影响非常大。如果跳过亚像素细化直接标定,重投影误差很容易从0.15像素飙升到0.4像素以上。(11, 11)是搜索窗口尺寸,(-1, -1)是死区尺寸,习惯上就按这个组合来。迭代终止条件是达到30次迭代或精度低于0.001。
calibrateCamera的返回值要记清楚。ret是重投影误差的RMS值,单位是像素;mtx是3×3内参矩阵;dist是畸变系数,长度为5时对应k1、k2、p1、p2、k3;rvecs是旋转向量列表,每个元素对应一张图的旋转;tvecs是平移向量列表。
gray.shape[::-1]这个写法也经常有人问。因为shape返回的是高和宽,而calibrateCamera要求传入的是宽和高,所以颠倒一下顺序。
3.3 标定结果的应用:畸变校正
标定完成之后,最直接的应用就是对图片做畸变校正。这里有两种做法,按场景选择。
第一种是最简单的undistort,适合单张图处理:
img = cv2.imread('calib_images/sample.jpg') h, w = img.shape[:2] new_cam_mtx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist, (w, h), 1, (w, h)) undistorted = cv2.undistort(img, mtx, dist, None, new_cam_mtx)第二种是用initUndistortRectifyMap生成映射表,再用remap对每帧图像做查表映射。这种方式适合视频流或批量处理场景,因为映射表只计算一次,后续每帧只是查表,速度比直接undistort快很多:
map_x, map_y = cv2.initUndistortRectifyMap(mtx, dist, None, new_cam_mtx, (w, h), cv2.CV_32FC1) undistorted = cv2.remap(img, map_x, map_y, cv2.INTER_LINEAR)这里getOptimalNewCameraMatrix的第四个参数alpha很有讲究。alpha取0时,校正后的图像会剔除边缘黑边和变形区域,输出图像会略微缩小,视角场变小;alpha取1时,会保留所有原始像素,边缘会出现黑边;取中间值则是两者折中。如果后续要做特征匹配或拼接,建议alpha取0,让输出画面更干净;如果只是观察校正效果、做畸变展示,alpha取1更直观。
4. 标定结果分析与实战排查
代码跑通只是第一步,标定结果合不合理、怎么排查失败,才是真正让新手头疼的地方。
4.1 重投影误差到底怎么读
calibrateCamera返回的ret就是重投影误差RMS。它表示“每个角点在世界坐标下投影到图像平面的像素坐标”和“实际检测到的像素坐标”之间的平均偏差。
- RMS小于0.3像素:精度很好,说明采图质量和算法执行都比较理想。
- RMS在0.3~0.5像素:可以接受,常见于手持拍摄或低分辨率相机。
- RMS大于1像素:不合格,标定结果不可信,需要检查采图质量、棋盘打印平整度、角点数设置。
我碰到过很多次“重投影误差重大”的情况,最后发现不是算法问题,而是有几张图片本身不清晰,或者棋盘在画面边缘有轻微遮挡导致角点提取出现偏移。处理技巧:先只用其中误差明显偏大的图片,打印出来看角点检测是否正确;如果只是某张图质量差,直接从数据集里移除该图重新标定即可。
4.2 常见的“角点检测失败”排查链路
下面这个排查套路我用了很多年,遇到角点检测失败基本都能定位。
第一,检查内角点数设置。这是最高频的错误。内角点数等于格子数减一。如果打印的棋盘是10×7个格子,那内角点就是9×6。填反了或者填多了,findChessboardCorners直接返回False。
第二,检查图片清晰度。拍摄时手抖或快门速度太慢导致模糊,角点检测在亚像素阶段会出现不收敛。把图片放大看看网格边缘是否锐利,能明显看到模糊直接重拍。
第三,检查棋盘范围。棋盘不能超出画面,也不能被遮挡。哪怕只缺一个角点,整个图案无法构成完整的棋盘拓扑,检测就会失败。
第四,检查光照。强反光、强阴影会让棋盘的黑白对比度失衡,导致findChessboardCorners在阈值分割阶段出错。补光均匀、避免直射反光,是图像采集的基本要求。
第五,检查分辨率适配。如果你的图片超大(比如工业相机5000万像素),棋盘在画面中非常小时,角点检测也会失败。这种情况下要么靠近棋盘让格子变大,要么先把图片降采样后再检测。
4.3 提高标定精度的几个细节
标定精度除了受图像数量影响,还有几个容易被忽略的细节。
- 棋盘格尺寸要精确。
SQUARE_SIZE是后续求解外参和距离时的物理尺度基准。打印后用尺子量一下实际边长,别只按打印机设置的尺寸填。差个1毫米,内参会体现在尺度上,做测距时就是比例误差。 - 照片覆盖视野边缘。假如标定图像中棋盘总是只出现在画面中央,那么边缘区域的畸变参数会被“猜”出来而没有被真正约束。最好让棋盘出现在画面的四个角上,这样畸变参数才有效。
- 避免纯平面旋转姿态。所有照片的棋盘都平行于成像面时,焦距和主点的解算病态。至少保证有几张棋盘有明显的俯仰角或偏转角,让标定算法能从几何变化中解出相机参数。
- 同一相机同一分辨率。相机内参并不是绝对的“固定”,它依赖于传感器的像素区域使用情况。你用了1920×1080标定,那就必须在1920×1080下使用;切到4K或720P,需要重新标定。这一点在工业项目中经常被忽略。
5. 从标定延伸出去:后续能做的事情
拿到内参和畸变系数之后,能做的事情就很多了。最常见的两个方向是单目测距和双目视觉。
单目测距比较简单粗暴:先通过标定得到的内参将图像坐标反投影到归一化相机平面,再结合已知高度的假设(比如地面平面)或已知尺寸的物体,利用透视关系估算距离。这个方向对畸变校正精度要求很高,所以标定阶段不能糊弄。
双目视觉则是先对左右相机分别做单目标定,再做双目标定求取两个相机之间的相对位姿R和T,最后做立体校正。OpenCV里的stereoCalibrate就是在单目标定结果的基础上进一步求解的。如果单目标定的内参本身就不准,双目标定的外参也会被带偏,后面做深度估计就会出现严重的系统误差。
另外,我个人经验里还有一个小技巧:标定文件的命名和保存最好带上相机型号和分辨率,比如camera_left_1920x1080.npz。因为项目迭代一段时间后,很容易忘记当前结果到底对应哪台相机、哪个分辨率,到时候重新对一遍数据会很崩溃。
如果你想做更直观的验证,可以在标定后拍一张棋盘放在画面边缘的图,做畸变校正后叠加检测角点,观察角点是否仍然精确对齐。如果校正后棋盘边缘位置偏了不少,先回去检查标定数据,不要急着改后续算法。
本文还有配套的精品资源,点击获取