news 2026/10/10 1:08:17

相机标定实战指南:四种模型与标定板选择,避开双目测距那些坑

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
相机标定实战指南:四种模型与标定板选择,避开双目测距那些坑

简介:相机内参标定与双目标定工具库,面向计算机视觉、摄影测量领域的研究者与工程师,解决多相机模型、多标定板场景下的相机校准问题。工具库支持针孔模型、Kannala-Brandt模型、MEI模型和Scaramuzza模型,适配棋盘格、圆形网格、非对称圆形网格及ArUco标定板,可满足从普通镜头到广角/鱼眼镜头的标定需求。资源共225个文件,压缩包24.14MB。内容以C++源码(.h/.cc)为核心,涵盖PinholeCamera、CataCamera、ScaramuzzaCamera、EquidistantCamera等实现及双目标定程序;同时配有yaml/yml/json配置文件、170张jpg标定图像样本,以及pdf/md/docx/txt说明文档,便于理解算法和直接上手。目前已有102人学习。通过这套工具库,读者可以直接调用多种相机模型与标定板组合,快速搭建内参标定和双目立体标定流程,省去从零编写底层算法的成本,提升视觉系统标定效率与准确性。

1. 相机内参标定和双目标定,为什么一套工具库能省你三天时间

做视觉SLAM和双目测距,拿到新相机第一件事永远是相机内参标定和双目标定。很多项目组把这一步当小事,拉一组OpenCV示例代码跑一遍,结果鱼眼镜头边缘的重投影误差一直下不来,双目基线和物理实测差出好几个毫米,排查半天发现是相机模型和标定板类型选错了。这个工具库把针孔、KannalaBrandt、MEI、Scaramuzza四种相机模型和棋盘格、圆形网格、非对称圆形网格、ArUco四种标定板统一成一套流程,换镜头只改一个枚举参数,不用再在OpenCV不同模块之间跳来跳去。适合机器人抓取、三维重建、AR设备校准的从业者,也适合新手照着复现理解标定原理。

2. 四种相机模型怎么选:针孔、KannalaBrandt、MEI、Scaramuzza的适用边界

2.1 针孔模型:常规镜头的默认起点,但广角会翻车

针孔模型描述的是光心到像素平面的直线投影,畸变用围绕理想位置的多项式修正项来补偿。OpenCV里 calibrateCamera 用的就是这套模型,畸变系数一般取[k1, k2, p1, p2, k3]五项。当镜头视场角小于60度时它非常稳,60到90度也能用,但超过90度特别是120度以上的广角镜头,画面边缘的直线会弯成明显的弧线,五项多项式拟合这种强非线性就力不从心了。具体表现是重投影误差从中心到边缘逐渐变大,明明角点检测很准,误差却怎么都压不到0.5像素以下。

判断镜头适不适合用针孔模型,有个土办法:拍一张有门框、瓷砖缝这类直线的场景,看图像边缘位置那条直线是不是明显弯曲。弯曲越剧烈,说明畸变越接近鱼眼级别,这时候硬用针孔模型标定,得到的焦距和畸变系数其实是耦合纠缠的一组数值,换一组图像重新标定,内参可能漂移好几个百分点。我一般会先用单目标定跑一遍,如果发现 dist 里 k1 的绝对值超过1,就说明模型选型不对劲,该往鱼眼模型切了。

2.2 KannalaBrandt模型:鱼眼镜头的标准答案

KannalaBrandt 模型是鱼眼镜头标定里最常用的模型,OpenCV 的 fisheye 模块就是它的实现。它的思路和针孔模型完全不同:不再修正像素平面上的坐标偏移,而是直接用多项式描述入射角到成像半径的映射关系,近似等距投影r = f * theta。这套模型对180度左右的鱼眼镜头拟合得很好,畸变系数是[k1, k2, k3, k4]四项,没有切向畸变的说法,因为鱼眼镜头的光学结构决定了切向分量可以忽略。

用这个模型标定时最容易犯的错是把标定板始终放在画面中心。鱼眼镜头真正畸变最厉害的区域在边缘,如果标定板只在中间30%的画面范围里转,内参里的 fx、fy 会有很强的相关性,标定结果就显得很玄学,同一组图跑两次误差差不多但内参差1%。正确做法是让标定板靠近画面边缘,最好让棋盘格对角线方向指向画面中心,这样每一帧都能在边缘位置提取到有效角点。我自己的经验是25张图里至少10张要有边缘角点,否则 fisheye 标定出来的 k4 基本是废的。

