尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

相机标定原理与OpenCV实战:从透视投影到鱼眼畸变校正

相机标定原理与OpenCV实战:从透视投影到鱼眼畸变校正 1. 从“拍歪了”到“算准了”相机标定到底在干什么你有没有遇到过这种情况用手机或者工业相机拍一张棋盘格明明棋盘格是平的但照片里靠近边缘的线条却有点弯曲或者用相机测量一个物体的尺寸结果发现离相机中心越远测出来的尺寸误差越大。这背后就是相机镜头和传感器带来的“失真”。我们人眼和大脑能自动校正这种失真但计算机不行。想让计算机“看懂”真实世界第一步就是告诉它“你的眼睛相机有点问题这是它的‘体检报告’以后看东西得按这个报告来校正。”这个过程就是相机标定。简单来说相机标定就是给相机做一次“体检”和“配镜”。通过拍摄一组已知尺寸和形状的图案最常用的就是棋盘格我们能够计算出相机的“内参”和“外参”以及镜头的“畸变系数”。内参描述了相机本身的属性比如焦距、主点坐标它决定了三维空间点如何投影到二维图像上。外参描述了相机在世界坐标系中的位置和朝向。畸变系数则描述了镜头因为物理限制尤其是广角、鱼眼镜头导致的图像扭曲程度。标定的核心目的就是建立从真实三维世界到相机二维图像之间准确的数学映射关系从而为后续的视觉测量、三维重建、机器人导航等任务提供准确的几何基础。普通镜头和鱼眼镜头的标定虽然核心思想一致但数学模型和复杂程度天差地别。普通镜头畸变较小通常用“布朗-康拉德”模型只考虑径向和切向畸变计算相对简单。而鱼眼镜头为了获得超广视角故意引入了巨大的桶形畸变其投影模型完全不同常用的有等距投影、等立体角投影等标定过程也更复杂。如果你正在做机器人视觉、AR/VR、车载环视或者任何需要广角视野的项目搞懂这两者的区别和实现方法是绕不开的坎。接下来我就结合自己踩过的坑和项目经验把这两种标定的原理、OpenCV实现细节以及那些手册上不会写的注意事项给你掰开揉碎了讲清楚。2. 透视投影与畸变普通相机标定的数学内核在深入代码之前我们必须先理解相机成像的数学模型。这是所有标定工作的理论基础知其然更要知其所以然否则参数调起来就是盲人摸象。2.1 理想的小孔成像模型最基础的模型是小孔成像模型。它假设光线通过一个无限小的孔在成像平面上形成一个倒立的实像。在这个理想模型中世界坐标系中的一个点[X, Y, Z]投影到图像像素坐标[u, v]的公式如下s * [u, v, 1]^T K * [R|t] * [X, Y, Z, 1]^T这个公式包含了我们之前提到的所有参数s是一个任意的尺度因子。K是相机的内参矩阵这是一个3x3的矩阵。它包含了相机的核心属性K [ fx, 0, cx ] [ 0, fy, cy ] [ 0, 0, 1 ]fx,fy 分别是x轴和y轴方向的焦距单位是像素。它俩不一定相等这取决于传感器像素是否是正方形。fx F / dx其中F是物理焦距dx是每个像素在x方向的物理尺寸。cx,cy 主点坐标通常是图像的中心点width/2, height/2但实际中由于组装误差会略有偏移。它代表了光轴与成像平面的交点。[R|t]是相机的外参矩阵是一个3x4的矩阵。它由旋转矩阵R3x3和平移向量t3x1组成描述了相机坐标系相对于世界坐标系的位姿即相机摆在哪里朝向何方。[X, Y, Z, 1]^T是世界点的齐次坐标。注意这里有一个关键理解点。我们标定时使用的棋盘格其角点坐标我们设定在Z0的平面上比如棋盘格平面就是世界坐标系的XOY平面。这样公式可以简化为一个从二维平面棋盘格到二维图像的单应性矩阵变换大大简化了初始求解过程。这也是为什么棋盘格必须是一个平面的原因。2.2 现实的畸变模型小孔模型是理想的但真实的镜头由于透镜形状和组装工艺会引入畸变。主要有两类径向畸变 由透镜形状引起光线在远离透镜中心的地方比靠近中心的地方更加弯曲。表现为“桶形畸变”图像边缘向外膨胀或“枕形畸变”图像边缘向内收缩。OpenCV使用三个参数(k1, k2, k3)来建模。切向畸变 由透镜和成像平面不平行即安装不完美引起。OpenCV使用两个参数(p1, p2)来建模。校正公式如下。对于一个理想投影点(x, y)经过内参归一化位于Z1的平面上r^2 x^2 y^2 x_distorted x * (1 k1*r^2 k2*r^4 k3*r^6) 2*p1*x*y p2*(r^2 2*x^2) y_distorted y * (1 k1*r^2 k2*r^4 k3*r^6) p1*(r^2 2*y^2) 2*p2*x*y然后再将(x_distorted, y_distorted)通过内参矩阵K转换到最终的像素坐标(u, v)。为什么是k1, k2, k3和p1, p2这本质上是用多项式来拟合畸变曲线。径向畸变是关于半径r的函数用偶次多项式r^2, r^4, r^6拟合足够精确k3通常用于畸变非常大的镜头。切向畸变模型则是基于薄棱镜畸变理论推导出来的简化形式。对于大多数普通镜头k1, k2, p1, p2四个参数已经足够。2.3 张正友标定法从二维棋盘格反推三维参数我们知道了模型但如何从一堆棋盘格图片求解出K,[R|t],k1, k2, k3, p1, p2这些参数呢这就是张正友标定法的精髓。其步骤可以概括为单应性矩阵估计 对于每一张棋盘格图片利用其角点的世界坐标Z0和检测到的图像像素坐标计算一个单应性矩阵H。这个H矩阵包含了该视角下内参矩阵K和外参矩阵[R|t]的混合信息。内参约束求解 利用旋转矩阵R的正交性即R * R^T I带来的约束可以从多个不同视角的单应性矩阵H中求解出内参矩阵K。这是整个算法的核心推导。外参求解 一旦K已知对于每一张图片可以通过H和K反解出该图片对应的外参[R|t]。畸变参数初始估计 通常将畸变系数初始化为0。非线性优化 将以上步骤得到的内参、外参、畸变系数作为初始值构建一个重投影误差最小化的目标函数使用Levenberg-Marquardt等非线性优化算法对所有参数进行联合精炼。重投影误差是指将已知的世界坐标点用当前估计的相机参数投影到图像上得到计算出的像素坐标然后与检测到的真实像素坐标进行比较二者的差值平方和就是误差。优化的目标就是让这个误差最小。这个过程听起来复杂但OpenCV已经帮我们封装好了。我们的任务就是准备好高质量的输入数据并理解每个输出参数的意义。3. 手把手实现普通相机标定OpenCV代码详解与避坑指南理论是骨架代码是血肉。我们直接上代码看看如何用OpenCV的cv2.calibrateCamera函数完成标定并解释每一个关键步骤背后的意图和可能遇到的坑。3.1 环境准备与棋盘格制作首先你需要一个棋盘格。OpenCV支持不对称圆网格和棋盘格但棋盘格最常用角点检测也更稳定。import cv2 import numpy as np import glob棋盘格的参数pattern_size指的是内部角点的数量。例如一个8x10的棋盘格有8行10列黑白方格其内部角点就是7行9列。这是最容易出错的地方之一。# 定义棋盘格尺寸内部角点数行列 pattern_size (7, 9) # 例如7行9列内部角点 # 每个方格的实际物理尺寸单位任意毫米米但后续所有计算都基于此单位 square_size 25.0 # 假设每个格子边长25毫米制作棋盘格可以用OpenCV或在线工具生成一张高分辨率的图片打印出来并贴在平整的硬板如亚克力板上。平整度至关重要皱巴巴的纸会引入无法建模的误差。3.2 图像采集决定标定质量的80%标定结果的好坏绝大部分取决于你采集的图像质量。这里有几个黄金法则多视角 至少需要10-15张最好20张以上。让棋盘格出现在图像的不同位置中心、四个角落并具有不同的倾斜角度俯仰、偏航、滚转。覆盖整个视场尤其是边缘区域对于准确估计畸变系数至关重要。清晰度 图像必须对焦清晰避免运动模糊。光照均匀 避免强烈的反光、阴影或过曝。棋盘格黑白对比要分明。保持静止 拍摄时相机和棋盘格最好相对固定相机移动或棋盘格移动都可以但不要两者都动。采集完成后将图片放在一个文件夹里。3.3 角点检测与数据准备这是自动化的第一步。我们需要遍历所有图片找到棋盘格角点。# 准备对象点世界坐标系中的3D点 (X, Y, Z) # 因为棋盘格在Z0平面上所以Z始终为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 [] # 3d点在世界坐标系中 imgpoints [] # 2d点在图像平面中 images glob.glob(./calibration_images/*.jpg) # 你的图片路径 for fname in images: img cv2.imread(fname) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 查找角点 ret, corners cv2.findChessboardCorners(gray, pattern_size, None) if ret: # 如果找到添加对象点 objpoints.append(objp) # 角点亚像素精细化提高定位精度 criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) corners_refined cv2.cornerSubPix(gray, corners, (11,11), (-1,-1), criteria) imgpoints.append(corners_refined) # 可视化可选 cv2.drawChessboardCorners(img, pattern_size, corners_refined, ret) cv2.imshow(Corners Found, img) cv2.waitKey(500) else: print(f未在 {fname} 中找到角点跳过。) cv2.destroyAllWindows()关键点解析cv2.findChessboardCorners 这个函数是自动检测的关键。它可能失败原因可能是图像模糊、光照不均、棋盘格部分被遮挡、或者pattern_size设置错误。失败时ret为False。cv2.cornerSubPix强烈建议使用。初始的角点检测是像素级的这个函数通过迭代可以将角点定位精度提高到亚像素级别如0.1像素这对提升标定精度有显著帮助。参数(11,11)是搜索窗口大小(-1,-1)表示没有忽略的死区。可视化很重要它能帮你确认检测是否正确。特别是边缘的角点是否都准确找到了。3.4 执行标定与理解输出收集好objpoints和imgpoints后就可以调用核心函数了。# 获取图像尺寸用于初始化内参 img_shape gray.shape[::-1] # (width, height) # 执行标定 ret, camera_matrix, dist_coeffs, rvecs, tvecs cv2.calibrateCamera( objpoints, imgpoints, img_shape, None, None ) print(f标定是否成功: {ret}) print(f相机内参矩阵 K:\n{camera_matrix}) print(f畸变系数 [k1, k2, p1, p2, k3]:\n{dist_coeffs.ravel()})输出参数详解ret 重投影误差的RMS均方根值。这是衡量标定精度的核心指标。单位是像素。对于普通镜头这个值最好小于0.5像素0.3以内算优秀1.0以上就需要检查数据质量了。它代表了所有角点、所有图片的平均投影偏差。camera_matrix 就是我们求得的3x3内参矩阵K。dist_coeffs 畸变系数向量默认顺序是(k1, k2, p1, p2[, k3[, k4, k5, k6]])。通常我们使用5个参数(k1, k2, p1, p2, k3)。k3对于广角镜头比较重要。rvecs,tvecs 每个棋盘格图片对应的旋转向量罗德里格斯向量和平移向量。可以用cv2.Rodrigues()将旋转向量转换为3x3旋转矩阵。常见问题与排查重投影误差过大检查角点检测 是不是有些图片的角点检测不准特别是边缘角点。回顾可视化结果。检查棋盘格平整度 这是隐形的杀手。打印的纸是否拱起检查图像质量 是否有运动模糊、失焦尝试不同的flagcv2.calibrateCamera有一些可选标志比如cv2.CALIB_FIX_K3固定k3为0、cv2.CALIB_RATIONAL_MODEL使用更复杂的畸变模型包含k4, k5, k6。如果误差主要来自图像边缘尝试使用理性模型。焦距fx,fy异常 如果它们与图像的宽高不在一个数量级比如图像1920x1080焦距算出来几十万很可能是square_size单位设置错误或者棋盘格实际尺寸与输入不符。主点(cx, cy)偏离图像中心很远 这可能是正常的组装误差但如果偏离超过图像尺寸的10%也需要怀疑数据质量或镜头有严重问题。3.5 标定结果验证与图像去畸变标定完了怎么知道好不好最直观的方法就是看校正后的图像。# 读取一张测试图 img_test cv2.imread(./test_image.jpg) h, w img_test.shape[:2] # 优化新相机矩阵可以选择保留更多有效像素 new_camera_matrix, roi cv2.getOptimalNewCameraMatrix( camera_matrix, dist_coeffs, (w, h), 1, (w, h) ) # 方法1使用 cv2.undistort 去畸变 dst cv2.undistort(img_test, camera_matrix, dist_coeffs, None, new_camera_matrix) # 方法2先计算映射再重映射适用于需要多次处理同一相机视频流的场景 mapx, mapy cv2.initUndistortRectifyMap( camera_matrix, dist_coeffs, None, new_camera_matrix, (w, h), cv2.CV_32FC1 ) dst cv2.remap(img_test, mapx, mapy, cv2.INTER_LINEAR) # 裁剪ROI区域可选 x, y, w_roi, h_roi roi dst dst[y:yh_roi, x:xw_roi] cv2.imshow(Original, img_test) cv2.imshow(Undistorted, dst) cv2.waitKey(0) cv2.destroyAllWindows()cv2.getOptimalNewCameraMatrix 这个函数很实用。去畸变后图像的有效区域可能会变小像被“修剪”了边缘会出现黑边。参数alpha1表示保留所有原始像素会有黑边alpha0表示裁剪掉所有黑边区域得到一个稍小的但全有效的图像。它返回一个新的内参矩阵和感兴趣区域ROI。观察校正后的图像原本弯曲的直线如门框、窗户边缘是否变直了这是最直接的检验。4. 鱼眼相机标定当世界变成“球”鱼眼镜头追求的是视角最大化通常达到180°甚至更大其投影模型不再是透视投影而是将场景投影到一个球面上然后再映射到图像平面。因此普通镜头的畸变模型完全无法描述这种极端扭曲。4.1 鱼眼投影模型OpenCV主要支持四种鱼眼投影模型最常用的是等距投影模型。其公式为r f * θ其中r是像点到主点的距离f是焦距θ是入射光线与光轴的夹角。 对于世界坐标系中的一个点其投影到归一化球面坐标后到光轴的角度为θ然后根据上述模型计算r再结合方位角得到图像上的畸变点坐标。鱼眼标定的畸变参数通常记为(k1, k2, k3, k4)用于多项式拟合θ与r之间的关系r(θ) θ * (1 k1*θ^2 k2*θ^4 k3*θ^6 k4*θ^8)这与普通镜头的径向畸变多项式意义完全不同。4.2 OpenCV鱼眼标定实现差异鱼眼标定在OpenCV中使用cv2.fisheye子模块。其步骤与普通标定类似但API不同。# 鱼眼标定 import cv2 import numpy as np import glob # 准备对象点和图像点与普通标定相同 objp np.zeros((1, pattern_size[0]*pattern_size[1], 3), np.float32) objp[0,:,:2] np.mgrid[0:pattern_size[0], 0:pattern_size[1]].T.reshape(-1, 2) objp * square_size objpoints [] # 注意这里格式是list of arrays imgpoints [] # ... (角点检测部分与普通标定完全相同略过) ... # 鱼眼标定 K np.zeros((3, 3)) # 初始化内参矩阵 D np.zeros((4, 1)) # 初始化鱼眼畸变系数 (k1, k2, k3, k4) rvecs [np.zeros((1, 1, 3), dtypenp.float64) for i in range(len(objpoints))] tvecs [np.zeros((1, 1, 3), dtypenp.float64) for i in range(len(objpoints))] criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 100, 1e-6) flags cv2.fisheye.CALIB_RECOMPUTE_EXTRINSIC cv2.fisheye.CALIB_CHECK_COND cv2.fisheye.CALIB_FIX_SKEW ret, K, D, rvecs, tvecs cv2.fisheye.calibrate( objpoints, imgpoints, img_shape[::-1], # (width, height) K, D, rvecs, tvecs, flagsflags, criteriacriteria ) print(f鱼眼标定RMS误差: {ret}) print(f鱼眼内参矩阵 K:\n{K}) print(f鱼眼畸变系数 D (k1, k2, k3, k4):\n{D.ravel()})关键差异与注意事项数据格式objpoints和imgpoints的格式要求更严格通常是list of Nx1x3和list of Nx1x2的数组。上面代码的构建方式是一种常见写法。初始化 鱼眼标定对初始值更敏感。内参矩阵K可以初始化为一个近似值比如主点在图像中心焦距设为图像宽度的一半。畸变系数D初始化为0。标志位flagsCALIB_RECOMPUTE_EXTRINSIC 每次迭代都重新计算外参通常建议开启。CALIB_CHECK_COND 检查条件数有助于数值稳定性。CALIB_FIX_SKEW 固定切向畸变鱼眼模型中通常不考虑切向畸变因为其影响远小于径向畸变。终止条件criteria 鱼眼标定可能需要更多迭代次数如100次精度要求也可以设得更高如1e-6。4.3 鱼眼图像校正鱼眼图像的校正也与普通图像不同它需要将半球面的视图展开。# 鱼眼图像去畸变展开 img_test_fisheye cv2.imread(./fisheye_test.jpg) h, w img_test_fisheye.shape[:2] # 计算新的相机矩阵用于定义输出图像的视图 new_K cv2.fisheye.estimateNewCameraMatrixForUndistortRectify( K, D, (w, h), np.eye(3), balance0.0 # balance 控制视野大小0保留所有1裁剪最多 ) # 计算畸变映射 map1, map2 cv2.fisheye.initUndistortRectifyMap( K, D, np.eye(3), new_K, (w, h), cv2.CV_32FC1 ) # 重映射 dst_fisheye cv2.remap(img_test_fisheye, map1, map2, interpolationcv2.INTER_LINEAR) cv2.imshow(Fisheye Original, img_test_fisheye) cv2.imshow(Fisheye Undistorted, dst_fisheye) cv2.waitKey(0)balance参数是关键它控制输出图像的视野和拉伸程度。balance0会保留所有信息但边缘拉伸严重balance1会裁剪掉扭曲最严重的边缘区域得到一个看起来更“正常”但视野变小的图像。通常需要根据应用场景在0到1之间调整找到一个折中点。鱼眼校正的目标不一定是得到完全“横平竖直”的图像有时只是为了后续算法如特征匹配、SLAM能使用标准的透视模型进行处理。5. 标定实战中的高级技巧与深度避坑掌握了基本流程后一些进阶技巧和深坑能让你标定的结果更可靠、更鲁棒。5.1 标定板的选择与优化棋盘格 vs 圆网格 棋盘格角点检测速度快但容易受光照和模糊影响。不对称圆网格如OpenCV的CHARUCO板检测精度更高对部分遮挡和光照变化更鲁棒因为每个圆都有唯一的ID但需要更复杂的检测算法。在工业高精度场合圆网格是更好的选择。棋盘格尺寸与格子大小 格子大小要合适。太小角点检测容易出错太大一张图片里包含的角点信息量少。通常让棋盘格占据图像区域的1/3到1/2为宜。物理尺寸要精确测量这是所有世界坐标的基准。打印质量 使用哑光材料打印避免反光。确保打印没有缩放100%比例。可以用游标卡尺测量打印后格子的实际尺寸与代码中的square_size严格对应。5.2 标定流程的自动化与评估自动筛选图像 可以写脚本自动计算每张标定图片的重投影误差剔除误差异常大的“坏样本”。这能有效提升整体标定质量。交叉验证 不要用所有图片标定再用它们验证。应该留出几张“测试集”图片不参与标定只用它们来计算最终的重投影误差这样评估更客观。参数稳定性测试 改变参与标定的图片数量比如从15张里随机选10张多次标定观察内参和畸变系数的变化。如果波动很大说明标定结果不稳定需要更多数据或更高质量的数据。5.3 特殊场景处理远心镜头标定 远心镜头几乎没有透视畸变其模型更接近正交投影。OpenCV的标准标定方法可能不适用需要专门的正交或弱透视标定方法或者使用cv2.calibrateCamera时固定某些参数如主点位于中心切向畸变为0。双目标定 对于立体视觉需要同时标定两个相机并求出它们之间的旋转和平移关系R和T。OpenCV提供了cv2.stereoCalibrate函数。其关键在于左右相机必须同步采集同一标定板的图像并且标定板要充满共同视野。双目标定的精度要求比单目更高因为任何误差都会被基线放大。在线标定与自标定 在某些无法使用标定板的场景如已部署的系统可以利用场景中的平行线、消失点等先验信息进行标定或者使用基于运动的自标定方法如通过相机移动求解基础矩阵和本质矩阵来反推内参。这些方法精度通常低于基于标定板的方法但灵活性高。5.4 我踩过的那些“坑”“幽灵”畸变 有一次标定一个CCTV镜头重投影误差始终在1.2像素左右下不去。检查了所有环节都没问题。最后发现是相机自带的ISP图像信号处理器在输出图像前做了轻微的几何校正桶形畸变校正。这相当于我在标定一个已经被“预处理”过的图像。解决方案是获取相机的原始RAW图或关闭所有图像增强功能再进行标定。温度漂移 在户外机器人项目上早上标定的参数中午再用发现测量误差变大了。原因是相机CMOS和镜片会受温度影响导致焦距和主点发生微小变化特别是塑料镜头。对于高精度户外应用需要考虑温度补偿或定期重新标定。“主点”不主 一个工业相机标定出来主点(cx, cy)严重偏离图像中心。起初以为是标定错误后来用激光笔垂直照射镜头中心发现光斑确实落在那偏离的位置。原因是相机传感器在封装时就有偏移。所以标定结果反映的是物理现实不要盲目认为主点必须在中心。鱼眼标定的“平衡”艺术 做车载环视拼接时鱼眼校正的balance参数调得我头疼。balance0时四个方向的图像能拼接上但车身自身拉伸得像面条balance1时车身正常了但四个图像的接缝处对不齐有重影。最后是通过实验为前后左右四个相机分别找到了不同的balance值并在拼接时做羽化融合才得到可用的结果。鱼眼校正没有“完美”解只有“适用”解。相机标定是计算机视觉里一项基础但绝不平庸的工作。它连接着物理世界和数字世界其精度直接决定了上层应用的成败。理解原理、注重细节、耐心调试你得到的将不仅仅是一组参数更是对你手中“眼睛”的深刻认知。当你看到经过自己标定校正后的图像线条笔直测量精准时那种成就感就是工程师的快乐。
返回列表