做视觉方向项目的人几乎都躲不过相机标定这道坎。我最早接触“相机标定”是在一个测距项目里当时觉得不就是一个矩阵加几个系数嘛真正动手才发现根本不是那么回事角点检不出来、标定完重投影误差莫名其妙地大、换一个摄像头参数就全飘。这篇就把“相机标定实践1-获取内参”这件事完整拆开讲——从内参到底是什么、为什么非标定不可到棋盘格怎么选、图像怎么拍再到用OpenCV完成标定的完整流程和坑点尽量让第一次动手的人也能顺利跑通也让有基础的朋友能回头补上一些细节层面的理解。这篇内容既适合准备做三维重建、单目测距、SLAM、AR叠加的初学者也适合项目里遇到“标定结果不稳定”“精度上不去”这类问题的开发者。我先说一句大实话标定的原理并不高深真正决定结果好坏的是操作习惯而操作习惯恰恰是很多教程不会细讲的部分。1. 内参到底解决了什么问题1.1 内参在视觉管线中的位置相机本质上是一个投影设备三维世界里的点经过镜头被投射到二维图像传感器上。这个“三维到二维”的映射关系是后续一切视觉计算的基础。你去做单目测距本质上是利用几何关系反推距离你去做SLAM需要从图像特征反推相机位姿你去做三维重建更是完全依赖准确的投影几何。如果投影关系本身就错了后面所有计算都会失真。这个投影关系由两部分组成一是相机自身的内部属性即内参二是相机在某个坐标系下的位置和朝向即外参。内参描述的是“相机镜头和传感器本身如何去投影”包括等效焦距、主点坐标、畸变系数。它不随相机移动而变化只要你不动镜头、不调焦距它就固定在那里。外参描述的是“这架相机现在摆在哪里、朝哪个方向看”它是随位姿变化的。举一个生活化的例子内参相当于一个人的视力参数外参相当于这个人站在哪里、脸朝哪边。你问“这个人看到的苹果在图像哪个位置”两个信息缺一不可。如果视力参数错了就算你准确知道他的站位和朝向也无法预测苹果出现在图像上的正确位置。所以标定的第一步就是先把“视力参数”搞清楚这也是本篇的核心任务——获取内参。1.2 标定结果的评判标准说完内参是什么紧接着要解决另一个问题标定完怎么知道结果是好是坏很多初学者跑完calibrateCamera拿到一组数据看到重投影误差很小就以为万事大吉实际上这是不够的。评判标定结果最常用的指标是重投影误差RMS简单说就是把标定板上已知的角点坐标通过你标定出来的内外参重新投影到图像平面上计算它和实际检测到的角点坐标之间的距离。这个误差通常用像素为单位一般低于0.1像素就算不错低于0.3像素也还能接受超过0.5像素就要检查是否存在严重问题。这个指标只能告诉你“整体拟合程度”不能直接暴露是哪张图出了问题。我在实践中还有一个习惯不要只看整体的RMS要单独查看每一张图像的误差分布。如果大多数图像误差都在0.1以内但某一张达到了0.5甚至更高这一张往往就是问题图要么是标定板发生了弯曲要么是光照不均匀导致角点检测偏差要么是拍摄时轻微运动产生了模糊。把这样的图剔除或重拍整体结果会立刻改善很多。另外还可以做一个更直观的验证用标定好的内参对图像做畸变校正把校正前后的图像叠在一起观察直线是否变直、边缘是否被拉正。很多表面上RMS很低的结果实际校正效果并不理想就是因为某些区域误差被平均掉了。所以我的建议是RMS只作为参考最终的畸变校正效果才是真正的验收标准。2. 标定板与图像采集的准备2.1 棋盘格标定板怎么选标定板是整个标定流程里的“几何基准”。你声称自己知道标定板上每个角点的三维坐标这个“知道”的误差会直接传导到标定结果中。所以标定板选不好后面做得再认真也是白费。目前用得最普遍的是棋盘格标定板。选棋盘格时最先看的是角点数量比如8x6的意思是有8列、6行内角点。这里有个经验内角点数尽量选“奇偶搭配”也就是一个边长是奇数、另一个是偶数。这样做的好处是棋盘图案在图像中的朝向具有唯一性计算时不容易出现180度翻转的歧义。我常用的规格是7x10、8x6和9x7根据目标场景来定。格子大小怎么选主要看你的应用场景和拍摄距离。格子太小相机距离稍远时角点检测会失败格子太大标定板本身的平面度难以保证。举个例子如果摄像头固定在桌面高度、拍摄范围是40厘米左右的区域单格尺寸20到30毫米比较合适如果做室外远距离场景单格尺寸可能要放大到50毫米以上。一个简单的判断标准是在你要用的工作距离下棋盘格占画面面积不少于30%每个格子至少要有十几个像素的成像宽度。制作方式上我推荐两种。第一种是打印后贴在硬质平板上关键是底板要平推荐用铝塑板或亚克力板不要用普通纸直接贴墙上墙面不平整会直接导致角点位置偏移。第二种是用高分辨率屏幕显示棋盘格这种方式对屏幕分辨率要求高而且屏幕玻璃的反光会干扰角点检测我自己用得较少更适合临时验证。无论哪种方式打印或显示后要用直尺或游标卡尺实际测量几个格子的尺寸确认与理论值一致打印缩放导致的比例误差是新手最容易忽略的问题。2.2 图像采集的规范标定图像采集的好坏直接决定了你能拿到的内参质量。这里说的“好”不是画质多精美而是视角覆盖是否充分、姿态是否多样、成像是否清晰。我常用的一个说法是拍标定图像就是在“让相机从不同角度认识一个已知尺寸的物体”。采集数量上15到20张是底线如果需要高精度建议拍到25到30张。数量本身不是目的覆盖度才是目的。所谓覆盖度包括了标定板在画面中的位置要覆盖中心和四个角落标定板的姿态要同时有左右旋转、上下俯仰和平面内旋转标定板的距离要从近到远分段变化。这样做的原因从原理上也很好理解标定本质上是从多张图像中估计相机参数如果所有图像里标定板都只出现在画面中央且姿态几乎一样那不同图像提供的约束高度重复未知参数解算就不稳定容易出现多组参数都能拟合但真实精度很差的情况。拍摄时还有几个硬性要求。一是光照要均匀避免标定板表面出现强烈反光尤其是黑白格交界处的高光会干扰角点定位。二是标定板要保持静止后再拍照手持拍摄很容易产生轻微运动模糊平面标定对特征点定位精度要求很高哪怕零点几个像素的偏差都会进入误差。三是镜头如果有变焦或对焦功能拍摄过程中必须固定不要中途调整焦距因为内参是假设焦距不变的。四是相机参数设置建议用手动模式固定曝光和白平衡避免自动增益导致不同图像的亮度差异过大。2.3 采集时容易踩的坑第一坑标定板弯曲。这是最隐蔽也最致命的问题。把棋盘格贴在不平的表面上或者在拍摄过程中手捏着标定板的一角导致微弯都会让角点的实际空间位置偏离“共面且规则”的假设。解决办法是使用足够坚硬的底板拍摄时尽量让标定板自由靠在一个稳定的支撑物上不要用力捏。第二坑反光。棋盘格的黑白格表面如果用了光面纸或覆膜很容易在某个角度下形成大面积高光反射。检测算法在强反光区域会发生误检导致角点坐标偏差。建议选择哑光纸打印拍摄时打光角度不要太正稍微侧一点。第三坑视角重复。很多人贪图方便站在原地只转动标定板的角度拍结果拍出来的图像里标定板在画面中的位置几乎都集中在中心区域。这样的数据集对角点位置覆盖是不充分的。正确做法是每拍一张都主动改变标定板在画面中的位置和朝向确保内参的估计在各个区域都有足够的约束。第四坑图像数量不足或者无效帧混入。我在项目中遇到过一位开发同学采集了30张图结果跑代码时发现一半图像角点检测失败最后只剩十几张有效图。他拍的时候偷懒没有当场检查检测结果直到标定阶段才发现采集数据不够用。所以我的习惯是每拍一组就直接跑一遍角点检测当场确认哪些图有效、哪些图需要重拍这比回家以后再补拍省事得多。3. 核心原理与参数计算3.1 针孔模型与内参矩阵理解内参绕不开针孔相机模型。这个模型其实很简单把相机想象成一个不透光的盒子盒子前面有一个小孔外面的光线穿过小孔后在盒子内部的平面上投出一个倒像。真实相机的镜头替代了这个小孔但基本关系是一致的。三维世界点与图像坐标之间的关系可以拆解为几步世界坐标先通过外参变换到相机坐标相机坐标再通过透视投影落到图像平面最后通过像素坐标系的缩放和平移得到像素位置。内参就出现在最后这两步里具体到公式就是把相机坐标下的点投影为像素坐标[ \begin{bmatrix} u \ v \ 1 \end{bmatrix} \begin{bmatrix} f_x 0 c_x \ 0 f_y c_y \ 0 0 1 \end{bmatrix} \begin{bmatrix} X_c / Z_c \ Y_c / Z_c \ 1 \end{bmatrix} ]中间那个3x3矩阵就是内参矩阵K。f_x和f_y是等效焦距单位是像素它等于物理焦距除以单个像素的尺寸。c_x和c_y是主点坐标也就是光轴与图像平面的交点。理论上主点应该位于图像中心但实际生产中传感器安装存在偏差所以需要通过标定来确定真实值。这里有一个很容易理解偏差的点f_x和f_y虽然都是焦距但它们不一定相等。原因是像素传感器在x方向和y方向上的物理尺寸可能不同或者存在轻微的装配误差。所以标定结果是分别给出两个值不要把它们强行设成相等。3.2 畸变模型镜头毕竟是物理器件不可能完美符合针孔模型。它引入了畸变这也是为什么标定要同时求解畸变系数。畸变主要分两种径向畸变和切向畸变。径向畸变来自镜头的曲面形状表现为图像中的直线变成向外弯曲桶形畸变或向内弯曲枕形畸变。这种畸变在画面边缘最明显越靠近中心越弱。OpenCV中径向畸变通常用k1、k2、k3三个系数描述一般场景下k1和k2就够用只有像鱼眼镜头这类大畸变镜头才需要引入k3。切向畸变则是镜头与传感器平面不平行造成的表现为图像在某些方向上被拉伸用p1、p2两个系数描述。畸变模型的作用可以类比成“修正镜头的坏习惯”先让镜头把画面拍歪拍弯再通过畸变系数把图像“还原”到理想针孔模型下的样子。讲到这里我要强调一个原则标定求出来的畸变系数其符号和数值大小是有物理意义的不要随意做截断。比如有人看到k3很小就干脆设成0这在大部分场景问题不大但在高精度重建时会造成边缘区域的细小误差。更重要的是一旦畸变系数参与计算整个计算链路要统一使用同一组内参和畸变系数不要混用不同次标定的结果。3.3 平面标定法为什么流行现在主流的标定方法可以追溯到张正友提出的平面标定法OpenCV里calibrateCamera的实现基本遵循这个思路。它的核心想法是不需要昂贵的三维标定块只需要一块平面棋盘格并在不同位姿下拍几组照片就能解出内参和外参。为什么平面板也能标定出三维几何关系原因是每一张棋盘格图像都提供了一个从三维平面到图像平面的单应性映射。单应性矩阵中包含内参和该位姿下的外参信息。当我们有多张不同位姿的图像时就相当于有了多个方程通过联立求解可以把内参从外参中分离出来。随着图像数量和姿态多样性的增加方程组约束越来越充分解的稳定性也随之提高。这里也解释了为什么标定图像需要充分覆盖画面和姿态变化不同姿态的图像提供了不同的单应性约束如果姿态只集中在很小的角度范围内约束条件高度相关矩阵求解容易出现数值不稳定。这也是为什么我前面花了大量篇幅讲采集规范因为这些操作习惯直接影响的就是数学层面的可解性。4. 实操用OpenCV完成内参标定4.1 环境准备与代码框架我习惯使用Python加OpenCV的环境来做这件事。版本上OpenCV 4.x是当前的主流选择配合numpy使用。需要说明的是OpenCV有两个角点检测接口老接口findChessboardCorners对新版部分畸变较大的图像检不出角点我会优先建议使用findChessboardCornersSB它是基于更鲁棒的算法实现的对光照不均和边缘模糊的图像表现更好。整体流程是读取图像、检测棋盘格角点、对检测到的角点做亚像素细化、收集所有图像的三维点和二维点对、调用calibrateCamera求解内参和畸变系数、输出结果并分析误差。4.2 角点检测与亚像素细化角点检测是标定流程中最容易失败的一步。检测函数需要你指定棋盘格的内角点尺寸比如我的棋盘格是7列10行内角点代码里就传(7, 10)。注意这个参数是(列数, 行数)的顺序搞反了会直接检测失败。检测到整数像素坐标后我还会调用cornerSubPix对角点位置做亚像素级精化。这一步很多人会跳过但它对提高标定精度很有帮助。亚像素细化本质是在角点附近的小窗口内利用灰度梯度信息对角点位置做更精细的估计把定位精度从像素级提升到亚像素级。毕竟重投影误差本身就在像素这个量级上角点定位偏差越小最后解算出来的内参越可信。亚像素细化的参数也值得说一下。搜索窗口大小我一般取(11, 11)或(15, 15)太小容易丢失梯度信息太大又可能跨到别的纹理区域。终止条件我设为迭代200次或误差小于0.001实际使用中这个组合效果很稳。4.3 标定计算完整代码下面给出一段我自己在用的标定脚本略作精简核心逻辑完整import cv2 import numpy as np import glob # 棋盘格内角点尺寸列数, 行数 pattern_size (7, 10) # 生成标定板上所有内角点的三维坐标单位毫米 # 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 obj_points [] # 三维点 img_points [] # 二维点 img_files sorted(glob.glob(./calib_imgs/*.jpg)) for fname in img_files: img cv2.imread(fname) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 使用更鲁棒的SB接口检测角点 found, corners cv2.findChessboardCornersSB(gray, pattern_size) if not found: print(角点检测失败:, fname) continue # 亚像素细化 criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 200, 0.001) corners_refined cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) obj_points.append(objp) img_points.append(corners_refined) print(有效图像数量:, len(obj_points)) # 标定求解 rms, camera_matrix, dist_coeffs, rvecs, tvecs cv2.calibrateCamera( obj_points, img_points, gray.shape[::-1], None, None ) print(重投影RMS:, rms) print(内参矩阵:\n, camera_matrix) print(畸变系数:\n, dist_coeffs) # 每张图像单独计算重投影误差 for i in range(len(obj_points)): img_points_proj, _ cv2.projectPoints( obj_points[i], rvecs[i], tvecs[i], camera_matrix, dist_coeffs ) error cv2.norm(img_points[i], img_points_proj, cv2.NORM_L2) / len(img_points[i]) print(f图像 {i}: 平均重投影误差 {error:.4f} px)代码本身不长但每一个环节都有作用。obj_points存的是标定板上角点的三维坐标这是已知量img_points存的是你在图像里检出来的二维点这是观测量calibrateCamera就是根据这些对应关系去反求相机内参。最后输出的rms和每张图的误差是我们判断标定质量的核心依据。4.4 结果分析与结果验证拿到输出后第一件事看rms。如果rms在0.1左右说明整体标定质量不错如果在0.3以上就要按我在1.2节说的方法逐张检查每张图的误差定位问题图。畸变系数的输出格式是[k1, k2, p1, p2, k3]总共5个值。这里要特别提醒OpenCV不同版本的输出顺序和含义是一样的但有人会把dist_coeffs当作一个矩阵来理解实际上它是一个向量每个元素对应不同的畸变参数。后续做畸变校正时直接把整个向量传给cv2.undistort或cv2.initUndistortRectifyMap即可不需要手动拆分。验证畸变校正效果我常用这样一段代码img cv2.imread(./calib_imgs/sample.jpg) h, w img.shape[:2] new_camera_matrix, roi cv2.getOptimalNewCameraMatrix( camera_matrix, dist_coeffs, (w, h), 1, (w, h) ) mapx, mapy cv2.initUndistortRectifyMap( camera_matrix, dist_coeffs, None, new_camera_matrix, (w, h), cv2.CV_32FC1 ) undistorted cv2.remap(img, mapx, mapy, cv2.INTER_LINEAR)校正后的图像重点关注画面边缘的直线是否变直。如果边缘仍然有明显的弯曲说明畸变系数没标准通常需要补充更多大角度、靠近边缘的图像重新标定。5. 常见问题与排查技巧5.1 角点检测失败的典型原因我用表格整理一下最容易遇到的几种情况方便对照排查。现象可能原因处理手段所有图像都检不出角点pattern_size传错、棋盘格角点太少核对内角点尺寸确保列行顺序正确某些图像检不出角点标定板部分离开画面、强反光、运动模糊重拍保证完整角点可见且成像清晰检测到的角点顺序混乱棋盘格图案对称性强导致方向歧义使用奇偶搭配的内角点数避免对称图案标定结果RMS很高标定板弯曲、图像中有无效帧逐张看误差剔除问题图像后重标这里多说一句如果棋盘格的角点数量太少例如4x4这种检测很容易失败因为图案信息量不足。建议内角点数至少取6x8以上数据越充分检测和后续求解都越稳。5.2 重投影误差到底怎么用重投影误差是最常被误解的指标。它本质上是“拟合误差”也就是标定模型对这批图像的拟合程度但并不直接等价于“实际测距精度”。我见过有人拿着RMS为0.05的结果去做单目测距预期误差几个毫米结果实际误差几十毫米然后回头怀疑标定流程是不是出了问题。问题的本质在于RMS衡量的是标定板和图像特征点之间的一致性而当你在真实场景里做测距时被测物体与标定板不是同一个平面这个误差还会受到物体纹理、光照、镜头分辨率等多种因素影响。所以RMS是用来指导标定过程本身的不是用来打包票最终精度的。正常范围是采集规范的情况下RMS低于0.1属于优秀0.1到0.2属于良好超过0.3就要回头查数据质量。5.3 标定结果的有效性验证我对标定结果有一个固定的验收流程分三步走。第一步是重投影误差分析即1.2节和4.4节的方法。第二步是畸变校正可视化用标定得到的内参对图像做校正检查边缘直线度和图像整体形状是否正常。第三步是实际场景验证把标定结果投入到你要做的具体任务里比如测距就放几个已知距离的物体去量重建就拍一个已知尺寸的盒子去重建直接对比输出结果。第三步才是真正有说服力的验收因为标定是为任务服务的任务精度达标才说明标定真正满足需求。我还要特别提醒关于内参矩阵的一个细节相机在出厂时硬件装配一致的情况下同一型号的相机之间的内参差异并不大但这不代表你可以从网上直接抄一份同型号相机的内参来用。每台相机的实际焦距、主点位置和畸变都可能有细微差别尤其在需要做高精度的任务时必须逐台相机标定。批量项目里如果要标定很多台设备我的做法是每台设备单独标定并保存独立的标定文件宁可多花点时间也不要去图省事共用一份参数。5.4 标定文件的管理习惯标定完的内参不要只存在Python变量里建议保存成文件。我通常保存为.npz或.json格式包含内参矩阵、畸变系数、标定时间、图像分辨率、标定板规格和RMS值。这些元信息在后面调试时会非常有用。举个例子如果某天你发现某个功能模块的精度突然变差回溯到标定文件时看到一个明显过高的RMS值就能立刻判断是不是标定环节拖了后腿。保存标定文件时我会把与内参配套使用的分辨率一起记录因为内参矩阵本身是依赖分辨率的。如果后续代码里对图像做了缩放内参也需要做对应的缩放比如图像宽度缩为原来的一半fx、fy、cx、cy也应该近似减半。这个细节处理不好轻则校正效果变差重则整个几何计算全乱。实际项目中我就见过有人把1080p下标定的内参用在了720p的图片上结果测距偏差特别大排查了很久才定位到这个问题。最后再分享一个小技巧标定完成后我会顺手用标定结果对一张典型的现场图像做一次畸变校正把校正前后的图像横向拼接保存下来命名里带上RMS值。这样做的好处是在后续任何需要校验标定结果的时候只需要打开这张拼接图就能直观地看到校正效果而不用重新跑一遍完整标定流程。这套习惯帮我在多个项目里省掉了大量重复排查的时间也让我对每台设备的标定状态一目了然。
阅读完成 · 觉得有帮助?