另一个区别是 fisheye 标定时对初始猜测更敏感,特别是 k3、k4。OpenCV 里 fisheye.calibrate 提供 flags 参数,可以固定某些系数不参与优化。我一般先用默认参数跑一遍,观察哪些系数在多次标定里方差大,就把它固定住,再重新优化一次,这样能把内参的稳定性提上来不少。

2.3 MEI模型和Scaramuzza模型:全向相机的两种解法

MEI 模型和 Scaramuzza 模型都用来处理折反射全向相机和视场角大于180度的猫眼镜头。MEI 的思路是针孔投影加镜面反射,把全景相机看成光轴方向通过一面镜面反射后进入针孔再成像,所以参数里多了一个镜面参数 xi。Scaramuzza 则更直接,用一个高阶多项式把入射光线方向映射到像素坐标,不需要显式的镜面几何。OpenCV 的 omnidir 模块里两个模型都有实现。

选型上,MEI 适合光线路径比较标准的折反射相机,Scaramuzza 更适合超广角鱼眼和猫眼镜头,因为它的多项式自由度更高。但 Scaramuzza 也有代价:多项式阶数需要试,阶数太低拟合不动,阶数太高会把噪声也拟合进去,标定结果像黑匣子。我的建议是从4阶开始往上加,每次看重投影误差的变化,误差下降小于0.02像素就回到上一阶。对于这两个模型,工具库里通常只需要切换一个 model_type 枚举,但输入的初始猜测参数不一样,MEI 的 xi 初始值给1左右,Scaramuzza 只需要给多项式系数向量长度。

Scaramuzza 模型的输出是一个高维多项式系数数组,相机几何反解时要用牛顿迭代,所以它不太适合实时SLAM里做逐个像素的去畸变,更适合离线标定后再把映射表缓存下来用。

2.4 模型选型速查表

相机模型适用镜头可用FOV畸变系数典型坑
针孔模型普通工业相机、手机摄像头90度以内k1,k2,p1,p2,k3广角镜头边缘误差压不下去
KannalaBrandt鱼眼镜头约180度k1,k2,k3,k4边缘角点不足时内参漂移
MEI折反射全向相机超过180度xi,k1,k2,p1,p2对初值敏感,容易陷局部最优
Scaramuzza猫眼/超广角镜头超过180度多项式系数阶数要试探,高了过拟合

这张表看着简单,但真正决定选型的除了镜头本身,还有你的下游需求。做双目测距时,左右目边缘畸变如果标不准,立体匹配在边缘区域就会出现系统性偏差;做AR叠加时,标定误差会被直接放大到虚拟物体的投影位置上。所以不要嫌标定麻烦,这一步半小时的投入能省后面三天的排查时间。

3. 标定板选择与生成:棋盘格、圆形网格、非对称圆形网格、ArUco

3.1 棋盘格和圆形网格:特征提取方式的本质差异

棋盘格和圆形网格的差异不在标定算法本身,而在特征检测的稳定性和精度上。棋盘格提供的是角点,可以用 cornerSubPix 做亚像素细化,理论上精度能到0.1像素以内,所以大多数单目标定场景都首选棋盘格。但棋盘格对图像质量很敏感:光照不均匀导致格子边缘发虚、运动模糊让角点糊成一片、甚至板子稍微弯折都会让检测失败或角点错位。圆形网格提供的圆心没有天然的亚像素细化手段,但圆在噪声下比角点更稳定,所以在低分辨率相机、强噪声视频帧里,圆形网格反而比棋盘格好用。

选圆形网格时要注意一个细节:透视投影下圆形会变成椭圆,椭圆中心并不等于圆的真实投影中心,存在一个系统偏差。这个偏差在镜头畸变越大时越明显,所以它的标定精度上限通常比棋盘格低,但在需要自动化的产线上,圆心检测的识别率比角点高得多,产线标定优先用圆形网格是合理的。打印时棋盘格和圆形网格都建议用激光打印机加哑光纸,喷墨打印的黑色块边缘会洇开,角点定位会整体偏上零点几毫米,标定出的焦距也跟着偏。

3.2 非对称圆形网格:双目标定里真正的主角

