☰
相机标定实践:从原理到OpenCV获取内参的完整指南
2026/10/21 13:36:47 网站建设 项目流程

做视觉方向项目的人,几乎都躲不过相机标定这道坎。我最早接触“相机标定”是在一个测距项目里,当时觉得不就是一个矩阵加几个系数嘛,真正动手才发现根本不是那么回事:角点检不出来、标定完重投影误差莫名其妙地大、换一个摄像头参数就全飘。这篇就把“相机标定实践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值。这样做的好处是,在后续任何需要校验标定结果的时候,只需要打开这张拼接图,就能直观地看到校正效果,而不用重新跑一遍完整标定流程。这套习惯帮我在多个项目里省掉了大量重复排查的时间,也让我对每台设备的标定状态一目了然。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询