ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

张正友相机标定法实战指南:OpenCV棋盘格标定与畸变校正

张正友相机标定法实战指南:OpenCV棋盘格标定与畸变校正 简介基于OpenCV的张正有标定法完整工程包面向计算机视觉初学者与相机标定工程师帮助理解并实现相机内外参求解。资源以C代码和棋盘图为核心内含14张不同角度标定图及配套棋盘图可一键运行得到内参、外参、旋转和平移矩阵并提供标定效果评价以及原始棋盘图矫正演示代码附有详细注释便于逐行学习。压缩包共61个文件其中bmp/jpg图像素材约28张另含cpp源码、Visual Studio工程文件、可执行exe及相关编译中间文件包体17.07MB结构清晰适合直接打开运行。已有2057人浏览学习适合希望从代码层面掌握张正有标定流程并快速上手实践的用户。1. 从标定板拍一张照片开始它到底解决了什么如果你跟我一样在项目中第一次拿到一个USB摄像头想用它做尺寸测量或者机械臂抓取定位多半很快就撞上一堵墙——图像里的一根直线在真实世界里并不是直的。边缘往外鼓是桶形畸变往内凹是枕形畸变就算镜头质量不错光轴中心附近和画面边缘的像素对应关系也完全不是一个均匀缩放。这就是相机内参和外参没有被标定的典型状态。张正友标定法准确说叫Zhangs Method是1998年张正友在微软研究院时期提出的一种“介于传统标定和自标定之间”的灵活方法。它只需要一块平面棋盘格拍摄多张不同姿态的照片就能同时估计出相机的内参矩阵、畸变系数以及每一张照片对应的外参。这个方案到今天仍然是OpenCV相机标定模块的底层核心也是机器人、自动驾驶、AR/VR、工业检测里最通用的相机标定方案。这篇文章我把整个流程拆开讲清楚原理怎么理解、棋盘图怎么准备、代码怎么写、标定结果怎么看以及我实际跑数据时踩过哪些坑。目标是让你看完之后不靠复制粘贴也能自己写出一套可用的标定流程。代码以Python OpenCV为主文末会补充C版本的关键差异。2. 相机从三维到二维的路线图内参、外参和畸变2.1 核心公式从世界坐标到像素坐标张正友法的本质是先建立一个从三维世界坐标到二维像素坐标的投影模型然后用一组已知尺寸的角点去反解模型里的未知数。整个过程用一条链路表达世界坐标 - 相机坐标 - 归一化图像坐标 - 像素坐标第一步世界坐标转到相机坐标做的是刚体变换包括旋转和平移旋转矩阵R3x3和平移向量t3x1就是外参描述相机在世界坐标系中的位姿。第二步相机坐标转到归一化图像坐标就是用相机内参中的焦距fx、fy和主点cx、cy做透视投影。这里有个很重要的中间量——归一化平面。它是相机坐标(xc, yc, zc)除以zc得到(xn, yn)相当于把三维点投影到z1这个平面上。畸变模型作用在这个归一化坐标上代入畸变公式后得到畸变后的坐标(xd, yd)最后再通过内参映射到像素坐标u fx * xd cx v fy * yd cy把整个过程写成齐次坐标形式就是著名的公式s * m A [R|t] * M其中m是图像像素坐标M是世界坐标A是内参矩阵s是尺度因子。张正友法的巧妙之处在于它让标定板位于世界坐标系的z0平面上这样单应性矩阵H A [r1 r2 t]可以从单张图直接估计出来再通过多张图之间的约束关系分解出内参。2.2 畸变模型桶形、枕形和切向畸变说完几何投影再谈畸变。真实镜头不是理想的针孔模型光线经过镜片会发生折射偏离。OpenCV默认使用Brown畸变模型主要包含两部分径向畸变它的特点是像素点距离主点越远偏移越明显表现为直线变弯。OpenCV里用k1、k2、k3三个参数描述归一化坐标修正公式是xd xn * (1 k1r^2 k2r^4 k3*r^6)切向畸变由镜头和传感器平面不平行导致用p1、p2两个参数描述xd xn 2p1xnyn p2(r^2 2xn^2) yd yn p1(r^2 2yn^2) 2p2xnyn对于工业镜头和普通USB摄像头一般用4个畸变系数(k1, k2, p1, p2)就够了k3只在广角或鱼眼镜头下才需要启用。OpenCV的calibrateCamera函数通过rvecs和tvecs向量返回结果你传入的flags参数决定求解多少个畸变系数。2.3 为什么偏偏用棋盘格张正友选棋盘格做标定板不是随手拍的。棋盘格有四个天然优点第一角点特征极其稳定黑白交界的角点通过亚像素检测可以拿到很高的精度比圆点阵列在透视变形下的质心偏移问题小得多第二每个角点在图像上的位置可以用findChessboardCorners自动提取无需人工点选第三格子数量可以精确控制角点坐标已知宽度是精确的物理尺寸而不是像素尺寸第四棋盘是平面结构可以做成打印纸、亚克力板、铝基板满足不同精度的需求。我在实际项目中对比过圆形标定板Asymmetric Circles Grid和棋盘格圆形标定板在运动模糊下检测更稳但在大倾角拍摄时圆的中心会因透视变形产生系统性偏移。棋盘格几乎是所有视觉工程师的第一选择OpenCV对它的支持也是最成熟的。3. 棋盘图制作与拍摄误差源头从这里开始渗透3.1 标定板怎么生成别用Word里自绘表格棋盘格不是用Word或者Excel画一个黑白表格就行角点坐标的精度直接决定了标定结果的精度。正确的生成方式有两种第一种用OpenCV自带的生成脚本。新版OpenCV在源码的samples目录下提供了gen_pattern.py可以生成棋盘格、圆点阵、ArUco等多种标定板支持输出SVG和PNG。指定棋盘格尺寸、格数、每格宽度就能导出一张可缩放的高清晰度SVG图。第二种自己写一个生成函数。逻辑很简单创建一张白色图像按方块尺寸循环填充黑色矩形import cv2 import numpy as np # 棋盘格参数内角点数(列,行)每个格子的边长像素 pattern_size (9, 6) square_size 30 # 像素 margin 50 cols pattern_size[0] 1 rows pattern_size[1] 1 img_w cols * square_size 2 * margin img_h rows * square_size 2 * margin board np.full((img_h, img_w), 255, dtypenp.uint8) for i in range(rows): for j in range(cols): if (i j) % 2 0: continue x0 margin j * square_size y0 margin i * square_size board[y0:y0square_size, x0:x0square_size] 0 cv2.imwrite(chessboard.png, board)这里有个很关键的细节pattern_size在OpenCV里指的是内角点数也就是黑白格子交界处的角点。9x6的内角点意味着横向有10个格子纵向有7个格子。很多新人在这里搞混生成的棋盘格数量不对或者读取角点时维度对不上直接报错。3.2 打印和贴板的实操细节打印标定板时我用的是120g以上的哑光铜版纸激光打印。哑光比高光好高光在斜射光照下会形成镜面反射角点检测直接失效。打印后用直尺量一下打印出来的实际格子宽度因为打印机的缩放误差通常有0.1mm~0.3mm测量时取多个格子的总宽度除以格子数能减小单格测量误差。贴板时一定要把纸完全贴平不能有气泡或褶皱。硬质底板我推荐3mm厚的铝板或者亚克力板不要用瓦楞纸板纸板在多次手持后会弯折标定板一旦不共面整个标定的几何假设就崩了。有条件的话把标定板四边用双面胶固定确保它完全平直。3.3 拍摄决定成败的十五张照片标定板准备完毕接下来是最容易被低估的环节——拍图。我在帮同事排查标定效果差的问题时发现十次有八次不是代码问题而是拍摄姿势有问题。拍摄的黄金法则是“多变”变化角度、变化位姿、变化图像中的位置。你需要让棋盘格在画面中呈现:正面正对相机左右旋转15°~30°上下俯仰15°~30°放在图像左上、右上、中心、左下、右下五个区域靠近相机拍几张、远离相机拍几张推荐的采集数量是15~20张。少于10张内参求解的约束不足标定结果不稳定多于30张边际收益极低还容易引入模糊帧。如果你的镜头是广角边缘畸变很大要保证棋盘格至少有一部分出现在画面的边界处否则边缘畸变参数拟合不出来。拍摄时还要注意几个物理条件光照要均匀避免强光直射镜头造成光晕手持或固定相机都可以但拍摄过程中相机不能变焦变焦会改变内参标定结果就废了棋盘格不要在画面中占过小的面积我推荐每张图中棋盘格的边长至少占图像短边的1/4。一个能显著提升成功率的小技巧是拍摄时轻微小幅度移动棋盘格而不是完全静止这样角点检测的像素响应是多个采样的叠加配合亚像素细化能拿到更稳定的坐标。3.4 筛图标准模糊、过曝、反光的帧直接删拍完20张不要急着全部灌进代码。快速用图像查看器扫一遍删掉以下几类任何有运动模糊的帧角点检测的精度直接降一个量级棋盘格区域存在高光反光的帧白色格子在过曝后失去纹理角点退化为两黑夹一白很容易检测出错棋盘格被物体遮挡的帧遮挡会引入错误的角点梯度干扰超过半幅图像被棋盘格占满的极端特写帧过大的视角会让最外圈角点落在畸变最严重的区域反而主导优化方向保留15~20张干净、多样、清晰的图比灌进去50张废图效果好得多。标定算法的本质是一个最小二乘优化混入异常值会污染全部参数。4. 角点提取与核心标定代码4.1 findChessboardCorners与亚像素细化角点提取是标定流程中的第一步也是误差的主要来源。OpenCV提供两个关键函数先粗检测再细提取import cv2 import numpy as np import glob # 内角点数与生成棋盘格时保持一致 pattern_size (9, 6) square_size 19.0 # 实际打印后的格子宽度单位mm 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 * square_size obj_points [] # 世界坐标系中的角点 img_points [] # 图像坐标系中的角点 images glob.glob(calib_imgs/*.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 not ret: print(f角点检测失败: {fname}) continue # 亚像素细化 corners_refined cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) obj_points.append(objp) img_points.append(corners_refined) # 可视化检测结果 vis cv2.drawChessboardCorners(img.copy(), pattern_size, corners_refined, ret) cv2.imwrite(fvis_{fname.split(/)[-1]}, vis)cornerSubPix的窗口大小选(11, 11)迭代条件里的epsilon设0.001实测效果比较稳定。窗口太小亚像素精度不够窗口太大容易把邻近角点拉进来干扰计算。findChessboardCorners有时会检测失败尤其是在大倾角、光照不均、模糊的情况下。OpenCV从4.x版本起增加了findChessboardCornersSB它基于quad检测和Saddle Point特征对模糊和大倾角的鲁棒性明显更好。如果普通版本检测失败的帧比较多可以换用SB版本代价是耗时高一些。4.2 calibrateCamera一次拿到所有参数角点收集完成后核心调用就是calibrateCameraret, mtx, dist, rvecs, tvecs cv2.calibrateCamera( obj_points, img_points, gray.shape[::-1], None, None ) print(内参矩阵:) print(mtx) print(畸变系数 (k1, k2, p1, p2, k3):) print(dist.ravel()) print(重投影误差:, ret)理解一下返回的含义mtx是3x3内参矩阵fx是x方向焦距以像素为单位fy是y方向焦距cx和cy是主点坐标。对于像素为方形传感器fx和fy应该非常接近如果差距超过2%说明镜头的像素不是方形或者标定数据有问题dist是畸变系数顺序是(k1, k2, p1, p2, k3)OpenCV的默认输出只有5个参数可以通过DISTORTION_MODEL选择更复杂的模型rvecs和tvecs是每张图标定板相对相机的旋转向量和平移向量内部求解逻辑一句话概括先用线性方法DLT和单应性分解求初始解再用Levenberg-Marquardt做非线性优化最小化所有角点的重投影误差。这就是张正友法论文中提出的两步法calibrateCamera完整实现了它。4.3 验证标定结果重投影误差与畸变校正图标定完成不自检等于白标。最直观的验证手段是看两个东西第一个是重投影误差。把检测到的角点用标定出的外参重新投影回图像平面计算与原始检测位置的均方根误差。OpenCV的calibrateCamera返回值ret就是这个值。我的经验标准普通USB摄像头在VGA分辨率下重投影误差小于0.3像素就算优秀小于0.5像素完全可用超过1像素说明数据或代码有问题。第二个是畸变校正效果。用undistort或initUndistortRectifyMap remap把一张原图校正观察画面中的直线是否变直# 获取去畸变映射表 h, w img.shape[:2] 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里的alpha参数值得单独说。alpha0时校正后的图像会裁剪掉边缘的黑色空洞区域画面有效区域缩小alpha1时保留全部原始像素但边缘会有黑色填充。做测量时用alpha0做视觉定位时通常alpha1或者用0.5折中看你是更重视画面完整性还是边缘无黑边。4.4 结果不太好怎么办一次完整的排查思路如果重投影误差大于1像素按以下顺序排查第一步检查是否所有图片的棋盘格和角点都检测正确。用drawChessboardCorners把结果画出来一眼就能看出哪张图的角点错位。常见错误是在大倾角下OpenCV的角点顺序发生跳变。第二步检查棋盘格实际物理尺寸。我踩过一次很隐蔽的坑打印机的缩放导致实际格子宽度是19.2mm而代码里写的是19.0mm。这种系统性误差会让焦距的尺度有约1%的偏差但重投影误差看起来依然很好。用它做三维重建时物体的长度误差会同步放大1%。第三步检查主点cx和cy是否离图像中心太远。对于大多数COM口摄像头主点应该在图像中心附近几百像素范围内。如果标定出的主点偏离到角落要么是初始值给得不好要么是标定图的位姿覆盖不够全面——棋盘格始终出现在画面的同一侧主点就会被拉扯过去。第四步检查帧的清晰度。重新浏览一遍图像删除任何在角点附近有模糊的帧。4.5 C版本的关键差异虽然Python的OpenCV标定API和C几乎一一对应但工程落地时有两个差异值得注意第一个是findChessboardCorners的输入类型。C接口里cornerSubPix需要的输入是CV_8UC1的Mat输出是vector 而findChessboardCorners接受vector 作为输出。第二个是calibrateCamera的调用签名。第二个参数应该传入vectorvector 第三个参数是vectorvector 第四个参数是imageSize。C版本可以通过flags参数指定CV_CALIB_FIX_K3来固定k3不参与优化在小畸变镜头下这个技巧可以让求解过程更稳定。cv::Mat cameraMatrix, distCoeffs; std::vectorcv::Mat rvecs, tvecs; double rms cv::calibrateCamera( objectPoints, imagePoints, imageSize, cameraMatrix, distCoeffs, rvecs, tvecs, cv::CALIB_FIX_K3);在Visual Studio中配置OpenCV时我建议直接用vcpkg安装而不是手动下载省去一堆环境变量和lib配置的麻烦。vcpkg install opencv4:x64-windows一条命令就能装好包含contrib在内的完整库。5. 让标定结果真正可用的四个进阶操作5.1 单应性标定只标内参不平移视角如果你只需要做平面物体的尺寸测量比如传送带上的工件、文档扫描矫正可以退一步只做平面单应性标定——一张棋盘格照片、一个findHomography就能搞定像素坐标系到物理平面坐标系的映射。它的适用场景和完整标定有明显区别单应性标定适用于工件固定在一个平面上的场景它把透视变换直接打包进H矩阵不区分内参和外参结果是平面的像素-毫米换算关系。但一旦物体离焦平面有高度差单应性标定就会引入测量误差因为H矩阵只对特定高度的平面成立。完整标定解出内参后结合物体高度和相机到平面的相对位姿理论上可以对任意高度的平面做测量。我在项目中用到的实践是传送带测量用单应性机械臂抓取用完整标定两者各司其职不要混用。5.2 使用标定结果完成像素到真实坐标的映射标定参数拿到后机械臂抓取场景下的坐标映射公式是对图像上的像素点(u, v)先用undistortPoints去除畸变pts np.array([[[u, v]]], dtypenp.float32) undist_pts cv2.undistortPoints(pts, mtx, dist, Pmtx)得到去畸变后的归一化相机坐标(xn, yn)通过外参标定板到相机的位姿或手眼标定结果转换成机器人基座坐标。这里最容易犯的错误是直接拿(u, v)像素坐标去乘一个比例系数。因为存在畸变和透视图像边缘的像素比例和中心的比例可能差好几个百分点。正确的做法是先把图像去畸变到一个“理想相机”像素平面再做仿射或单应性映射。5.3 手眼标定从固定相机到机械臂协同这一节属于进阶中的进阶但我还是要提一句。OpenCV的calibrateHandEye函数封装了Tsai、Park等四种求解方法。它的目标是求解相机坐标系与机械臂末端坐标系之间的固定变换关系配合张正友标定出的相机内参才能实现“看到物体 - 换算到机器人坐标”的完整链路。如果你的项目里相机装在机械臂末端Eye-in-Hand移动机械臂到多个位姿分别记录机械臂位姿和标定板在图像中的外参调用calibrateHandEye即可得到相机到末端的变换矩阵。这个领域的坑比单纯内参标定多得多主要在于机械臂位姿精度本身有限标定板的位姿变化太小会导致求解退化。建议机械臂运动时让末端做旋转和平移的复合运动不要只平移因为纯平移对旋转部分的约束不足。5.4 一张表总结常见报错与含义error: (-215:Assertion failed) nimages 0 in function cv::calibrateCameraRO 含义: 没有成功检测到任何棋盘格角点检查图片路径和pattern_size error: (-215:Assertion failed) ni 0 in function cv::cornerSubPix 含义: 传入cornerSubPix的corner数组为空或尺寸不匹配 error: OpenCV(4.x) ... !ssize.empty() in function cv::remap 含义: initUndistortRectifyMap生成的映射表为空检查图像尺寸是否与标定时一致 error: (-215:Assertion failed) nimages npoints ... 含义: obj_points和img_points的图片数量不一致某张图角点检测失败后没有跳过如果程序直接跑崩最可能是角点检测失败的那一帧图片没被正确处理应该在检测失败后用continue跳过而不是继续往列表里append。6. 关于标定精度我想说的几句实在话标定这种事精度不是靠某一个大杀器函数提上去的而是靠每一个环节都做到位。棋盘格的平整度、打印尺寸的准确性、拍摄姿态的多样性、角点亚像素细化的参数每一环只贡献零点几个像素的误差但累积起来就是天壤之别。我在实际项目里的体会是标定精度的上限往往不是算法决定的而是标定板本身的物理质量决定的。你拿一张褶皱的A4纸做的标定板换任何算法都不可能拿到亚像素级别的标定结果。如果有条件做高精度测量推荐去打印店用PVC材料打印棋盘格或者直接买成品陶瓷标定板平整度和热稳定性都好得多。另外建议把标定的全部参数序列化保存成一个JSON或YAML文件内参矩阵、畸变系数、图像尺寸、标定时间都记下来。项目迭代半年后你大概率会忘记当时用的哪台相机、哪个分辨率留个记录能省很多排查时间。OpenCV的FileStorage可以直接读写YAMLPython侧用json就行。最后分享一个我经常用来快速判断标定质量的小技巧拿一把直尺放在画面中间拍一张图做畸变校正用cv2.Canny提取边缘后观察校正图里的直尺边缘是否是一条直线。如果边缘有可见弯曲说明畸变系数拟合得不够好需要补拍更多边缘区域的图片重新标定。这个方法两分钟就能完成比单独看重投影误差更直观也更接近最终应用的真实效果。本文还有配套的精品资源点击获取
返回列表