简介:这份资源是面向计算机视觉学习者与工程师的相机标定项目源码包,聚焦相机内参标定与双目标定两大核心任务,支持多种相机模型与多种标定板,适合机器人视觉、自动驾驶、三维重建、视频监控等场景下的标定需求。包内共223个文件,以170张jpg标定图像、17个h头文件、15个cc实现文件为主,另含yaml配置、pdf文档、md说明及少量py脚本,压缩包约24.15MB,涵盖相机模型参数估计、外参求解、特征点检测、图像预处理、畸变校正与结果验证等模块,并涉及张正友标定法等经典实现。已有420人学习关注。源码结构清晰,便于读者对照理解针孔与畸变相机模型的差异、棋盘格等标定板的角点提取流程,以及内参与双目外参的完整求解链路,可作为课程设计、科研复现与工业标定落地的参考工具。
1. 相机标定源码包拆解:从内参到双目标定,一套代码怎么吃透
做视觉项目的人迟早会撞上同一个问题:检测框明明画得挺准,一到测距、三维重建、机械臂抓取就全乱套。十有八九不是算法不行,而是相机参数没标对。这份「相机标定-相机内参标定+双目标定」源码包,解决的就是这件事——它把单目内参标定和双目立体标定放在同一套工程里,支持多种相机模型(针孔、鱼眼等)和多种标定板(棋盘格、圆点阵、ChArUco 之类),拿到手就能对着自己的相机跑一遍。适合谁?刚接手 D435i、ROS 相机或工业相机的工程师,以及需要把标定结果喂给 OpenCV、ROS 或自研重建管线的开发者。下面我按「这是什么 → 怎么跑 → 坑在哪」的顺序,把这份源码拆开讲。
2. 相机模型与标定板选型:为什么同一套代码要留这么多分支
2.1 针孔、鱼眼、畸变模型到底差在哪
相机标定的本质,是求一个从三维世界点到二维像素点的映射。最基础的是针孔模型,它假设光线穿过一个理想小孔,投影关系是线性的,用内参矩阵 K 描述:
K = [fx 0 cx] [ 0 fy cy] [ 0 0 1]fx、fy 是焦距在像素单位下的表示,cx、cy 是主点(光轴与像平面交点)。但真实镜头有畸变,常见做法是在针孔模型后面挂一组畸变系数,径向畸变 k1、k2、k3,切向畸变 p1、p2。普通工业镜头用这个五参数模型基本够用。
鱼眼镜头视场角能到 180 度甚至更大,针孔模型直接失效,得换成等距投影、等积投影这类模型,OpenCV 里对应cv2.fisheye模块。源码包把这两类分开实现,不是炫技,是因为鱼眼用针孔参数去标,重投影误差会大得离谱,你调参调到怀疑人生也救不回来。
选型建议很直接:视场角小于 120 度,用针孔加畸变;超过 140 度,直接上鱼眼模型。别想着用针孔硬标鱼眼,那是给自己挖坑。
2.2 棋盘格、圆点阵、ChArUco 怎么挑
标定板的选择直接影响角点提取的稳定性。棋盘格是最常见的,角点由黑白方格交界处定义,OpenCV 的findChessboardCorners对它支持最好,但缺点是遮挡一半就废了,而且角点位置靠亚像素优化,光照不均时容易飘。
圆点阵标定板用圆心作为特征点,提取时对模糊和噪声更鲁棒,因为圆心是通过椭圆拟合算出来的,不是靠边缘梯度。缺点是圆点阵的角点排序在某些视角下会乱,需要额外逻辑判断。
ChArUco 是棋盘格和 ArUco 码的结合体,每个角点都有唯一 ID,部分遮挡也能标,特别适合双目或多相机系统——左右相机各拍一半,靠 ID 就能对上。代价是标定板制作麻烦一点,检测也慢一些。
源码包里这几种板子都有对应接口,我一般这么选:单目快速标定用棋盘格,双目或者需要抗遮挡用 ChArUco,工业现场光照差用圆点阵。
2.3 标定板参数怎么填才不出错
不管用哪种板子,你都得告诉代码板子的物理尺寸。棋盘格要填「内角点数量」和「方格实际边长」,注意是内角点,不是方格数。比如 9x6 的棋盘格,指的是内部 9 列 6 行角点,实际方格可能是 10x7。这个填错,标定出来的焦距会整体缩放,你拿尺子一量发现差了好几倍,就是这里的问题。
圆点阵要填圆心间距和圆半径,ChArUco 要填方格边长和 marker 边长。单位统一用毫米或米都行,但整条链路要一致,别标定用毫米、后面重建用米,那又是一场血泪。
提示:标定板打印后一定要贴在硬质平板上,纸张翘曲会直接引入系统性误差,这种误差靠增加图片数量是补不回来的。
3. 内参标定实操:从采图到重投影误差收敛
3.1 采图环节的硬性要求
内参标定的精度,七成取决于你采的图。源码包里的标定流程再完善,也救不了一组烂图。我一般要求:
- 图片数量 15 到 30 张,太少方程欠定,太多冗余且慢;
- 标定板在画面里要覆盖不同位置:中心、四角、边缘都要有;
- 姿态要有倾斜,纯正对相机的图对解畸变没帮助;
- 每张图里标定板要完整、清晰,模糊的图直接删。
有个反直觉的点:标定板不一定要占满画面。占太满反而容易在边缘产生畸变拉伸,导致角点提取偏差。我习惯让标定板占画面 1/3 到 1/2,留出边距。
3.2 角点检测与亚像素优化
采完图,下一步是提取角点。以棋盘格为例,核心调用是:
import cv2 import numpy as np # 棋盘格内角点数量,注意是内角点 pattern_size = (9, 6) # 读取图片并转灰度 img = cv2.imread('calib_01.jpg') 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) corners_refined = cv2.cornerSubPix( gray, corners, (11, 11), (-1, -1), criteria ) # 可视化,确认角点顺序没错 cv2.drawChessboardCorners(img, pattern_size, corners_refined, ret) cv2.imshow('corners', img) cv2.waitKey(0)findChessboardCorners返回的是整数像素级角点,cornerSubPix把它优化到亚像素级。(11, 11)是搜索窗口,太大容易把邻近角点带偏,太小优化不充分,11 是常用值。criteria里 30 是最大迭代次数,0.001 是精度阈值,一般不用改。
这一步做完,一定要把角点画出来肉眼过一遍。如果角点顺序乱了或者漏检,后面标定全白搭。源码包里通常会有可视化开关,别嫌麻烦,打开看。
3.3 调用 calibrateCamera 与误差判读
角点齐了,就可以解内参:
# 准备世界坐标:标定板在 Z=0 平面上,按方格边长生成 square_size = 25.0 # 毫米 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 和 imgpoints 分别是世界坐标和像素坐标的列表 ret, K, dist, rvecs, tvecs = cv2.calibrateCamera( objpoints, imgpoints, gray.shape[::-1], None, None ) print('内参矩阵:\n', K) print('畸变系数:', dist.ravel()) print('重投影误差:', ret)calibrateCamera返回的ret就是重投影误差,单位是像素。这个值直接反映标定质量:小于 0.5 像素算优秀,0.5 到 1.0 可用,超过 1.0 就得回头查图。常见原因是某几张图角点提取不准,或者标定板不平。
K是内参矩阵,dist是畸变系数。注意畸变系数的顺序,OpenCV 是 k1, k2, p1, p2, k3,别自己搞混。源码包里如果支持鱼眼,鱼眼的内参和畸变是分开的四个系数,不能和针孔混用。
注意:重投影误差低不代表标定一定对。如果所有图都是同一个角度拍的,误差可能很低,但参数外推到其他视角就崩了。采图多样性比单纯追求低误差更重要。
3.4 参数保存与跨平台格式
标定完要把结果存下来。源码包一般会提供 YAML 或 JSON 输出,方便 OpenCV、ROS 读取。我习惯存成 YAML,字段清晰:
camera_matrix: rows: 3 cols: 3 data: [fx, 0, cx, 0, fy, cy, 0, 0, 1] distortion_coefficients: rows: 1 cols: 5 data: [k1, k2, p1, p2, k3] image_width: 640 image_height: 480ROS 的camera_calibration用的就是这种格式,直接能互通。如果你后面要接 ROS 或 D435i,这一步省不得。
4. 双目标定:从单目参数到立体校正
4.1 双目标定到底多算了什么
双目标定不是把两个单目标定拼一起就完事。除了各自的内参和畸变,它还要算两个相机之间的旋转矩阵 R 和平移向量 T,也就是外参。有了 R 和 T,才能做立体校正,把左右图对齐到同一极线,让同名点只在水平方向有视差,这是双目测距和深度图生成的前提。
源码包里双目标定的入口通常是stereoCalibrate,输入是左右相机各自的内参、畸变,以及左右图里同一标定板的角点对。注意,左右图必须是同一时刻拍的同一块板子,时间不同步或者板子动了,外参就错了。
4.2 双目标定的调用与参数
# 先分别单目标定得到 K1, dist1, K2, dist2 # 再用 stereoCalibrate 联合优化 ret, K1, dist1, K2, dist2, R, T, E, F = cv2.stereoCalibrate( objpoints, # 世界坐标,左右共用 imgpoints_left, # 左图像素坐标 imgpoints_right, # 右图像素坐标 K1, dist1, K2, dist2, gray_left.shape[::-1], criteria=(cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 100, 1e-5), flags=cv2.CALIB_FIX_INTRINSIC # 固定单目内参,只优化外参 )flags是个关键参数。CALIB_FIX_INTRINSIC表示固定已经标好的单目内参,只优化 R 和 T,适合单目标定已经很准的情况。如果单目标定误差大,可以放开让stereoCalibrate一起优化,但计算量会上去,而且可能把内参带偏。我一般先用CALIB_FIX_INTRINSIC,看重投影误差,不行再放开。
返回的R和T是右相机相对左相机的旋转和平移。E是本征矩阵,F是基础矩阵,做极线校正和特征匹配时会用到。
4.3 立体校正与视差图验证
标定完,下一步是stereoRectify和initUndistortRectifyMap,把左右图校正成标准立体对:
R1, R2, P1, P2, Q, roi1, roi2 = cv2.stereoRectify( K1, dist1, K2, dist2, gray_left.shape[::-1], R, T, alpha=0 # 0 表示裁剪掉无效区域,1 表示保留全部 ) # 生成映射表 map1x, map1y = cv2.initUndistortRectifyMap( K1, dist1, R1, P1, gray_left.shape[::-1], cv2.CV_32FC1 ) map2x, map2y = cv2.initUndistortRectifyMap( K2, dist2, R2, P2, gray_right.shape[::-1], cv2.CV_32FC1 ) # 对左右图做 remap left_rect = cv2.remap(gray_left, map1x, map1y, cv2.INTER_LINEAR) right_rect = cv2.remap(gray_right, map2x, map2y, cv2.INTER_LINEAR)alpha=0会裁掉校正后出现的黑边,适合做视差;alpha=1保留全部像素,适合做全景拼接。Q是视差到深度的重投影矩阵,后面reprojectImageTo3D要用。
验证校正效果有个土办法:把左右校正图叠在一起,看同一物体是不是在同一水平线上。如果上下错位明显,说明外参有问题,回去查双目标定的角点对是不是配错了。
4.4 双目标定的常见翻车点
双目标定比单目更容易翻车,我列几个高频问题:
- 左右图角点顺序不一致:左图从左上角开始,右图从右下角开始,
stereoCalibrate会算出完全错误的外参。解决办法是统一角点排序,或者用 ChArUco 靠 ID 匹配。 - 标定板在左右图中不同时完整:一边拍到一边没拍到,角点对数量不匹配,直接报错。
- 基线太短或太长:基线太短,深度精度差;太长,左右图重叠区域小,匹配点少。一般基线取场景深度的 1/10 到 1/5。
- 忘记固定内参:如果单目标定已经很好,双目标定时放开内参,可能把 fx、fy 优化到不合理值,导致校正图拉伸。
5. 避坑与排查:标定结果不对时先查这几条
5.1 重投影误差正常但测距不准
现象:单目重投影误差 0.3 像素,双目标定误差也小,但视差图算出来的深度和实际差很多。
原因:多半是标定板尺寸填错,或者双目基线 T 的单位和场景单位不一致。比如标定板填的是毫米,但后面重建时把 T 当成米用,深度直接差 1000 倍。
解决:从头核对单位链路,标定板尺寸、T 的模长、Q 矩阵里的参数,全部统一到同一单位。用卷尺量一下实际基线,和 T 的模长对一下,差太多就是单位问题。
5.2 角点检测失败或顺序错乱
现象:findChessboardCorners返回 False,或者返回 True 但角点顺序明显不对。
原因:光照不均、标定板反光、背景干扰、棋盘格尺寸填错。顺序错乱常见于圆点阵,椭圆拟合在某些视角下会翻转。
解决:先确认 pattern_size 填的是内角点数量。然后改善光照,避免反光。圆点阵可以加CALIB_CB_ASYMMETRIC_GRID标志,或者改用 ChArUco。实在不行,手动点几个角点做初始估计,再让算法优化。
5.3 鱼眼标定用针孔模型
现象:鱼眼镜头用普通calibrateCamera标定,重投影误差看着还行,但边缘畸变完全没校正,直线还是弯的。
原因:针孔模型无法描述鱼眼的大畸变,优化时只能拟合中心区域,边缘直接放弃。
解决:换cv2.fisheye.calibrate,用鱼眼模型。注意鱼眼标定要求图片数量更多,一般 30 张以上,且标定板要覆盖整个视场,包括边缘。
5.4 双目标定后校正图上下错位
现象:stereoRectify后左右图同一物体不在同一水平线,视差匹配全乱。
原因:外参 R、T 不准,或者左右角点对配错。也可能是单目内参本身有较大误差,联合优化时把外参带偏。
解决:先单独检查左右单目标定误差,确保都在 0.5 像素以内。然后检查角点对,用可视化工具把左右角点画出来对比。如果还不行,用CALIB_USE_INTRINSIC_GUESS让stereoCalibrate重新优化内参,但要有心理准备,计算时间会明显增加。
5.5 标定结果在不同分辨率下失效
现象:在 640x480 下标的参数,换到 1280x960 直接用,结果全错。
原因:内参矩阵和分辨率绑定,fx、fy、cx、cy 都是像素单位,分辨率变了必须重新标定,或者按比例缩放。但缩放只在理想情况下成立,实际镜头畸变和分辨率不是线性关系。
解决:要么在每个分辨率下单独标定,要么标定一次后,用cv2.getOptimalNewCameraMatrix按新分辨率重新计算内参。别直接拿旧参数硬套。
6. 进阶技巧:用 ChArUco 和 ROS 把标定流程固化下来
如果你经常要标定不同相机,或者团队里多人共用一套流程,我建议把标定板换成 ChArUco,再把整个流程包成脚本。ChArUco 的好处是每个角点有唯一 ID,左右图匹配不靠顺序,靠 ID,双目标定时基本不会配错。而且部分遮挡也能标,采图时不用强求整块板子都入镜。
具体做法是先生成 ChArUco 板:
import cv2 # 定义字典和板子参数 aruco_dict = cv2.aruco.getPredefinedDictionary(cv2.aruco.DICT_6X6_250) board = cv2.aruco.CharucoBoard((7, 5), 0.04, 0.02, aruco_dict) # 7x5 是方格数,0.04 是方格边长(米),0.02 是 marker 边长 # 生成图片并保存 img = board.generateImage((1400, 1000)) cv2.imwrite('charuco_board.png', img)打印时注意保持比例,别拉伸。标定时用cv2.aruco.CharucoDetector检测角点和 ID,再传给calibrateCamera或stereoCalibrate。因为 ID 唯一,左右图匹配就是查表,省掉了排序逻辑。
ROS 用户可以直接用camera_calibration包,它支持棋盘格和圆点阵,双目标定也有stereo_calibrator。把源码包里的参数和 ROS 的 YAML 对齐,就能在 ROS 生态里直接用。D435i 这类相机出厂有标定,但如果你换了镜头或者撞过,重新标定是必须的。我一般会先用出厂参数跑一遍,再自己标一遍对比,差得多就以自己标的为准。
最后说个习惯:每次标定完,我都会拿一把卷尺量一下实际距离,和视差算出来的深度对一下。误差在 2% 以内算合格,超过 5% 就回去查。这个动作花不了一分钟,但能帮你省掉后面几小时的调试。从那以后我每次标定都强制走一遍「重投影误差 + 实测距离」双验证,再也没出现过参数看着漂亮、一用就崩的情况。希望帮到你。
本文还有配套的精品资源,点击获取