普通对称圆形网格的行列间距相同,当标定板旋转180度后,特征点集合和原来的位置完全重合,算法无法区分这一帧到底有没有翻转。单目标定还能靠初始猜测和外部约束蒙混过去,双目标定时左右目图像里的同名点会配错,优化出来的旋转矩阵 R 明显偏大,基线长度也不对。非对称圆形网格通过让行列间距或数量不一致来打破这种对称性,所以如果你要做的项目是双目相机标定,直接用非对称圆形网格,别在对称网格上浪费时间。

工具库里对非对称圆形网格的读法也有讲究。OpenCV 的 findCirclesGrid 要求传入 grid_size 时,行数必须填奇数,因为内部要利用非对称性判断方向。我最初不知道这个约束,行数填成偶数,检测永远返回 False。世界坐标系里非对称网格的间距要分别填 x 方向和 y 方向的值,比如5行6列的板子,水平间距25mm、垂直间距25.5mm,两个值必须不相等才叫非对称。建议打印前把间距值写进配置文件,标定完用卡尺复测一次再代入。

3.3 ArUco标定板:支持遮挡的现代选择

ArUco 标定板和棋盘格最大的区别是每个 marker 自带ID,即使只拍到整块板的一部分也能提取出一组带身份的特征点。这意味着标定过程中板子出画面一半的情况不会导致整帧报废,采集效率高很多,在双目标定里尤其有用。aruco 在线生成的网站很多,搜一下就有,生成时选择字典 DICT_5X5_100,填入 marker 边长和间距,注意单位要换算成毫米,因为标定算法需要的是物理尺寸,像素尺寸只在生成图片时有意义。

ArUco 板生成时的一个常见错误是把 marker 间距填得过小,导致相邻 marker 的黑色边界连在一起,检测器会把它识别成一个大 marker 的边框,ID识别率直线下降。一般建议 marker 间距至少等于一个 marker 的边长。打印后同样要实测边长,因为A4纸打印时如果选了适应页面,实际尺寸会比设定值小几个百分点,标定出的焦距会整体跟着缩放。用 ArUco 板标定时,检测到的角点是 marker 的四个角点,同样可以做亚像素细化,精度和棋盘格相当。

如果你的项目里标定板的打印精度很难保证,可以考虑购买玻璃基底的工业标定板。表面平整度对棋盘格角点的亚像素定位影响极大,卷曲的板子会让世界坐标假设失效,标出来的畸变系数带着整块板的弯曲,换一块平板重标定结果完全不一样。

4. 跑通一次完整标定:图像采集、命令参数与结果解读

4.1 采集标定图像:数量、角度、光照的硬性要求

不管用哪种模型哪种标定板,图像采集的质量直接决定标定结果的可靠性。单目标定至少20张,双目标定建议30对以上,多的那部分是为了让优化算法在约束充分的情况下收敛。每张图不要只是在位置上有变化,角度上也要有倾斜变化。具体做法是想象把标定板放在一个球面上移动:中心、左上、右上、左下、右下、左、右、上、下,这九个位置各拍几张,每张还带15度左右的俯仰或偏航。

光照方面,避免强光源直射板面产生反光,反光处特征点丢失是检测失败的头号原因。运动模糊也容易忽略,USB摄像头直接抓取时曝光时间不够短,板子稍微晃动就会糊。我的做法是录一段视频,然后筛选图像清晰度指标挑帧,把模糊帧全部丢掉。宁可用15张清晰的图,也不用40张里混着5张模糊的,因为重投影误差是最小二乘优化的结果,极端的错误对应点会成倍拉高误差。

4.2 单目标定:一个最小可复现的Python流程

下面是针孔模型加棋盘格的最小标定流程,其他模型切换只需改对应的检测函数和标定函数:

import cv2 import numpy as np # 棋盘格内角点数:7x7 不是格子数,是交叉点数量 pattern_size = (7, 7) square_size_mm = 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[:, :2] *= square_size_mm obj_points = [] img_points = [] for img_path in image_paths: img = cv2.imread(img_path) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, corners = cv2.findChessboardCorners(gray, pattern_size, None) if not ret: continue criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) corners_sub = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) obj_points.append(objp) img_points.append(corners_sub) ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera( obj_points, img_points, gray.shape[::-1], None, None ) print("reprojection error (px):", ret) print("camera matrix:\n", mtx) print("distortion coeffs:\n", dist)

