恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
单目相机标定全攻略:从针孔模型到鱼眼畸变校正
首页
资讯中心
/
单目相机标定全攻略:从针孔模型到鱼眼畸变校正
单目相机标定全攻略:从针孔模型到鱼眼畸变校正
发布时间:2026/10/19 16:44:57
1. 单目标定到底在解什么难题1.1 从三维空间到二维图像相机做了什么摄像头成像的本质是把三维世界里一个点的坐标通过透镜投影到二维传感器平面上。这个过程中信息丢了一个维度但我们希望在数学上建立一个尽可能精确的模型知道一个像素坐标能反推它在三维空间中对应的射线反过来知道三维坐标也能算出它投影到图像里的准确位置。整个投影过程可以拆成几段来理解。第一步是对应关系三维点先通过外参旋转矩阵和平移向量从世界坐标系变换到相机坐标系第二步是投影相机坐标系下的点经透镜成像到归一化成像平面第三步是采样映射从成像平面坐标变成最终像素坐标这一步涉及传感器像元尺寸、主点位置等因素。单目标定要做的就是把中间所有未知量估计出来相机的焦距、主点位置、畸变系数以及每张标定图像对应的外参。这里有个特别容易忽视的点内参矩阵里面的fx、fy和主点坐标虽然叫内参但并不是纯物理属性。同一颗镜头搭配不同分辨率的传感器或者图像经过裁剪缩放内参数值都会变。我之前见过有人直接把别人标定好的内参拿过来用结果投影位置偏得离谱原因就是图像分辨率对不上内参矩阵单位是像素分辨率一变数值就要重新算。所以任何标定结果都必须和具体分辨率绑定记录这一点后面实操部分还会单独强调。1.2 普通镜头与鱼眼镜头的模型分水岭普通镜头视角大概六十到九十度的成像行为非常接近针孔模型。针孔模型的本质是假设光线沿直线穿过一个小孔投射到平面上物和像按相似三角形的比例缩小。真实镜头因为透镜结构会产生畸变但畸变量相对较小用几个多项式系数就能修正回来。OpenCV里最经典的是径向畸变加切向畸变的组合k1、k2、k3描述径向拉伸或挤压p1、p2描述透镜组装配时与传感器平面不平行带来的斜向错位。鱼眼镜头则是完全不同的设计思路。它要做到一百六十度甚至超过两百度的视场角靠的不是简单放大而是刻意让入射光在透镜内部发生强烈弯曲把大角度的光线强行压缩到传感器上。这时候再套针孔模型就不成立了因为投影关系已经不是相似三角形而是接近等距投影、等立体角投影这类非线性映射。如果把普通镜头的畸变模型硬套在鱼眼上参数收敛会非常差畸变系数被拟合得巨大边缘区域校正后依然一片混乱。所以标定之前一定要先判断手里的镜头属于哪一类。普通镜头是五个畸变系数加针孔模型鱼眼镜头是四个畸变系数加专门的鱼眼投影模型两者的数学框架完全不同。这也是为什么不少人拿普通标定代码去跑鱼眼镜头调了一整天重投影误差始终压不下来根本原因是模型选错了。2. 标定板、相机与采集姿势的选择2.1 标定板怎么选、怎么做标定板的选择决定标定结果的上限。我使用最多的是棋盘格原因在于OpenCV对棋盘格的支持最完善角点检测稳定性好而且棋盘格的交点是黑白格边界的交叉位置亚像素定位精度高。棋盘格有两个核心参数内角点数和格子边长。内角点数是黑白格交叉点的数量比如一张物理上是7行9列方格的棋盘内角点数是6行8列代码里传入的尺寸是(6, 8)。内角点数宁大勿小因为角点越多每张图能提取的特征约束越多标定优化时的方程组越稳定。我常用的规格是内角点7x9或9x11格子边长二十到四十毫米。格子边长不能随便定它决定了工作距离和成像比例的关系如果格子太小低分辨率摄像头拍出来边缘模糊角点检测精度会明显下降。标定板必须高度平整。有人为了省事直接把棋盘格贴在纸板上纸板稍微受潮弯曲标定出来的焦距就会系统性偏大重投影误差看着不大但后续做三维测量时误差非常明显。我自己是把棋盘格贴在亚克力板或者玻璃板上贴的时候用胶带把四角拉平表面不留气泡。打印环节也别省精度差的打印机打出来的格子边缘带锯齿角点检测精度会打折扣。如果条件允许也可以用圆点阵列标定板圆点标定板对非中心对称畸变的鲁棒性好一些但OpenCV对它的检测函数没有棋盘格那么方便用于普通镜头时优势不算大。我的建议是除非有特殊需要先用棋盘格稳。2.2 图片采集的数量、角度与位置要求很多人第一次标定随手拍三五张就开始跑结果输出误差一堆还说OpenCV不够好。其实问题不在工具而在约束不够。标定本质上是光学参数的拟合约束信息全部来自标定板在不同姿态下的影像。如果姿态太单一比如板子总是正面朝向镜头、放在画面中心每个角点在图像上的分布高度相似方程组就退化焦距和主点之间的耦合无法分离。我的经验是普通镜头至少拍十五到二十张鱼眼镜头建议拍到二十五到三十张。拍摄时姿态要拉开左右倾斜、上下倾斜、绕相机光轴旋转都要有远处近处交替板子放在画面中央和边缘都拍。尤其是鱼眼镜头边缘畸变非常大如果标定板只在中心区域活动边缘畸变信息缺失标定出的畸变系数并不可靠。采集时图像质量是第一位的。标定板不能过曝白色格子一旦过曝成一片白板角点检测很容易失效。反光也要避免反光会造成局部亮度突变角点位置被带偏。这里分享一个自检技巧采集完一组图像后先跑一遍角点检测把检测失败的图单独挑出来重新拍。检测失败的表现通常是找不到角点或者角点排列顺序错乱这种图混进标定流程会影响最终结果。2.3 如何判断一组图像是否合格一个实用的判断标准是标定板在所有图像里的投影面积应该覆盖整个图像空间。可以想象把图像分成九宫格如果标定板在左上、右上、左下、右下这些区域里从来没出现过那对应位置的畸变参数就缺乏约束。采集完成后我会快速浏览所有图片确认四个角落区域里标定板至少都出现过一次。姿态变化幅度也需要检查。一张合格的标定图标定板平面和镜头光轴之间的夹角最好在三十度到九十度的大范围里分布。夹角很小时板子基本正面朝向镜头对估计焦距几乎没有区分度。从数学角度看多个姿态的关键是让角点在图像上产生足够大的非线性位移这样才能把畸变参数和射影关系解耦。如果你发现标定出的畸变系数符号和之前版本相反或者多次标定结果符号不稳定很大概率是姿态多样性不够。3. 普通镜头标定从角点检测到参数求解3.1 基于OpenCV的标定流程代码普通镜头的标定流程已经高度标准化核心库是OpenCV。下面这段代码是我在多个项目里稳定使用的流程可以直接参考。import cv2 import numpy as np import glob CHESSBOARD_SIZE (7, 9) SQUARE_SIZE_MM 30.0 objp np.zeros((CHESSBOARD_SIZE[0] * CHESSBOARD_SIZE[1], 3), np.float32) objp[:, :2] np.mgrid[0:CHESSBOARD_SIZE[0], 0:CHESSBOARD_SIZE[1]].T.reshape(-1, 2) objp * SQUARE_SIZE_MM obj_points [] img_points [] 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, CHESSBOARD_SIZE, None) if not ret: print(f角点检测失败: {fname}) continue corners_sub cv2.cornerSubPix( gray, corners, (11, 11), (-1, -1), criteria(cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) ) obj_points.append(objp) img_points.append(corners_sub) rms, mtx, dist, rvecs, tvecs cv2.calibrateCamera( obj_points, img_points, gray.shape[::-1], None, None ) print(f重投影误差: {rms:.4f}) print(f内参矩阵:\n{mtx}) print(f畸变系数: {dist.ravel()})这里有个步骤很多人会忽略就是cornerSubPix亚像素细化。findChessboardCorners返回的角点精度已经够用但还停留在像素级而标定是高度依赖角点精度的优化过程像素级误差会在反投影时被放大。cornerSubPix通过局部梯度拟合把角点坐标细化到亚像素精度这一步能显著降低最终的重投影误差。我实测过跳过这一步时误差可能在0.3像素左右做了之后能压到0.1甚至更低。关键是保存角点的时候一定要跟obj_points一一对应这里说两个很常见的错误。第一findChessboardCorners检测成功的图才往列表里添加失败的要跳过去第二obj_points里的三维点坐标要和检测到的corner顺序完全一致如果混入一个没有对应关系的点后面整个优化都会错乱。3.2 内参矩阵和畸变系数怎么看calibrateCamera返回四个关键结果重投影误差rms、内参矩阵mtx、畸变系数dist、每张图的外参rvecs和tvecs。内参矩阵是一个3x3矩阵结构一目了然。fx和fy是归一化焦距单位是像素它们与真实物理焦距的关系是fx等于焦距除以像元宽度。同一颗镜头传感器分辨率越高fx就越大这是正常现象。cx和cy是主点也就是光轴与传感器平面的交点理论上应该在图像中心但实际装配误差会让它偏移几十个像素这属于正常范围。畸变系数dist在普通标定里一般包含五个数k1、k2、p1、p2、k3。注意k3存放在向量的最后它表示六阶径向畸变的量。这些系数本身的大小没有绝对的正常值不同镜头差异很大但要关注它们的稳定性。同一个镜头重复标定几次如果畸变系数出现正负号翻转或数量级剧烈跳动说明标定数据有问题。重投影误差是衡量整体精度的核心指标。它的含义是把所有检测到的角点用标定出的内外参重新投影回图像平面然后计算投影点和原始检测点之间的平均像素距离。通常认为误差小于0.1像素是优秀小于0.3像素可用超过0.5像素就需要回头检查采集过程。误差不是越低越传奇但如果某次结果从0.15突然跳到0.8几乎可以肯定是混入了不合格的图像。3.3 畸变校正的两种实现方式拿到内参和畸变系数后最常见的目标是把原始图像校正成畸变较小的理想图像。OpenCV提供两条路线。第一种最简单直接调用cv2.undistort输入原始图像、内参矩阵和畸变系数得到校正图。优点是代码量少缺点是每次调用都要重新计算整幅图像的映射坐标如果放在实时视频流处理里白白消耗CPU。第二种性能更好先用cv2.initUndistortRectifyMap计算出一张映射表之后每一帧只用cv2.remap重映射。h, w gray.shape[:2] newcameramtx, roi cv2.getOptimalNewCameraMatrix(mtx, dist, (w, h), alpha, (w, h)) mapx, mapy cv2.initUndistortRectifyMap(mtx, dist, None, newcameramtx, (w, h), cv2.CV_32FC1) undistorted cv2.remap(img, mapx, mapy, cv2.INTER_LINEAR)这里alpha参数值得多说几句。alpha取0时校正后会裁掉所有黑色冗余区域视野变小边缘信息被丢弃alpha取1时尽量保留原始像素但四周会出现黑边。实际项目如果对视野有要求我通常用alpha等于0.8到1.0然后自己裁剪一块有效区域。如果alpha直接设成0校正后的视角变化可能很大给后续特征匹配带来额外的尺度不一致问题。4. 鱼眼/广角镜头标定换个模型再玩一次4.1 鱼眼镜头为什么不能用普通模型鱼眼镜头的光学设计目标是用极大的视角覆盖场景它的投影关系与针孔模型之间不是简单加个畸变项那么简单。鱼眼投影函数本身就不同于针孔模型常见的鱼眼投影包括等距投影、等立体角投影、体视投影和正交投影。一百多度的入射角在等距投影下像高r与入射角θ满足r等于f乘以θ而在针孔模型下满足r等于f乘以tan(θ)。当θ很大的时候tan(θ)会迅速趋向无穷普通镜头为了维持近似线性关系只能把视角限制在几十度。鱼眼用等距这种近似线性的关系在数学上天然允许大角度入射。如果拿针孔模型的畸变多项式去逼近鱼眼的曲线需要非常高阶的多项式才勉强拟合而高次多项式带来的数值不稳定性又非常大。所以正确做法是采用专门的鱼眼相机模型来建模。OpenCV的fisheye模块使用的模型基于Kannala-Brandt模型它的畸变参数最多四个分别是k1、k2、k3、k4在实现上通过多项式来逼近某种投影函数配合内参矩阵一起求解。对市面上绝大多数鱼眼镜头来说这个模型足够应付了。fisheye模块的API和普通标定模块很接近但它是独立的一套函数。最容易踩的坑是fisheye.calibrate返回的畸变系数D向量长度为4而普通calibrateCamera返回的是5个。如果两个搞混后续调用fisheye相关函数时会报长度不匹配的错误。4.2 fisheye模块的实操流程import cv2 import numpy as np import glob CHESSBOARD_SIZE (7, 9) SQUARE_SIZE_MM 20.0 objp np.zeros((1, CHESSBOARD_SIZE[0] * CHESSBOARD_SIZE[1], 3), np.float32) objp[0, :, :2] np.mgrid[0:CHESSBOARD_SIZE[0], 0:CHESSBOARD_SIZE[1]].T.reshape(-1, 2) objp * SQUARE_SIZE_MM obj_points [] img_points [] images glob.glob(fisheye_calib/*.jpg) for fname in images: img cv2.imread(fname) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, corners cv2.findChessboardCorners(gray, CHESSBOARD_SIZE, None) if not ret: print(f角点检测失败: {fname}) continue corners_sub cv2.cornerSubPix( gray, corners, (11, 11), (-1, -1), criteria(cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) ) img_points.append(corners_sub.reshape(1, -1, 2)) obj_points.append(objp) K np.zeros((3, 3)) D np.zeros((4, 1)) rvecs [np.zeros((1, 1, 3), dtypenp.float64) for _ in range(len(obj_points))] tvecs [np.zeros((1, 1, 3), dtypenp.float64) for _ in range(len(obj_points))] rms, K, D, rvecs, tvecs cv2.fisheye.calibrate( obj_points, img_points, gray.shape[::-1], K, D, rvecs, tvecs, flagscv2.fisheye.CALIB_RECOMPUTE_EXTRINSIC cv2.fisheye.CALIB_FIX_SKEW ) print(f鱼眼重投影误差: {rms:.4f}) print(f鱼眼内参矩阵:\n{K}) print(f鱼眼畸变系数: {D.ravel()})注意鱼眼版的obj_points和img_points维度是(1, N, 3)和(1, N, 2)和普通版本不一样这是新手最容易写错的地方。另一个重点参数是CALIB_FIX_SKEW它的作用是固定fx与fy的比值强制像素为方形。对鱼眼镜头来说制造上通常保证像元是方形的使用这个flag可以显著减少优化参数数量让畸变求解更稳定。除非项目对x和y方向焦距有独立测量需求我建议加上这个flag。鱼眼镜头采集图像时还有一个额外要求标定板要尽量靠近画面边缘因为鱼眼的畸变集中在边缘区域如果只拍中心区域边缘畸变系数完全无法约束。这跟普通镜头标定不太一样普通镜头即使板子大部分在中间边缘畸变影响也没那么显著鱼眼镜头则是中心区域几乎看不出畸变只有让板子出现在图像边缘才能把畸变参数约束住。4.3 鱼眼标定结果的校正差异鱼眼镜头的畸变校正效果和普通镜头完全不同。普通镜头校正后是标准的透视投影直线变直视角维持原样。鱼眼校正后通常有两种选择。一种是输出一个窄视角的透视投影把视角缩小到可接受范围比如90度此时画面中心区域被放大边缘弯曲消失视觉上像一个普通镜头。另一种是保持大视角用柱面投影或立体视图方式呈现但那已经超出单目标定的范畴了。实现鱼眼校正的方式和普通镜头类似同样用initUndistortRectifyMap加remap只是换成fisheye模块的函数。h, w gray.shape[:2] newK cv2.fisheye.estimateNewCameraMatrixForUndistortRectify(K, D, (w, h), np.eye(3), None, balance0.8) mapx, mapy cv2.fisheye.initUndistortRectifyMap(K, D, np.eye(3), newK, (w, h), cv2.CV_32FC1) undistorted cv2.remap(img, mapx, mapy, cv2.INTER_LINEAR)balance参数的含义和普通镜头里的alpha类似balance越大校正后保留的原始视场越多黑边越明显balance接近0时裁切越激进视场变小中心区域放大更多。如果后续做目标检测我通常把balance控制在0.6到0.9既保留足够的环境信息又让画面边缘不再严重扭曲检测框的稳定性会明显提升。5. 标定过程中的常见问题与排查实录5.1 重投影误差居高不下的排查思路重投影误差大是我遇到最多的问题。误差偏大时的系统排查顺序如下。第一步检查是否有图像模糊。标定板轻微失焦时人眼可能觉得还能看但角点亚像素检测已经不准了。手持标定板晃动时特别容易拍出局部模糊尤其是棋盘格边缘。这种情况用三脚架固定相机或者提高快门速度能明显改善。第二步检查是否有反光。标定板表面局部反光会让角点检测把反射造成的亮度梯度当作参考角点位置偏移几个像素。发现这个问题调整光源位置或者换一个拍摄角度重新拍。第三步检查标定板是否平整。纸板受潮变形、玻璃板边缘固定不牢导致轻微弯曲都会让标定结果失真。翻过板子看看变形了就换一块。第四步检查姿态覆盖度。如果重投影误差集中体现在某几张图上逐张查看那几张的板子位置和角度很可能姿态太过单一。把姿态分散开后重新跑一遍误差通常会明显下降。5.2 内参数值异常的情况有一次标定结果主点偏移到了画面宽度五分之一的位置明显不合理。排查后发现问题在于混入了一张标定板只露出一半的图像。这张图里角点检测成功了但标定板信息严重残缺导致整体优化被带偏。剔除这张图重新标定主点立刻恢复正常。另一个常见异常是fx和fy相差超过百分之十。在没有使用CALIB_FIX_SKEW的情况下这有时候是真实存在的比如传感器像元不是正方形。但如果同时畸变系数非常大且不稳定更可能是模型不匹配。比如把鱼眼镜头用普通模型标定fx和fy经常会有巨大差异多次标定数值波动也大。这时候应该换模型重新标定。5.3 常见问题速查表症状可能原因处理方案重投影误差大于0.5图像模糊、反光、板不平整重拍合格图像剔除问题图某张图角点检测失败标定板过曝或超出视场调整曝光减少超出部分主点cx、cy明显偏移混入不完整标定板图像剔除该图重新标定多次标定内参波动大图像数量不足或姿态单一增加图片分散姿态鱼眼镜头普通模型误差大模型不匹配改用fisheye模块重新标定校正后黑边太多balance或alpha设置过大降低参数值重新裁剪ROI这张速查表覆盖了大多数标定坑。每个项目可能还会遇到边界情况但排查思路是一致的先检查数据质量再检查模型选择最后检查参数设置。6. 标定完成后如何验证结果6.1 用直线变直的程度做直观验证标定的最终目的是让校正后图像符合理想投影关系。最简单直观的验证方法是找矩形物体比如门框、瓷砖缝、屏幕边框拍一张原始图再用标定参数做校正观察图像中物体的直线边缘是否变直。这个验证尤其适合鱼眼镜头如果校正后的门框还是明显弯曲说明畸变参数没拟合到位。有个经验之谈是即使是普通镜头校正后边缘区域直线也可能因为插值产生轻微锯齿这是正常的不影响精度。但如果在校正后图像边缘的直线仍有肉眼可见的弧线就要回到第5节的排查流程重新检查。6.2 用三维反投影验证几何精度如果项目目标是测距、定位这类几何任务建议额外做一次精度验证。在现实场地里放置几个已知间距的点用校正后图像或者直接用内参矩阵解算像素位置的归一化射线方向再比较射线方向与真实方向的夹角误差。夹角误差能直观反映标定参数对空间几何关系的拟合质量。具体做法是在地面或标定板上粘贴多个已知位置标记点拍一张图检测标记点像素坐标利用内参矩阵求每个点的归一化射线方向。由于单目相机缺少深度信息只能得到射线方向但可以通过射线之间的夹角或者射线与已知平面的交点来验证。这个方法在平面测距、机器人定位场景里非常实用。6.3 标定参数的存档与复现标定参数是一次性得出的但项目迭代时经常需要重新标定。我的习惯是每次标定结束保存一份带元数据的参数文件记录图像数量、重投影误差、标定板规格、flags设置以及镜头型号和分辨率。{ camera_model: fisheye, resolution: [1920, 1080], calib_images_count: 28, rms_error: 0.08, fx: 512.3, fy: 512.3, cx: 960.5, cy: 540.2, dist: [-0.012, 0.003, -0.001, 0.0005] }有了这份档案换相机或者改分辨率后能很快判断哪些参数可以复用哪些需要重新标定。我之前有个项目因为画面裁剪导致分辨率变化以为内参可以简单等比缩放结果发现畸变系数的行为并不按比例变化最后重新标定才解决。这个教训让我学到一个原则分辨率一旦变化最稳妥的方案永远是重新标定。做单目摄像头标定这件事最磨人的其实不是算法本身而是对数据质量的把控。普通镜头和鱼眼镜头虽然模型不同但底层逻辑是一样的用足够多样、足够清晰的标定板图像把一个精确的几何模型拟合出来。只要模型选对、图片拍够、误差控制在合理范围后续做畸变校正、三维重建、目标定位都会轻松很多。我个人的体会是遇到内参不稳或者误差偏大的情况先别急着调代码回去重新审视标定板的物理状态和采集过程问题往往藏在最不起眼的细节里。最后再分享一个经验如果鱼眼镜头和普通镜头在同一个项目里混用尽量使用同一块维护良好的标定板保持相机分辨率一致这会省掉大量适配的麻烦。