ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

张正友标定法:从棋盘格到内参畸变系数的OpenCV落地指南

张正友标定法:从棋盘格到内参畸变系数的OpenCV落地指南 简介这是一份基于OpenCV-Python实现的张正友相机标定完整项目面向计算机视觉初学者及需做三维重建、视觉测量的开发者旨在解决相机内参、外参与畸变系数的标定问题。压缩包共15个文件包含12张棋盘格标定图片、1个Python标定脚本、1个相机参数XML文件及1个README文档整体大小约2.43MB结构精简可直接运行学习。目前已有305人学习下载。项目源码完整实现了从棋盘格角点检测、标定方程组构建到最小二乘求解内外参数的核心流程并附有参数输出与评估验证适合对照README边看边练快速掌握张正友标定法在OpenCV中的实际落地。通过真实标定图片与脚本配合用户既能巩固标定原理又能将结果直接用于后续三维重建、视觉测量等项目。代码风格简洁注释清晰便于二次修改与复用。1. 张正友标定法为什么到现在还是入门首选一块棋盘格换来内参与畸变系数上周有个做视觉测量的朋友发来一张照片说产品边缘检测总偏 4 到 5 个像素怀疑是镜头质量问题。我让他先做一遍相机标定再谈换镜头畸变系数一出来误差回落到 0.8 个像素以内。这种事在视觉项目里太常见了像素坐标和物理坐标之间那层关系没立住后边再做检测、测量、定位抓取都是空中楼阁。相机标定camera calibration就是把这层关系用数学模型标出来而张正友标定法是最适合入门和项目落地的路线——一块棋盘格、一台普通相机、一段 OpenCV-Python 脚本15 张照片就能把内参矩阵和畸变系数解出来。这篇笔记就从原理讲到源码实现再落到参数设置和常见坑适合做测量、定位抓取、AR 叠加和缺陷检测的从业者照着复现。2. 先看懂张正友法的三个数学支点坐标系、单应矩阵和畸变模型2.1 四个坐标系怎么串成一条链路标定这件事本质上是解一条坐标转换链路。像素坐标系里的点(u, v)是从世界坐标系里的点(Xw, Yw, Zw)经过相机坐标系、图像坐标系一路变换过来的。整条链路写成矩阵形式是s [u, v, 1]^T K [R | t] [Xw, Yw, Zw, 1]^T其中K是内参矩阵包含fx、fy、cx、cy四个要素分别代表 x 和 y 方向的等效焦距以及主点坐标。[R | t]是外参描述标定板相对相机的位置和姿态。张正友法的聪明之处在于它不要求你测量相机在世界坐标系里的绝对坐标只要求标定板是一个已知尺寸的平面然后通过棋盘格角点把K和每一张图的[R | t]一起解出来。对应到 OpenCV 的接口上K就是calibrateCamera返回的cameraMatrixdistCoeffs是畸变系数rvecs和tvecs是每一张照片对应的外参。初学者常把注意力全放在K上忽略了rvecs和tvecs——它们在后边的位姿估计、机械臂抓取里同样重要。2.2 平面棋盘格让问题降维单应矩阵 H如果标定板是立体的你需要两个或三个不同深度的平面才能约束住内参。张正友法把标定板放在世界坐标系的Z 0平面上公式直接消去一列变成一个从平面到像素的 3x3 单应矩阵s [u, v, 1]^T K [r1, r2, t] [Xw, Yw, 1]^T这里r1、r2是旋转矩阵的前两列。单应矩阵H有 9 个元素去掉尺度自由度还剩 8 个未知数所以理论上 4 个非共线角点就能解出一张照片的H。但我们要的不是H本身而是藏在它里边的K。利用旋转矩阵的正交性每一张照片能给内参提供两个约束方程5 个内参理论上 3 张照片就能解但实际中噪声和畸变会让 3 张的解很不稳定所以项目里通常采集 10 到 20 张。理解了这一层你就知道为什么采集时要求棋盘格有不同姿态姿态越多样H之间越不相关对K的约束越强。如果所有棋盘格都正对着相机拍r1、r2之间的关系退化内参解出来会很不稳定。2.3 畸变模型OpenCV 默认求哪几个参数透视成像之外镜头还会引入畸变。张正友法原论文主要处理径向畸变OpenCV 在实现上做了扩展。径向畸变会让直线变弯用k1、k2、k3描述切向畸变是镜头装配不平行导致的用p1、p2描述。OpenCV 默认输出的畸变系数是 5 个[k1, k2, p1, p2, k3]。如果你的镜头畸变比较厉害比如广角镜头可以给calibrateCamera加上CALIB_RATIONAL_MODEL标志让它额外求k4、k5、k6变成 8 个参数。要直观验证单应矩阵的概念可以用 OpenCV 的findHomography试一下。它和标定里的H不完全是一回事但能帮你建立“平面到平面的射影关系”这个直觉import cv2 import numpy as np img1 cv2.imread(calib_imgs/checkerboard_01.jpg, cv2.IMREAD_GRAYSCALE) img2 cv2.imread(calib_imgs/checkerboard_02.jpg, cv2.IMREAD_GRAYSCALE) pattern_size (9, 6) # 内角点数量列数减一行数减一 ret1, corners1 cv2.findChessboardCorners(img1, pattern_size) ret2, corners2 cv2.findChessboardCorners(img2, pattern_size) H, mask cv2.findHomography(corners1, corners2) print(两幅棋盘格图像之间的单应矩阵:\n, H)这段代码先用findChessboardCorners提取两幅图的棋盘格角点再用findHomography求它们之间的射影变换。H是一个 3x3 矩阵它的自由度解释了为什么张正友法能用平面棋盘格替代昂贵的三维标定块——一个平面到另一个平面之间恰好就是 8 个自由度。3. 标定前的准备OpenCV-Python 环境、棋盘格采集与角点检测的边界3.1 环境搭建opencv-python 与 numpy 的版本选择标定不需要自己编译 OpenCV直接装预编译的 wheel 包就够了。用opencv-python加numpy两条命令搞定python -m pip install opencv-python numpy python -c import cv2; print(cv2.__version__); print(cv2.getBuildInformation().split(\n)[0])第二行命令用来确认装好的版本和编译选项。日常标定对版本没有苛刻要求OpenCV 4.x 都可以。一个常见误区是把opencv-python和opencv-contrib-python同时装进同一个环境两个包会互相覆盖文件导致import cv2报各种奇怪错误。需要contrib里的扩展模块时就只装opencv-contrib-python不要两个都装。如果运行脚本时提示ModuleNotFoundError: No module named cv2先确认当前解释器是不是你装包用的那个。很多人装完包换个终端窗口或者换到别的虚拟环境跑解释器路径变了包自然找不到。用python -c import sys; print(sys.executable)查一下当前解释器路径再和pip show opencv-python的安装位置对一下。另外标定脚本建议固定随机性和线程数避免多线程环境下去畸变结果抖动但这属于后边排错的部分。3.2 采集规范10 张起步15 到 20 张更稳标定结果的稳定性一半以上取决于照片怎么拍而不是算法怎么调。我的采集标准是这样的位姿类型建议张数要点正对镜头中心位置2 到 3棋盘格占画面 1/3 到 1/2偏向左上、右上、左下、右下8 到 10让角点分布到画面边缘和四角远近各一组3 到 4近景让棋盘格尽量大远景让棋盘格变小绕 X 轴、绕 Y 轴倾斜3 到 4倾斜 15 到 30 度避免纯平面旋转拍照时锁住焦距关掉自动对焦。手机摄像头和部分 USB 摄像头默认开着自动对焦标定时每个位姿焦距都可能变了一点fx、fy解出来就会漂。分辨率固定成同一个值不要标定时用一个分辨率部署时用另一个。棋盘格建议激光打印贴在玻璃或亚克力板上用双面胶压实避免纸面弧度。方格边长要量准常见的 25mm、30mm 是标称值打印出来可能存在 0.1mm 级的偏差测量精度要求高时用游标卡尺量一下再填进代码。3.3 findChessboardCorners 检测参数与返回值的边界检测角点是标定流程的第一步先写一个最小检测脚本把每张图的角点可视化出来确认所有照片都检测通过再进入下一步import cv2 import glob pattern_size (9, 6) # 内角点数不是棋盘格总数 criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) for fname in sorted(glob.glob(calib_imgs/*.jpg)): img cv2.imread(fname) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, corners cv2.findChessboardCorners( gray, pattern_size, flagscv2.CALIB_CB_ADAPTIVE_THRESH | cv2.CALIB_CB_NORMALIZE_IMAGE) if not ret: print(检测失败跳过:, fname) continue cv2.drawChessboardCorners(img, pattern_size, corners, ret) cv2.imwrite(corners_vis_ fname.split(/)[-1], img)pattern_size是内角点数量不是棋盘格行列数。一块 9x6 的棋盘格内角点是 8x5这是新手最容易踩的头一个坑。CALIB_CB_ADAPTIVE_THRESH会对图像做自适应阈值化减轻光照不均的影响CALIB_CB_NORMALIZE_IMAGE在阈值化之前做直方图归一化这两项在标定场景里默认打开通常没有副作用。如果照片里有反光或局部过曝可以再试一下CALIB_CB_FILTER_QUADS它会过滤掉形状不符合棋盘格单元的四边形。4. 完整标定流程从亚像素角点到重投影误差的代码实现4.1 构造 objectPoints 与 imagePoints维度、顺序和类型标定需要两组点对世界坐标系里的物点objectPoints和像素坐标系里的像点imagePoints。物点用一个(N, 1, 3)的 float32 数组表示像点是(N, 1, 2)。N是单张图的内角点数。所有照片共用一个物点数组因为标定板是刚体角点相对位置不变import numpy as np import cv2 import glob pattern_size (9, 6) # 内角点数量 square_len 25.0 # 方格边长单位 mm 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] * square_len objpoints [] # 每张图片一个 objp 副本 imgpoints [] # 每张图片对应的亚像素角点这里np.mgrid生成了从(0, 0)到(8, 5)的网格坐标.T之后的排列顺序和 OpenCV 内部对角点的排序约定一致。乘以square_len把单位换成毫米这样后边tvec解出来也是毫米单位。注意objpoints里存的是同一个objp的引用也可以但为了防止后续不小心改动它更稳妥的是[objp.copy() for _ in range(len(images))]。4.2 批量提取角点并调用 calibrateCamera有了物点和calibrateCamera的输入结构就可以把全流程串起来了。下面的脚本会遍历所有图像检测角点、做亚像素细化、调用标定函数import cv2 import numpy as np import glob pattern_size (9, 6) square_len 25.0 criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) # 构造物点 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] * square_len objpoints, imgpoints [], [] for fname in sorted(glob.glob(calib_imgs/*.jpg)): img cv2.imread(fname) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, corners cv2.findChessboardCorners( gray, pattern_size, flagscv2.CALIB_CB_ADAPTIVE_THRESH | cv2.CALIB_CB_NORMALIZE_IMAGE) if not ret: print(跳过角点检测失败:, fname) continue corners2 cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) objpoints.append(objp.copy()) imgpoints.append(corners2) h, w gray.shape[:2] flags cv2.CALIB_RATIONAL_MODEL mtx, dist, rvecs, tvecs, per_view_errors cv2.calibrateCamera( objpoints, imgpoints, (w, h), None, None, flagsflags) print(内参矩阵:\n, mtx) print(畸变系数:, dist.ravel())cornerSubPix的窗口(11, 11)表示以初始角点为中心的搜索窗口窗口太小亚像素估计不稳定太大容易跨到相邻角点11 到 15 是常见选择。(-1, -1)表示零死区保持默认即可。criteria里的30是最大迭代次数0.001是亚像素精度阈值。calibrateCamera的返回值里per_view_errors是 OpenCV 4.x 才有的每张图重投影误差可以直接打印出来定位哪张照片质量差。flags先用CALIB_RATIONAL_MODEL它同时会启用CALIB_RATIONAL_MODEL对应的八参数畸变模型。如果你的镜头是普通定焦镜头畸变不大去掉这个标志回归五参数反而更稳。CALIB_ZERO_TANGENT_DIST会固定切向畸变p1 p2 0除非你能确定镜头装配完美否则不建议手动固定。CALIB_FIX_ASPECT_RATIO会固定fx / fy的比值只有当你清楚传感器像素是正方形时才用默认不勾选。4.3 重投影误差逐张计算而不是只看均值calibrateCamera内部已经算了重投影误差但项目交付时最好自己再算一遍并且逐张打印定位是哪几张照片拉低了整体质量。重投影误差的做法是把解出来的内外参代回projectPoints把物点投影回图像平面和实际检测到的角点算欧氏距离mean_err 0.0 for i in range(len(objpoints)): proj, _ cv2.projectPoints(objpoints[i], rvecs[i], tvecs[i], mtx, dist) err cv2.norm(imgpoints[i], proj, cv2.NORM_L2) / len(proj) mean_err err if err 0.3: print(注意误差偏大的照片索引:, i, 误差: %.3f px % err) print(平均重投影误差: %.4f px % (mean_err / len(objpoints)))判断标准我一般这样定平均误差小于 0.1 像素说明图像质量和姿态覆盖都很好0.1 到 0.3 像素可用适合测量和定位任务超过 0.5 像素就需要重新检查照片质量、标定板平整度和角点检测结果。重投影误差高不一定都是算法问题镜头本身解析力不足、照片有运动模糊、棋盘格没有贴平都会让误差下不去。换固定焦距的工业镜头往往比盲目调参有效得多。4.4 畸变矫正undistort 与 remap 怎么选标定完内参紧接着就是把畸变矫正落到项目里。如果只是单张处理用cv2.undistort最直接如果是视频流或者批量处理应该用initUndistortRectifyMap配合remap映射表只算一次每帧只做查表重映射速度更快# 单张场景直接矫正 undist cv2.undistort(img, mtx, dist) # 视频流场景只算一次映射表 new_cam, roi cv2.getOptimalNewCameraMatrix(mtx, dist, (w, h), alpha0.0, newImgSize(w, h)) mapx, mapy cv2.initUndistortRectifyMap(mtx, dist, None, new_cam, (w, h), cv2.CV_32FC1) dst cv2.remap(img, mapx, mapy, cv2.INTER_LINEAR)getOptimalNewCameraMatrix返回一个新的内参矩阵new_cam和有效区域roi。alpha是这里最关键的参数alpha 0.0会裁剪掉矫正后没有像素的空洞区域视野略微缩小但输出干净alpha 1.0保留原始视野边缘会出现拉伸和黑边。测量类项目通常用alpha 0.0因为尺寸关系在任何区域都更可靠。INTER_LINEAR是双线性插值在矫正任务里足够不要因为追求“清晰”盲目上INTER_CUBIC它的耗时更高效果提升在标定场景里体现不出来。5. 标定避坑指南5 个让结果看起来很好却不能用的问题5.1 findChessboardCorners 一直返回 False照片一张都没通过现象脚本跑完控制台全是“检测失败”一张角点都没提取出来。原因pattern_size填成了棋盘格行列数实际需要的是内角点数。9x6 的棋盘格内角点是 8x5。另外图像分辨率太低、棋盘格占画面太小、反光或光照不均也会导致检测不通过。解决先把pattern_size改成内角点数再确认棋盘格占画面至少四分之一提高采集分辨率到 1280x720 以上。如果还有个别照片漏检给findChessboardCorners加上CALIB_CB_FILTER_QUADS或者换光照角度避免反光。5.2 标定误差很低但实际矫正后测量仍然偏现象平均重投影误差只有 0.06 像素可把标定结果拿去测工件尺寸结果就是差一截。原因误差低只说明算法在“自圆其说”不说明真值是对的。最典型的是标定板贴在不平整的表面上。A4 纸贴在墙上纸面有弧度角点的所谓世界坐标本身就是弯的算法只能拟合出一个让误差变小的奇怪模型。解决把棋盘格贴到玻璃板、亚克力板或者铝板上用双面胶压实。如果精度要求高直接用陶瓷或玻璃基底的工业标定板。另外用游标卡尺复核方格尺寸把square_len填成实测值而不是印刷标称值这条会影响所有后续米制测量。5.3 画面中心区域正常边缘畸变越靠近角落越离谱现象矫正后的图像中间是直的越往边缘越弯或者边缘像素被拉伸得很奇怪。原因采集照片时棋盘格始终放在画面中央边缘区域没有任何角点参与约束畸变模型在边缘区域全靠外推。畸变系数在中心区域拟合得准不代表在边缘区域准。解决重新采集让棋盘格出现在画面的左上、右上、左下、右下四个区域再配合绕 X 轴和 Y 轴各倾斜 15 到 30 度的照片确保所有象限都有角点覆盖。至少有 3 张照片的角点要触达画面边缘。5.4 自动对焦导致今天标定、明天就去畸变失配现象同一台相机标定当天效果好隔两天再用边缘“方”形畸变明显或者尺寸测量结果整体偏移。原因自动对焦或镜头环松动导致焦距变化内参矩阵里的fx、fy已经变了。手机和 USB 摄像头在每次对上焦时fx都可能不同标定结果自然失效。解决使用手动对焦镜头或者把镜头的对焦环用胶带固定死标定完成后就不要再碰镜头。项目里如果只能用自动对焦相机至少要在每次部署时用 5 张照片重新标定一次验证fx变化幅度。5.5 只用同一批照片验证误差导致“自我欺骗”式的误差虚低现象重投影误差 0.05 像素交付后客户用新照片测误差完全对不上。原因标定时用的照片又拿来算重投影误差这是拟合误差不是泛化误差。模型把用来训练它的数据拟合得很好不代表在新数据上也准。很多“标定报告很漂亮但项目翻车”的血泪经历根源都是这一条。解决采集时单独留出 3 到 5 张照片完全不参与calibrateCamera。标定完成后用这些没有参与拟合的照片去计算重投影误差或者直接对它们做去畸变肉眼观察棋盘格边缘线是否变直。这样得到的误差才有实际参考价值。6. 把标定结果用到项目里验证技巧、结果保存与外参延伸拿到内参和畸变系数之后真正的项目工作才开始。我习惯按三条路径把它接进下游第一用一张标定时没用过的棋盘格照片做去畸变然后拉一条直线从最左边角点连到最右边角点看中间角点是否都落在直线上。如果中间偏差超过 1 到 2 个像素说明标定结果还有问题回到采集环节重新拍不要继续调参数。第二结果保存用npz还是 JSON取决于下游是谁在用。Python 侧直接np.savez最省事加载也快np.savez(calib_result.npz, mtxmtx, distdist, per_view_errorsnp.array(per_view_errors)) data np.load(calib_result.npz) mtx data[mtx] dist data[dist]如果下游是 C 或者 ROS 节点存成 YAML 或 JSON 更通用注意dist要ravel().tolist()之后再写进去因为它本身是(1, 5)或(1, 8)的形状直接序列化会让其他语言侧解析麻烦。第三想从“矫正图像”跨到“空间位姿”就用solvePnP。它把一张图中的棋盘格角点和物点对应起来就能输出标定板相对相机的外参也就是旋转向量和平移向量ret, rvec, tvec cv2.solvePnP(objp, corners2, mtx, dist) # tvec 的单位和 square_len 一致25.0 表示方格边长 25mm这套东西再往下走就是双目相机标定和鱼眼相机标定。双目要用stereoCalibrate求两个相机之间的相对外参再配合stereoRectify做双目校正鱼眼镜头畸变大普通calibrateCamera的针孔模型容易求解失败要换cv2.fisheye模块单独处理。我早期做抓取项目时标定误差报告每次都漂亮机械臂抓取却总是偏后来发现是标定板贴在不平的打印纸上纸面弧度和姿态多样性不足共同把结果带偏。现在我的流程固定为硬质平贴标定板、至少 15 张照片覆盖四角和倾斜、误差算完再用未参与标定的照片重新验证一次。这个习惯帮我把标定相关问题造成的返工压到了很低希望帮到你。本文还有配套的精品资源点击获取
返回列表