ARTICLE DETAIL

资讯详情

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

相机标定实战:单目内参、双目标定与立体校正避坑指南

相机标定实战:单目内参、双目标定与立体校正避坑指南 简介相机标定项目源码面向具备一定视觉基础的研究者与工程师聚焦相机内参标定与双目标定兼容针孔、畸变、等距等多种相机模型以及棋盘格、圆形格等多种标定板可广泛应用于机器人视觉、自动驾驶、三维重建、视频监控等场景。压缩包共223个文件大小约24.15MB主要包含C源码.h/.cc、Python脚本.py、标定图像.jpg、配置文件.yaml/.yml及说明文档.md/.pdf等目录结构清晰便于按模块检索和二次开发。目前已有420人学习浏览。源码覆盖标定板特征点检测、内外参估计、畸变校正、结果验证等完整流程并提供单目与双目标定独立模块亦包含图像预处理与传统/机器学习标定算法实现整个项目设计完善可直接编译运行或参考其设计思路帮助使用者快速搭建高精度标定系统服务学术研究或工业应用。1. 相机标定为什么内参和双目非要放进同一个项目标定做得到不到位直接决定后面所有视觉算法的上限。我见过不少项目calibrateCamera跑一遍内参存进YAML就扔一边等到换一个镜头、换一块标定板或者从单目升级到双目标定流程又得从头折腾前面踩过的坑一个不落再踩一遍。这个项目把相机内参标定和双目标定做成了一套完整流程支持多种相机模型——针孔、鱼眼都有对应接口——也支持多种标定板棋盘格、ChArUco、实心圆点这些常见板型都能直接切过来用。它要解决的核心问题很具体畸变怎么去、外参怎么解、双目怎么对齐不用再靠经验猜。适合刚上手的视觉工程师也适合被标定误差反复折腾的老手拿它当参照系。2. 相机模型与标定板选型针孔鱼眼怎么选棋盘ChArUco怎么用2.1 针孔模型与鱼眼模型畸变系数到底在解什么大多数视觉标定都建立在针孔模型上。三维点经过透视投影落到成像平面像素坐标和相机坐标之间由内参矩阵K关联K里只有四个核心数字fx、fy、cx、cy分别代表两个方向的焦距按像素宽度折算和主点坐标。镜头不是理想透镜所以还要加畸变项径向畸变k1、k2、k3描述桶形和枕形失真切向畸变p1、p2描述镜头装调时产生的偏心误差。标定求内参本质就是把K和这一组畸变系数一起解出来。鱼眼镜头是另一条路线视场角能到180度以上针孔模型那几个多项式根本压不住。鱼眼模型改用等距投影这类映射关系标定参数的结构也和普通镜头不一样。经验上普通镜头直接走OpenCV的calibrateCamera鱼眼镜头要调用fisheye模块里的calibrate两边不能混用。这个项目把两类模型分开封装选哪种由配置参数控制就是为了防止有人拿普通镜头接口硬解鱼眼图。我自己的习惯是低畸变镜头先把畸变系数砍到k1、k2、p1、p2四个第一次标定只关心重投影误差。误差能稳在0.5像素以内就不再加高阶项超了再放开k3还不够就回头查板子是不是翘了。畸变系数给得越多拟合自由度越大十几张图的时候很容易过拟合结果就是参数一套看起来漂亮换个场景立刻露馅。相机模型典型镜头主要参数常见标定接口针孔模型工业镜头、监控、手机摄像头fx fy cx cyk1 k2 p1 p2 k3cv2.calibrateCamera鱼眼模型广角鱼眼、车载环视、全景fx fy cx cy 鱼眼畸变参数cv2.fisheye.calibrate全向模型折反射镜头、特殊全景高阶投影参数项目内封装接口选模型的判断标准不复杂镜头视场角小于90度、畸变肉眼可见也不算夸张针孔模型就够用镜片边缘出现明显弧形畸变比如180度环视就必须切鱼眼模型。还有一种情况是深度相机D435i这类RGB模组通常用针孔模型就能描述鱼眼版本的红外模组要按实际项目选不能想当然。张正友标定法的数学内核是把每张标定图看成一次单应变换。棋盘格平面上的点坐标和像素坐标之间有一个3×3单应矩阵这个矩阵同时编码内参和外参。因为所有图像共享同一组内参收集足够多姿态后用线性最小二乘先求出内参初解再做非线性优化把畸变系数、外参一起拟合。这里有个隐含条件每张图必须提供足够的点对应而且姿态要多变否则单应矩阵退化内参解不出来。这也是为什么同一套代码不同人标出来的结果千差万别——采集那一关就决定了结果上限。你要是更习惯Matlab的标定工具箱思路完全平行区别只是项目源码落在OpenCV链路里后面接去畸变、立体匹配、点云生成不用倒数据。2.2 棋盘格、ChArUco和实心圆点三种板子的选型逻辑棋盘格是张正友标定法的标配也是三种板子里最好上手的。角点检测要按整幅图找黑方格交点板子缺一角或者反光把某片区域吞掉findChessboardCorners大概率整帧返回失败。这个脾气在做产线设备标定时很讨厌机械结构一挡标定板往往摆不出完整姿态。ChArUco板在这类场景里更耐造。它外围是ArUco码内部是棋盘格即便视野里有部分码被遮挡只要还有几个标志可解就能推定棋盘格的大概位置再去做亚像素细化。同样遮挡条件下ChArUco能保住的有效帧数比纯棋盘格多不少。代价是检测流程多一步先解ArUco再映射角点。项目里带了ChArUco板的生成脚本能直接输出PDF打印不用自己拼图。实心圆点标定板走的是圆心检测路线对图像模糊和运动模糊的容忍度更好因为用的是整个圆斑的边界信息不是单个角点的梯度受噪声影响更小。要注意对称圆点阵列的方向歧义6×7的圆点格从某个角度拍上下左右可能互换。OpenCV的findCirclesGrid提供CALIB_CB_SYMMETRIC_GRID和CALIB_CB_ASYMMETRIC_GRID两个标志位来区分。我基本只用非对称圆点阵列省掉方向判定的烦恼。板型检测目标遮挡容忍精度潜力适合场景棋盘格角点低高亚像素细化实验室、标准标定ChArUco角点ArUco码高高产线、遮挡多的现场实心圆点圆心中中高模糊严重、高速运动还有一个概念经常被混在一起问标定板标定和九点标定的区别。九点标定是在固定平面内求像素坐标和机械手坐标的仿射关系本质是手眼标定标定板和镜头畸变都不参与标定板标定求的是相机内参、畸变和外参属于几何标定。这个项目解决的是后者。你要做机械臂平面抓取先去做九点标定要做三维测量、双目视差、深度恢复才轮到标定板标定上场。打印环节也要多提醒一句格子尺寸square_size要按物理尺度填别图省事填1。填1等于把所有坐标的尺度换成格子数之后外参平移量全数以格为单位换算起来极易翻车。我的习惯是量多个格子宽度除以格子数取平均直接用尺子量单格容易产生0.1毫米级别的误差。采集时按项目配套脚本的姿态提示让板子在左右、上下、远近三个维度分别变化保证单应矩阵不退化。3. 单目内参标定从原始图像到可复用的相机配置文件3.1 采集姿态与角点检测15张图里该有什么信息进入单目内参标定第一关是采集标定板图像。数量上通常15到20张不是越多越好而是要覆盖不同位置和姿态。具体说板子在画面里至少要有三个明显变化朝向左右、朝向上下、距离远近。如果板子始终平放在相机正前方所有图像退化成几乎同一个姿态外参自由度释放不出来内参解很容易偏移。有些项目一次拍50张但姿态高度重复信息量还不如15张拉开姿态的。光照对结果的影响比想象中大。板面反光会让角点灰度梯度变形亚像素细化时把角点位置拉偏。常见做法是环境光均匀避免直射强光和镜面反射工业光源优先用漫反射板打光。另外拍照时尽量保持静止运动模糊会造成角点定位漂移。无论手持相机还是固定相机移动板子曝光时间都不要拉太长。检测角点的数据准备逻辑基本是这样import cv2 import numpy as np import glob # 棋盘格内角点数11x8指内部交点不含最外沿 pattern (11, 8) square_size 0.02 # 单位米用游标卡尺量实际值 objp np.zeros((pattern[0] * pattern[1], 3), np.float32) objp[:, :2] np.mgrid[0:pattern[0], 0:pattern[1]].T.reshape(-1, 2) objp * square_size obj_points [] # 世界坐标 img_points [] # 像素坐标 for fname in sorted(glob.glob(calib_imgs/*.jpg)): img cv2.imread(fname) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ok, corners cv2.findChessboardCorners(gray, pattern, None) if not ok: print(f跳过 {fname}未找到完整棋盘) continue criteria (cv2.TERM_CRITERIA_MAX_ITER cv2.TERM_CRITERIA_EPS, 30, 0.001) corners cv2.cornerSubPix(gray, corners, (5, 5), (-1, -1), criteria) obj_points.append(objp) img_points.append(corners) print(有效帧数, len(obj_points))pattern和square_size必须严格匹配板子实物错一个值后面内参全部没有意义。cornerSubPix是在整数角点周围取小窗口做亚像素级精定位精度比原始检测提升不少这步不要省。第7到9行生成世界坐标是OpenCV的标准写法把格子索引展开成平面坐标乘以square_size才是真实物理尺度。sorted关键字是按文件名顺序读取帮助回看哪些帧被跳过排查采集阶段问题很方便。如果板子是ChArUco这一段检测逻辑要换成aruco字典加CharucoBoard引用对应cv2.aruco.detectMarkers接口圆点板子用cv2.findCirclesGrid。项目里把三种板型的检测入口分开封装改配置项就能切不需要动主流程。3.2 calibrateCamera到配置文件内参怎么落地拿到obj_points和img_points后调用calibrateCamera完成内参求解flags cv2.CALIB_RATIONAL_MODEL ret, mtx, dist, rvecs, tvecs cv2.calibrateCamera( obj_points, img_points, gray.shape[::-1], None, None, flagsflags ) print(重投影误差像素, ret) print(内参矩阵 K, mtx) print(畸变系数 dist, dist.ravel()) # 写 YAML 配置供后续进程直接加载 fs cv2.FileStorage(camera_calib.yaml, cv2.FILE_STORAGE_WRITE) fs.write(camera_matrix, mtx) fs.write(dist_coeffs, dist) fs.write(image_width, gray.shape[1]) fs.write(image_height, gray.shape[0]) fs.release()flagsCALIB_RATIONAL_MODEL表示启用k3高阶径向畸变项。如果前面几步发现畸变影响很小可以换成默认的0只用k1、k2、p1、p2结果更稳。calibrateCamera返回的mtx是3×3内参矩阵dist是畸变系数向量ret是所有角点的平均重投影误差单位是像素。ret在0.1到0.3像素之间算健康接近1像素就要回头查采集环境。FileStorage写YAML是OpenCV标准格式后续加载用cv2.FileStorage读一次就行不需要重复标定。如果你用的是鱼眼镜头这一段要换成fisheye模块接口内参形式和畸变维数都不一样不能混用这正是项目单独放鱼眼模型入口的原因。保存完配置文件后我强烈建议顺手做一次去畸变验证读一张标定图片用cv2.undistort矫正肉眼看边框直线是否变直。这一步比任何数值指标都直观也最容易发现fx和fy是否写反这类低级错误。4. 双目标定从R、T求解到立体校正一次跑通4.1 双目标定的本质先固定单目再求两个外参双目标定建立在单目内参稳定的前提下。两个相机各自先做一遍内参标定然后同一时刻抓拍同一块标定板分别检测两边的角点再用stereoCalibrate把外参求出来。外参是右相机相对左相机的旋转矩阵R和平移向量T衡量两个镜头光心之间的距离和朝向差也就是基线。基线长短和方向直接决定视差范围和深度精度在结构光、双目测距这类应用里这一环节做不干净后面所有三维恢复全是错的。一个容易被忽略的点左右相机必须严格同步采集。如果两帧图像拍摄时间差了几毫秒板子又恰好在移动角点位置会错开一个微小量外参求解时这部分误差会全部转嫁到T上。项目里如果接的是USB双目或者两个独立相机尽量用硬件触发或者把曝光时间缩短到能冻结运动。stereoCalibrate的调用方式如下ret, _, _, _, _, R, T, E, F cv2.stereoCalibrate( obj_points_l, img_points_l, img_points_r, mtx_l, dist_l, mtx_r, dist_r, gray_l.shape[::-1], flagscv2.CALIB_FIX_INTRINSIC ) print(双目重投影误差, ret) print(旋转矩阵 R, R) print(平移向量 T, T)flagsCALIB_FIX_INTRINSIC这个标志很关键它要求stereoCalibrate在求外参时不动两个相机的内参只解R和T。如果去掉两个相机的内参会参与再优化在图像数量不够多的情况下外参结果漂移得非常厉害。常见做法是先用fix intrinsic跑一遍稳定后再决定是否放开。E和F分别是本质矩阵和基础矩阵调试时可以拿来复核极线关系应用层一般不直接碰。4.2 stereoRectify与立体校正从原始图到对齐图外参求出来后下一步做立体校正。立体匹配视差的基础是两张图中对应极线在同一条水平线上。stereoRectify根据内参和外参计算出两个映射initUndistortRectifyMap生成查表映射然后对左右图像分别做像素重映射。完成校正后左右图中同一个物理点落在同一行视差求解退化为水平方向的搜索。R1, R2, P1, P2, Q, _, _ cv2.stereoRectify( mtx_l, dist_l, mtx_r, dist_r, gray_l.shape[::-1], R, T, flagscv2.CALIB_ZERO_DISPARITY, alpha0 ) map1_l, map2_l cv2.initUndistortRectifyMap( mtx_l, dist_l, R1, P1, gray_l.shape[::-1], cv2.CV_16SC2) map1_r, map2_r cv2.initUndistortRectifyMap( mtx_r, dist_r, R2, P2, gray_r.shape[::-1], cv2.CV_16SC2) rectified_l cv2.remap(img_l, map1_l, map2_l, cv2.INTER_LINEAR) rectified_r cv2.remap(img_r, map1_r, map2_r, cv2.INTER_LINEAR)P1、P2是投影矩阵Q是重投影矩阵后面三维重构直接用它。alpha0表示只保留有效像素区域黑边最少但损失视野alpha1保留全部原始图像内容能看到明显形变。项目里常见的做法是先设alpha0确认对齐效果再按应用需求调。map生成后建议做一个直观检查点击左图某个特征点看右图同一行附近能不能找到对应点。5. 避坑五个标定常见问题我踩过也修过5.1 采集与标定板的坑坑1棋盘格打印不平整现象棋盘格打印在普通A4纸上表面有气泡、卷边标定结果的重投影误差在0.5像素以上波动换一个角度又变大变小。用同一组图反复标两次内参矩阵能差出8个像素。原因纸张柔软受潮和温度变化会让尺寸漂移格子间距不再是标称值等于世界坐标本身就有误差。更隐蔽的是卷边让纸面局部弯曲相当于板子在某一帧存在微小形变标定模型没有参数能解释这部分误差。解决用硬质塑料板或铝板做底板纸打印后压平粘牢至少静置半天再标定。条件允许就直接上亚克力定制板。贴纸用喷胶不用液体胶水后者会让纸起皱。压平后等胶水固化和纸纤维稳定别贴完立刻开拍。坑2姿态太单一现象标定完成后内参矩阵看起来正常但换到真实场景测距离坐标误差明显偏大。同一台相机两次标定的fx能差出20像素。原因板子只在相机正前方摆姿态变化不足时单应矩阵退化焦距和主点虽然能解出数字但极不稳定。平面板在正前方的几何约束太弱方程组接近奇异任何一点噪声都会放大成内参变化。解决强制板子在画面里至少变化三个角度绕X轴、绕Y轴、不同距离都要有。项目配套的采集脚本会提示当前帧的姿态分布我照着补缺失方向。每次标定后保存有效帧的rvecs统计范围如果旋转向量分量几乎压在一起说明姿态重复了回去补拍。坑3圆点阵列方向歧义现象用对称圆点标定板时偶尔发现某帧检测到的圆心阵列旋转了180度投影误差正常但外参翻车。原因对称圆点阵列在图像上有方向二义性尤其是板子几乎正对镜头时上下左右特征完全一样算法分不清。解决改用非对称圆点阵列配合CALIB_CB_ASYMMETRIC_GRID标志把方向锁死。如果必须用对称阵列就在采集脚本里排除与上一帧圆心旋转接近180度的帧用数据预处理兜底。5.2 参数与验证的坑坑4双目标定时放开内参再优化现象stereoCalibrate跑完R、T结果和单目标定后手工估算差异很大重建点云出现整体弯曲测距误差超过5%。原因去掉CALIB_FIX_INTRINSIC后两个内参和外参被联合求解。图像数量不够、采集噪声又大时方程组欠约束结果被噪声牵着走。两个相机内参本来已经有单目标定兜底再次放开等于把已知量变成未知量。解决先单目各自标定stereoCalibrate固定内参。要再优化必须先用固定内参跑一遍确认双目重投影误差小于0.5像素再考虑带约束的联合优化。这个顺序我当刚性流程执行第一次标定绝不放内参。坑5只用RMS判断好坏现象平均重投影误差0.2像素看上去很健康但实际测量误差仍然超预期点云某些区域去畸变后出现肉眼可见的弯曲。原因RMS是所有角点误差的均值个别视角的大误差被平均掩盖了。几帧角点检测错位但数量占比小平均下来照样很小。误差分布往往不均匀集中在远场或特定姿态上。解决画误差散点图看每一帧、每个角点的误差分布找出远大于均值的角落删除误差大的帧重新标定。我不看单一均值而是逐帧看误差下一章给具体检查法。6. 收尾验证逐帧重投影误差查一遍再谈结果6.1 用误差分布替代单一RMS标定完成不代表收工必须验证。我每次拿到新标定结果都会做一遍逐帧重投影误差检查# 用标定返回的 rvecs、tvecs 把三维角点重新投回像素平面 errors [] for i in range(len(obj_points)): proj, _ cv2.projectPoints( obj_points[i], rvecs[i], tvecs[i], mtx, dist ) err cv2.norm(img_points[i], proj, cv2.NORM_L2) / len(proj) errors.append(round(err, 4)) print(f第{i1:02d}帧重投影误差{err:.4f} px) bad_frames [i for i, e in enumerate(errors) if e 2 * np.mean(errors)] print(异常帧索引, bad_frames)projectPoints把世界坐标按当前内外参投影回图像平面和检测到的角点做L2范数求平均。超过整体均值两倍的帧就是重点怀疑对象可能是角点检测错位可能是图像模糊也可能是板子局部翘曲。把这些帧从采集列表里剔除重跑一遍标定对比前后RMS。误差明显下降说明问题就出在那几帧上。双目场景下验证再多一步用校正后的左右图鼠标点击左图某个棋盘角点看右图同一行能否出现匹配点。极化对齐后左右y坐标差应该小于1像素。做立体匹配时视差图出现大量空洞优先回头检查这一步。还有一件事我坚持做标定结果文件里的image_width和image_height一定写对。这两个值在验算时约束输入尺寸写错会让重投影结果整体偏移。那次我把分辨率填反内参、畸变全对但投影检查永远对不上花了整整一个晚上才找到原因。从那以后我每次标定完都强制自己过一遍投影误差检查连分辨率也逐项核对。希望帮到你。本文还有配套的精品资源点击获取
返回列表