☰
相机标定实战:单目内参、双目标定与立体校正避坑指南
2026/9/25 1:24:37 网站建设 项目流程

简介:相机标定项目源码,面向具备一定视觉基础的研究者与工程师,聚焦相机内参标定与双目标定,兼容针孔、畸变、等距等多种相机模型,以及棋盘格、圆形格等多种标定板,可广泛应用于机器人视觉、自动驾驶、三维重建、视频监控等场景。压缩包共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 cy,k1 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, flags=flags ) 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()

flags=CALIB_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], flags=cv2.CALIB_FIX_INTRINSIC ) print('双目重投影误差:', ret) print('旋转矩阵 R:', R) print('平移向量 T:', T)

flags=CALIB_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, flags=cv2.CALIB_ZERO_DISPARITY, alpha=0 ) 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是重投影矩阵,后面三维重构直接用它。alpha=0表示只保留有效像素区域,黑边最少但损失视野;alpha=1保留全部原始图像内容,能看到明显形变。项目里常见的做法是先设alpha=0确认对齐效果,再按应用需求调。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'第{i+1: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一定写对。这两个值在验算时约束输入尺寸,写错会让重投影结果整体偏移。那次我把分辨率填反,内参、畸变全对,但投影检查永远对不上,花了整整一个晚上才找到原因。从那以后我每次标定完都强制自己过一遍投影误差检查,连分辨率也逐项核对。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询