ARTICLE DETAIL

资讯详情

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

相机标定与双目标定:从内参解算到标定板选型的工程实践

相机标定与双目标定:从内参解算到标定板选型的工程实践 简介面向计算机视觉开发者与研究人员的相机标定项目源码覆盖相机内参标定与双目标定支持多种相机模型和多种标定板可用于机器人视觉、自动驾驶、三维重建等场景。压缩包共223个文件包含170张jpg示例图像、17个h头文件与15个cc源码文件另有yaml配置、PDF说明、Python脚本等辅助内容包体仅24.15MB轻量易用。项目已有420人学习/浏览适合刚接触标定的初学者也适合需要高效标定方案的工程人员。源码中实现了针孔相机、畸变相机、全景相机等多种模型的参数估计与畸变校正提供棋盘格、圆形格等标定板的角点检测算法并集成张正友标定法、外参估计及结果验证模块。yaml文件可快速调整标定参数PDF文档与jpg样例图方便对照测试整套源码结构清晰便于二次开发与实际部署。1. 相机标定不是玄学内参、双目标定和标定板选型的底层逻辑做视觉测量、三维重建或者双目深度估计的工程师几乎都绕不开「相机标定」这道坎。这个标题里的核心词「相机内参标定双目标定」对应的是两类完全不同的标定任务单目内参标定解决的是「像素坐标到相机坐标系的映射关系」而双目标定解决的是「两个相机之间的相对位姿」。很多人用 OpenCV 的calibrateCamera跑通一遍就以为万事大吉结果换到鱼眼镜头、换到不同光照环境、或者把标定板稍微倾斜一点重投影误差就飙到 0.5 像素以上重建出来的点云直接变形。这个项目源码的价值在于它把多种相机模型针孔、鱼眼、全景和多种标定板棋盘格、圆点、ArUco统一封装在了一套标定流程里而不是让使用者自己去拼凑 OpenCV 的散装函数。这篇笔记我会从相机模型和标定板的选型讲起然后拆解内参标定和双目标定的完整实现步骤最后把最容易让结果翻车的几个坑一次性说清楚。适合看这篇笔记的人是自己已经用 OpenCV 或 MATLAB 做过标定但结果不稳定的人或者是刚接手双目项目、需要在不同相机和标定板之间切换的工程师。我会把参数怎么设、代码怎么改、错误结果怎么排查都写到位新手能照做熟手能挑刺。2. 相机模型与标定板选型先搞清楚你的镜头吃哪一套参数2.1 针孔模型 vs 鱼眼模型 vs 全景模型畸变参数的数量级差在哪标定的第一步不是拍标定板而是确认你的相机适合哪种成像模型。针孔模型Pinhole假设光线通过光心直线投影到像平面畸变用布朗模型Brown-Conrady描述OpenCV 里对应cv2.CALIB_RATIONAL_MODEL包含径向畸变k1, k2, k3和切向畸变p1, p2有些标定工具还会额外加入k4, k5, k6。这种模型对普通工业镜头和手机主摄足够但对视场角超过 120° 的鱼眼镜头针孔模型的畸变系数会变得极不稳定因为鱼眼镜头的投影关系本身就不是直线透视而是等距投影或等立体角投影。鱼眼模型Fisheye在 OpenCV 里对应cv2.fisheye.calibrate它的投影函数是r f * theta其中theta是入射光线与光轴的夹角畸变多项式是theta_d theta * (1 k1*theta^2 k2*theta^4 k3*theta^6 k4*theta^8)。这个模型和针孔模型本质区别在于鱼眼模型的畸变系数作用在入射角上而不是归一化像平面上所以你在做去畸变时不能用cv2.undistort必须用cv2.fisheye.undistortImage或cv2.fisheye.initUndistortRectifyMap。全景模型Omnidirectional则用统一球面模型Unified Camera ModelOpenCV 的cv2.omnidir模块适合折反射全景相机和超广角猫眼镜头。选型的判断依据很简单如果图像边缘的直线明显弯曲成弧形先看视场角小于 100° 还能凑合针孔模型加高阶畸变大于 120° 直接用鱼眼模型如果镜头是全景镜头或者带反射镜面直接走omnidir。项目源码里通常会在配置文件中暴露camera_model字段值可以是pinhole、fisheye、omnidir不同模型对应不同的标定入口和去畸变调用方式这点在阅读源码时最先确认。2.2 棋盘格 / 圆点阵 / ArUco 标定板为什么不能随便用图片打印了事标定板的核心作用是把「世界坐标系下的已知 3D 点」和「图像中的 2D 特征点」建立对应关系。棋盘格Checkerboard是最传统的选择OpenCV 的cv2.findChessboardCorners检测稳定但存在两个硬伤一是只能检测内角点二是对遮挡和反光敏感。圆点阵CirclesGrid用cv2.findCirclesGrid检测圆形轮廓对运动模糊和光照变化更鲁棒因为圆的几何中心比角点更容易通过重心计算得到亚像素精度但它要求标定板平面足够平整否则圆的投影变成椭圆后中心偏移。ArUco 标定板cv2.aruco是这几年做双目或多相机标定的常用方案它的优势是每个 Marker 自带 ID允许部分遮挡且单个 Marker 就能提供位姿估计所以它可以用来做「自由移动」的标定板采集——不需要整块板完整出现在视野中。项目源码里如果同时支持三种板子通常在配置里写pattern_type: chessboard | circles | aruco对应的检测函数分别是findChessboardCorners、findCirclesGrid、aruco.detectMarkers。从精度角度我不建议用普通 A4 纸打印标定板做高精度测量因为热胀冷缩和纸张弯曲会直接造成亚毫米级误差。硬板推荐 3mm 铝基板或亚克力板表面哑光。若不得不用纸张贴在平整玻璃上并确保室内温度稳定。标定板尺寸方面棋盘格方格边长和圆点间距要满足「每张图像里格子边长至少占 20 像素」否则角点检测的亚像素精度无法保证。用 ArUco 时aruco.Dictionary_get(cv2.aruco.DICT_6X6_250)是默认选择词典越大误检率越低。2.3 标定板标定和九点标定的区别别把手眼标定和内参标定混为一谈热词里有「标定板标定和九点标定的区别」这里顺手做一个关键区分。标定板标定Calibration Board Based求解的是相机内参和外参属于 photogrammetry 范畴九点标定Nine-Point Calibration是手眼标定里的一种简化操作用在机器人视觉引导场景中通过让机械臂末端走九个已知点建立像素坐标到机器人基座坐标系的仿射变换矩阵直接跳过内参解算。两者完全是不同层面的问题。项目源码如果明确写了「相机内参标定双目标定」那么核心是前者。九点标定解决的是 eye-to-hand 或 eye-in-hand 的坐标映射它的算法通常是cv2.estimateAffine2D或最小二乘拟合。很多新手把九点标定的误差归咎于内参不准其实九点标定对镜头畸变不敏感真正影响它的是机械臂绝对定位精度和相机安装松动。这块内容在源码里通常不会出现但理解区别能帮你避免在错误的方向上调参。3. 单目内参标定落地从采集到重投影误差低于 0.2 像素的完整流程3.1 标定板图像采集规范数量、姿态、光照和清晰度内参标定结果的精度70% 取决于采集的图像质量20% 取决于标定板加工精度只有 10% 取决于优化算法。很多人在标定板面前随意拍几张然后发现重投影误差 0.3 像素就抱怨源码不行其实是采集环节作弊。我的建议是采集 1525 张覆盖三个维度距离从近到远标定板在画面中的占比从 30% 到 70%俯仰角从 -30° 到 30°偏航角从 -30° 到 30°并且保证每张图像中标定板完整性良好边缘不超出视野但也不能只放在画面正中央。光照是被绝大多数人忽略的变量标定板表面不能有高光反射否则棋盘格的角点或者 ArUco 的黑色边框会被高光吞掉也不能在荧光灯下拍摄因为 50Hz 的光频闪会造成图像亮度不均匀检测时角点邻域灰度对比度不足。推荐使用漫射光环境或者用两台白炽灯从两侧打光。图像的清晰度也很关键如果镜头光圈开得太大导致景深太浅标定板边缘出现模糊亚像素角点检测就会偏向模糊方向系统性误差是无法通过后续优化消除的。采集时的标准动作是固定相机手持标定板缓慢移动每移动一个位置拍一张不要连续拍摄同一姿态的连拍因为相邻帧几乎提供相同的信息还会让非线性优化中的信息矩阵接近奇异。如果你用的是双目相机两颗镜头必须同时触发拍摄同一姿态的标定板也就是「双目标定采集同步」后面会展开讲。3.2 内参标定的核心代码检测角点、传入物点坐标、跑优化以 OpenCV 为例一套稳定的单目内参标定代码逻辑如下假定用的是棋盘格标定板规格 9x6 内角点方格边长 25mmimport cv2 import numpy as np import glob # 棋盘格内部角点数量宽, 高 pattern_size (9, 6) square_size 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 * square_size objpoints [] # 每张图像对应的3D物点 imgpoints [] # 每张图像对应的2D像点 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: criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) corners2 cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) objpoints.append(objp) imgpoints.append(corners2) # 标定返回内参矩阵、畸变系数和每张图的外参 ret, mtx, dist, rvecs, tvecs cv2.calibrateCamera( objpoints, imgpoints, gray.shape[::-1], None, None ) print(重投影误差:, ret) print(内参矩阵:\n, mtx) print(畸变系数:, dist.ravel())代码逻辑说明objp是按行生成的世界坐标mgrid生成从(0,0)到(8,5)的网格乘以square_size后单位变成 mm。cornerSubPix是亚像素细化窗口大小(11, 11)表示相邻角点距离的搜索半径这个值应该略大于角点间距在图像中的像素太小会细化不足太大容易把周围不相关的角点拉进来。calibrateCamera内部跑的是 Levenberg-Marquardt 优化返回的重投影误差ret是每个角点的均方根误差单位是像素。参数设置的常见误区pattern_size是内角点数不是格子数。9x6 的棋盘格外观看是 10x7 个格子新手经常在这里传错导致findChessboardCorners永远返回 False。square_size只影响外参的尺度不影响内参矩阵的值但如果你之后要做三维重建这个值必须准确否则重建出来的物体尺寸会整体缩放。如果标定结果里dist非常大比如k1 -0.5大概率是采集图像中畸变太大的鱼眼镜头用了针孔模型或者标定板图像过少太单一。3.3 怎么判断标定结果的好坏重投影误差会骗人calibrateCamera返回的ret是最直观的质量指标但它会骗人。误差小于 0.1 像素不代表标定就准因为误差是在同一组数据上拟合出来的存在过拟合风险。我的做法是保留 3 张图像不参与标定在标定完成后单独计算这 3 张图的投影误差如果验证集误差比训练集误差大一倍以上说明图像采集的姿态覆盖不够优化只是硬拟合了训练数据。另一个有效的交叉验证手段是把不同子集的图像分别标定对比内参矩阵的差异。比如从 20 张图中随机抽取 12 张标定 5 次如果fx的变化超过 0.2%说明某些图像姿态过于集中需要重新补充采集。对于双目系统还要额外验证左右目内参的焦距比fx_left / fx_right是否接近 1。如果你用的是鱼眼模型cv2.fisheye.calibrate返回的误差和针孔模型的误差量级不直接可比鱼眼模型因为畸变模型更贴合误差通常会更高一点不能直接把 0.2 像素的针孔标准套过去。4. 双目标定实现双目内参、外参、极线校正一条链路4.1 同步采集双目标定的第一步不是标定是把两边的图对齐双目相机标定在流程上比单目多一个关键前置步骤立体同步采集。如果你用的是 D435i 这类自带双目同步的深度相机左右目硬件已经同步直接用左右图对标定即可。但如果你用的是两个独立相机自制的双目系统就必须解决软同步问题——两个相机如果不同时拍摄标定板在移动过程中左右图拍摄到的是不同的空间位置双目标定算法会把这种时间错位误判为外参误差。我的常用做法是在一个纯色背景上高速移动标定板然后用以下两种方式之一同步一是用硬件触发两个相机接同一根外部触发线通过 GPIO 同时采集二是软件同步近似用一个高速闪烁的 LED比如通过串口控制亮灭在左右图像中用时间戳叉关联但这只适用于标定板静止的场景。如果两个相机之间帧率不一致比如一个 30fps 一个 60fps直接硬同步是做不到的只能保证每个左右图对的采集时间间隔小于 1 帧周期然后用插值处理移动造成的细微误差。双目标定的图像数量和平单目类似20 对左右图比较理想但姿态覆盖要求更高标定板在左右相机视野里都需要完整可见或大部分可见并且需要在不同深度位置重复采集。如果你发现某一对图中一个相机看不到完整标定板建议直接删除这对图不要让标定算法用残缺的角点做估计因为cv2.stereoCalibrate是基于相同的物点对应关系求解的左右视野不一致会引入离群点鲁棒性差的 LM 优化很容易被带偏。4.2 双目标定代码从单目标定到左右目外参求解双目标定的流程是先对左右目分别做单目标定拿到各自的内参和畸变再以这些内参作为初值联合优化两目的相对位姿R, T和共同的内参精调值。OpenCV 的cv2.stereoCalibrate可以一步到位但更稳妥的做法是分步走先固定单目结果做一次外参粗估计再放开内参做联合优化。import cv2 import numpy as np # 假设已经分别标定好左右目内参 mtxL, distL, mtxR, distR # objpoints 是每张立体图对共享的物点imgpointsL/imgpointsR 是对应的像点 criteria (cv2.TERM_CRITERIA_MAX_ITER cv2.TERM_CRITERIA_EPS, 100, 1e-5) ret, mtxL, distL, mtxR, distR, R, T, E, F cv2.stereoCalibrate( objpoints, imgpointsL, imgpointsR, mtxL, distL, mtxR, distR, image_size, # (width, height) criteriacriteria, flagscv2.CALIB_USE_INTRINSIC_GUESS | cv2.CALIB_FIX_ASPECT_RATIO ) print(立体标定误差:, ret) print(旋转矩阵 R:\n, R) print(平移向量 T (单位由标定板 square_size 决定):, T.ravel())关键参数说明flags里的CALIB_USE_INTRINSIC_GUESS表示把传入的mtxL/distL作为初值优化而不是完全重新估计。如果不加这个 flag函数会用零初值从头解算结果通常会差很多。CALIB_FIX_ASPECT_RATIO固定fx/fy比例对于像素是正方形大多数 CCD/CMOS 传感器满足的情况这个 flag 可以减小优化参数数量提升数字稳定性但如果你的相机传感器像素不是正方形少见比如某些工业相机需要查手册固定这个比例反而会引入误差。T的物理单位由标定板的square_size决定。如果你设定square_size25mm那么T的单位就是 mm通常双目基线距离在 5cm20cm 之间即T[0]在 50200 左右。如果算出来T只有 0.5几乎可以肯定square_size写错了。E是本征矩阵F是对极几何矩阵通常在后端不做直接使用但可以用cv2.decomposeEssentialMat检查R, T的合理性不过一般不需要。4.3 立体校正让左右图像极线对齐重投影误差的最终验证双目标定拿到R, T后下一步是立体校正Rectification把原始左右图像变换成极线对齐的像对这是后续立体匹配的前提。OpenCV 的cv2.stereoRectify会用R, T计算左右目的校正映射矩阵然后结合内参得到无畸变且极线水平的图像。R1, R2, P1, P2, Q, validPixROI1, validPixROI2 cv2.stereoRectify( mtxL, distL, mtxR, distR, image_size, R, T, alpha0, # 0 表示裁剪掉无信息的区域1 表示保留全部像素 newImageSizeimage_size, flagscv2.CALIB_ZERO_DISPARITY # 让光心共面适合做高精度双目 ) mapxL, mapyL cv2.initUndistortRectifyMap( mtxL, distL, R1, P1, image_size, cv2.CV_32FC1 ) mapxR, mapyR cv2.initUndistortRectifyMap( mtxR, distR, R2, P2, image_size, cv2.CV_32FC1 ) rectL cv2.remap(imgL, mapxL, mapyL, cv2.INTER_LINEAR) rectR cv2.remap(imgR, mapxR, mapyR, cv2.INTER_LINEAR) # 极线对齐验证绘制水平线观察特征点是否落在同一行 # cv2.line(rectL, ...), cv2.line(rectR, ...)alpha参数是很多人纠结的地方。alpha0会让校正后的图像裁掉上下边缘的无效区域有效画面变小但保留下来的区域极线对齐最准alpha1保留所有原始像素但边缘可能出现拉伸和黑边极线在边缘处可能会有轻微不齐。对于双目深度估计建议用alpha0因为深度图通常只需有效区域。Q矩阵是视差转深度的投影矩阵格式为[ [1,0,0,-cx], [0,1,0,-cy], [0,0,0,f], [0,0,-1/Tx, (cx-cx)/Tx] ]后续用cv2.reprojectImageTo3D进行深度恢复时要用到。验证极线对齐的快速方法取一张标定板的图校正后在左图上画几条水平线在右图对应行上观察角点是否落在同一行。如果偏差超过 1 像素重新检查T的符号和stereoCalibrate的输入顺序。T的正负方向取决于左右目的定义OpenCV 中T表示左相机坐标系下的右相机原点位置如果你的输入左右图顺序反了T的符号会互换极线校正结果会明显错误左右图垂直方向偏移巨大。5. 避坑/常见问题/排查手册标定过程中最常翻车的 5 个环节5.1 标定板角点检测一直失败现象、原因、解决现象findChessboardCorners对某些图像返回retFalse或者findCirclesGrid检测到的圆心数量不对。原因通常有三个一是pattern_size与实际标定板内角点数不符二是图像中角点对比度不足比如白色标定板在强光下泛白黑格边缘被高光模糊三是标定板太远角点间距在图像中小于 5 个像素超出了检测器的空间分辨率。解决方法是先检查pattern_size然后在图像采集界面实时预览检测结果确保每张图都检测成功再保存。对于反光问题可以用哑光覆膜覆盖标定板表面。ArUco 检测失败是另一套原因detectMarkers需要标定板字典和实际 marker 一致如果用的DICT_4X4_50但板子打印的是DICT_5X5_50检测率基本为零。此外 ArUco 对打印尺寸敏感marker 边长在图像中如果小于 10 像素检测会非常不稳定尽量靠近拍摄或选用更大的字典。5.2 重投影误差很低但深度图明显不对现象、原因、解决现象单目和双目标定的重投影误差都小于 0.15 像素但立体匹配生成的深度图在物体边缘有系统性偏差比如平面物体的深度呈现弯曲。原因大概率出在标定板平面的平整度上。如果用的是亚克力板在较大面积时会有弯曲弯曲形变会让角点世界坐标与真实物理位置偏差导致主点cx, cy估计不准而重投影误差仍然很小因为这个误差是「相对」拟合残差。解决方法是换 5mm 以上的铝基板或碳纤维板并在标定前用千分尺验证平面度如果只有亚克力板尽量让小尺寸板小于 A4参与标定。5.3 双目基线距离变化时 T 不稳定现象、原因、解决现象把双目相机拆装后重新标定T[0]的数值变化超过 5%。原因主要是左右目之间的结构刚度不够螺丝松动导致相机在采集过程中产生了微米级位移而双目标定会把每一帧的微位移平均进外参。解决方法是确保机械结构锁紧后用红漆做标记点每次拆装后检查标记是否对位。另外不要在两台相机温度不同的情况下标定热胀冷缩会让金属底板变形T在冷热状态下差异明显。如果项目要求高精度建议加装锁紧销钉孔位而不是依靠螺丝摩擦力。5.4 使用 D435i 标定时左右目内参差异过大现象、原因、解决现象D435i 这类双目深度相机标定后左右目fx相差 1% 以上双目深度精度下降。原因有两个可能一是 D435i 的内部左右目传感器本来就有公差出厂标定数据与你自己的标定结果存在差异二是你对左右目分别单目标定时使用了不同的图像子集导致各自内参收敛到不同局部极小值。解决方法是使用同一组标定板图像对左右目各 20 张先单目再联合stereoCalibrate微调而不是直接跳过单目统一优化。还可以试试把CALIB_FIX_ASPECT_RATIO打开给优化约束让fx/fy保持一致这能避免左右目因为个别图像姿态偏差造成的焦距分裂。5.5 标定结果在工程化部署时出现退化现象、原因、解决现象在实验室标定好的内参到了户外或者夜间场景去畸变后图像边缘放大率异常。原因通常是畸变系数过度拟合了实验室的标定板姿态分布。比如采集时标定板没有覆盖画面的边缘区域那么边缘畸变完全靠模型外推一旦真实镜头边缘畸变与模型有偏差误差会很大。解决方法是标定时务必将标定板靠近画面四条边缘进行采集让每个象限都有角点覆盖。如果边缘实在拍不到可以使用cv2.getOptimalNewCameraMatrix时的alpha参数来裁剪掉畸变过于严重的边缘像素而不是强行保留。6. 进阶用法把标定集成到自动化产线和多相机系统的两个关键技巧标定做完之后真正拉开差距的是如何把标定结果嵌入到用 Arduino/PLC 控制旋转台、或者多相机拼接等场景里。这里分享两个我常用的进阶做法。第一个技巧是自动化标定的自检逻辑写一个脚本在每次产线开机时用固定位置的标定板自动检测内参漂移。具体做法是将标定板固定在一个支架上开机后程序自动拍摄一张图像检测角点再用现有的内参计算重投影误差。如果误差超过阈值比如 0.3 像素则说明镜头松动或温度漂移触发人工复标定。这个做法能在相机受到碰撞或温度变化后第一时间发现标定失效而不是等到测量结果精度下降才回忆可能出过什么事。第二个技巧是应对多相机同步标定的信息冗余问题。三个以上相机标定时直接用cv2.stereoCalibrate两两配对会出现环状误差即 A-B 的外参和 B-C 外参的乘积不等于 A-C 的直接外参。我的经验是先做两两标定把结果作为初值再运行一次图优化用非线性最小二乘统一优化所有相机到参考相机的外参这样可以消除累积漂移。项目源码里如果支持多相机通常会在核心代码里预留一个optimize_camera_poses的函数参数选择上注意要对旋转误差加权重因为旋转矩阵小角度误差对深度测量影响远比平移误差大。我的个人习惯是每次标定完成后立即把标定图像、标定结果、相机型号、镜头状态存成一个带时间戳的文件夹至少保留 3 到 5 份历史标定记录。后面做深度测量如果出现异常我会用存档恢复最近的已知良好标定状态这就是标定工作的后悔药。相机标定不是一次性的而是在产线维护里长期存活的生命周期之一环。希望这篇笔记能帮你把单目内参标定和双目标定的流程一次跑通少走我当年调试时的弯路。本文还有配套的精品资源点击获取
返回列表