逻辑说明:objp 构造的是世界坐标系下 Z=0 平面上的角点坐标,单位是毫米。标定算法内部会把单位当成比例因子,所以 square_size_mm 填错会导致焦距和畸变系数整体缩放,但重投影误差不变,这就是为什么重投影误差低不代表内参一定对。findChessboardCorners 检测到角点后必须用 cornerSubPix 做亚像素细化,这一步能把角点定位精度提升一个量级。calibrateCamera 返回的 ret 是重投影误差,单位为像素。

参数说明:pattern_size 是内角点数量,不是棋盘格的格子数,9x6 的格子对应 8x5 的内角点,这个反了检测永远失败。criteria 里的30是迭代次数上限,0.001是角点位置变化阈值,这两个值在大多数场景不用改。如果换用非对称圆形网格,检测函数换成 cv2.findCirclesGrid,grid_size 必须把行数设为奇数,标定函数还是一样的。鱼眼模型则要换成 cv2.fisheye.calibrate,传入的角点类型相同,只是内部投影模型变为 KannalaBrandt。

提示:重投影误差低不意味着内参正确,标定板物理尺寸错误不会被重投影误差反映出来,务必用卡尺实测后代入配置。

4.3 双目标定:外参、基线和重投影误差怎么读

双目标定一般分两步:先用上面的流程分别对左目和右目做单目标定,再把左右目的角点对应关系交给双目标定函数做联合优化。OpenCV 里对应的是 stereoCalibrate,它拿着左右目各自的内参作为初值,接着优化左右目之间的旋转矩阵和平移向量。

ret, mtx_l, dist_l, mtx_r, dist_r, R, T, E, F = cv2.stereoCalibrate( obj_points, img_points_l, img_points_r, mtx_l, dist_l, mtx_r, dist_r, gray.shape[::-1], criteria=criteria ) baseline_mm = np.linalg.norm(T) print("baseline (mm):", baseline_mm)

逻辑说明:stereoCalibrate 的输入需要单目标定得到的内参初值 mtx_l、dist_l 等,如果直接传 None 也能跑,但收敛慢且容易陷入局部最优。返回的 T 是左目坐标系下右目光心相对位置的平移向量,其范数就是基线长度,物理意义是左右目光心的距离,单位和在单目标定里传入的 square_size_mm 一致。双目相机标定里外参旋转矩阵 R 和基线 T 直接影响立体校正的效果,如果基线读数比实测偏大或偏小,先回头查标定板的物理尺寸。

参数说明:obj_points 是同一个标定板在各自图像序列里对应的世界坐标,img_points_l 和 img_points_r 是左右目分别检测到的同名角点,两边必须一一对应,否则优化出来的外参是错的。双目标定的重投影误差通常比单目略高,0.8像素以内都可以接受,超过1像素先检查同一帧里左右目的角点数量是否一致。工具库部署时的常见约定是把标定结果写成 JSON 配置文件,内参、畸变系数、R、T 都放进去,下游模块只依赖这个文件。

5. 标定避坑指南:5个真实翻车场景与排查路径

标定工具的报错信息通常很简略,OpenCV 的标定函数返回一个 ret 就结束,不会告诉你问题出在模型还是数据上。下面这五类翻车场景是我在不同项目里逐个踩过的,按出现频率排序,如果你也遇到类似的诡异现象,直接对照排查。

5.1 棋盘格检测一帧都过不了

现象是 findChessboardCorners 返回 False,整段采集的图像一张都没有角点输出。先检查 pattern_size 是不是填成了格子数而不是内角点数,10x7 的格子对应 9x6 内角点,这个错让新手的检测率直接归零。排除了这个再查光照,板面有反光时反光区域的角点对比度不足,检测器会把那个位置的响应忽略掉,把光源移开或者换哑光板就能解决。最后看分辨率,低于 640x480 的图像里远处的角点只有两三个像素宽,亚像素细化没有意义,建议拍摄距离近一点或者换更高分辨率的相机。还有一类隐蔽的情况是图像来自两个不同相机,左右目分辨率不一致,采样时 pattern_size 明明一样但总有一边失败,先统一两端分辨率再跑。

5.2 重投影误差稳定在0.6到0.8像素调不下去

这种情况最迷惑人,因为误差看起来不大,而且图像上的角点也看不出明显错位。我踩过的一次血泪经验是:拿针孔模型标一颗140度的广角镜头,中心区域误差0.3,边缘拉到0.9,平均下来就是0.6。问题不在数据而在于模型,边缘强畸变超出了五项畸变系数的表达能力。解决方法是换 KannalaBrandt 模型,同一组图像重跑一遍,误差会明显下降。换完模型后如果误差还是高,再看角点检测是不是带着错位角点进的优化,把每对点的重投影误差拉出来排序,删掉误差最大的5%再重跑一次。

5.3 双目标定出的基线和物理实测差好几毫米

基线不对的直接原因是标定板的物理尺寸和配置不一致。比如配置文件里写 square_size_mm = 25,实际用的板子打印出来是24.6mm,基线就会被放大或缩小同样的比例。注意重投影误差不会因为这个变差,所以单看误差根本发现不了,必须拿卡尺量实测值。另一个原因是左右目图像对没配好,特别是用对称圆形网格时旋转180度后特征点重合,立体匹配把左目第1个点对应到右目第30个点,外参全乱,解决方法是换成非对称圆形网格重做一遍。如果用的是棋盘格,还要确认左右目同一帧里检测到的角点数量一致,左目49个右目48个,对应关系直接错位,把不完整的帧丢掉再进优化。

5.4 ArUco板识别时好时坏

现象是大部分帧能检测到 marker,但偶尔几帧整个板子消失或者ID错乱。先看字典是否统一,生成时用的 DICT_5X5_100,检测代码里也得用同一个,不同字典之间不兼容。再看 marker 间距,间距太小时相邻 marker 的边界连成一条黑线,检测器会误判为边框,把 marker 间距调到等于一个 marker 边长,问题基本消失。还有打印缩放的坑:如果打印时勾选了适应页面,实际 marker 边长比配置小,标定出的焦距会整体偏小。在双目配置里,左右目共用同一个字典生成板子时,检测后还要保证 ID 到物理坐标的映射表一致,映射表写错会导致外参出现一个奇怪的旋转变换。

5.5 同一镜头两次标定内参差1%以上

内参不稳定通常不是算法问题,而是输入图像集不够多样。如果25张图的标定板位置都集中在画面同一侧,优化方程组的条件数会变差,内参之间互相补偿,两次采集的结果自然不一样。解决方法是按4.1里九宫格的位置分布补拍,每次标定完把每张图的重投影误差打印出来,如果某一区域的外点特别多,就针对那个区域补图。另有一个容易被忽略的点:变焦镜头标定前要锁死焦距,任何一次对焦环转动都会让内参完全变掉,工业场景里务必固定镜头后重新标定。

6. 标定结果验证与工具库部署:把内参落到SLAM和双目测距里

标定完不只是看那一行重投影误差就收工。我会先做一次视觉验证:用 undistort 把标定图像拉直,看画面边缘的直线是不是真的变直了。如果中心直、边缘还是弯的,说明畸变系数没拟合到位,回到模型选择那一步换个模型再试。接着用 stereoRectify 对双目图像做极线校正,把左右目同一角点的行差打印出来,行差小于1个像素说明外参基本可靠,这个数才是双目测距精度的真实下限。

工具库部署时我习惯把配置固化成一个 JSON 文件,里面记录相机模型类型、图像分辨率、内参矩阵、畸变系数、外参和基线,所有下游模块只读这个文件。这里最容易踩的坑是畸变系数排列顺序:针孔模型是 k1,k2,p1,p2,k3,KannalaBrandt 是 k1,k2,k3,k4,两者同样叫 k 但物理含义不同,混用会直接让图像扭曲得更厉害。我自己的习惯是每换一次镜头,在配置注释里留一行「模型 + 重投影误差 + 采集帧数 + 日期」,半年后回看能清晰地知道哪套标定是可信的。这套先验证、再固化、后复用的流程我走了很久才稳定下来,希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/10 1:07:41

PCA9422与R7FA6M4AF3CFB协同实现嵌入式电源闭环管理

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/10 1:07:02

STM32与PCA9422的PMIC电源管理方案:从硬件到软件全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/10 1:06:41

从SQLCipher到FTS5:构建微信聊天记录实时查询与增量同步管道

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/10 1:04:53

基于PCA9422与STM32L021K4的低功耗电源管理设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/10 1:04:37

安卓图书管理系统课设:SQLite+RecyclerView实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/10 1:04:21

PCA9422+PIC24FV32KA304构建主动式低功耗电源管理系统

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华