☰
相机标定内参详解:从针孔模型到畸变校正的工程实践
2026/10/16 5:09:57 网站建设 项目流程

相机标定这活儿,说简单也简单,说玄也玄。我见过不少做测量、做AR、做感知的朋友,手里的相机并不便宜,拍标定板也很勤快,结果一上真实场景,三维投影出来的点和真实位置老是对不上,第一反应永远是怪算法。排查到最后才发现,问题出在相机内参那几个数字上——fx、fy、cx、cy 加上畸变系数,看着就是一堆小数,可任何一个偏差几个像素,就足以让一个三维重建项目整体偏掉十几厘米。

这篇文章把相机内参从头到尾讲透:内参矩阵里每个数字代表什么物理意义、畸变模型是怎么参与计算的、标准标定流程里哪些环节最容易翻车,以及拿到内参之后怎么验证它真的靠谱。刚入门的同学可以照着做,被标定折磨过的老手也能从中对照出自己踩过的坑。

1. 内参矩阵在算什么:从三维世界到像素的第一次投影

1.1 针孔模型其实就是相似三角形

所有相机内参的讨论,起点都是针孔模型。理解它不需要复杂的数学工具,只需要高中学的相似三角形。

假设空间里有一个点,它在相机坐标系下的坐标是 (Xc, Yc, Zc),其中 Zc 是点到光轴平面的深度。光线穿过光心打到传感器上,根据相似三角形,理想成像位置满足:

x_ideal = f * Xc / Zc y_ideal = f * Yc / Zc

这里的 f 是物理焦距,单位是毫米。但最终的图像是一个像素矩阵,所以必须回答两个问题:毫米怎么换算成像素?像素坐标系的原点定在哪?这两个问题的答案,就是内参矩阵存在的意义。

1.2 fx、fy、cx、cy 到底是谁

传感器上每个像素有固定的物理尺寸,比如常见的一英寸传感器,单个像素的边长大约是 dx 微米。把物理焦距除以像素边长,就把毫米单位换算成了像素单位:

fx = f / dx fy = f / dy

绝大多数传感器的像素是方形的,理论上 dx = dy,fx 和 fy 应该严格相等。但实际中,传感器读出电路、镜头横向色差、装配应力都可能让横竖两个方向的等效焦距出现细微差别,所以标定算法把 fx 和 fy 当成两个独立参数来估计,而不是强制画等号。

cx、cy 是主点,也就是光轴与传感器平面的交点,通常以像素坐标偏移量的形式出现。很多初学者以为主点就是图像中心,实际不是。镜头装配时传感器不可能完美对准光轴,主点在画幅内往往偏离中心几个像素到几十个像素,而且变焦时这个偏差还会移动。极端情况下,某些广角镜头的畸变模型会把主点偏移也耦合进去,标定出来的 cx、cy 和光学意义上的主点差得更远。

完整的投影关系写成矩阵形式:

[ u ] [ fx 0 cx ] [ Xc ] [ v ] = [ 0 fy cy ] [ Yc ] [ 1 ] [ 0 0 1 ] [ Zc ]

展开就是 u = fx * Xc / Zc + cx,v = fy * Yc / Zc + cy。这个 3x3 矩阵就是常说的内参矩阵 K。外参负责把世界坐标转换到相机坐标,内参负责把相机坐标投影到像素坐标,两者配合才是一个完整的相机模型。

1.3 为什么厂商参数表救不了你

有人会问:相机说明书上不是写了焦距和传感器尺寸吗,自己算一下不就行了?能算,但大概率不够准。

第一,焦距和传感器尺寸的标称值本身就有公差,工业相机的标称焦距误差通常在百分之几,消费级产品只会更夸张。第二,很多相机的视频模式和拍照模式会启用不同的传感器裁剪区域,等效焦距变了,fx 也跟着变。第三,变焦镜头在不同焦段下内参完全不同,哪怕锁定同一焦段,机械结构的细微回差也会造成主点漂移。

如果手里有镜头视场角(FOV)的实测值,倒是可以做一个粗略的交叉验证:fx ≈ W / (2 * tan(FOVx / 2)),W 是图像宽度像素数。这个公式用来估算量级没问题,但替代不了真实标定——它假设了理想针孔,没有考虑畸变和主点偏移。

所以实践中的默认做法只有一个:实际标定。用已知尺寸的标定板,在不同距离和角度下采集一组图像,反解出内参。第三节会完整展开。

2. 畸变模型:镜头比针孔复杂得多

2.1 径向畸变:桶形和枕形

如果镜头严格是针孔,投影就是线性的,但真实镜头是一组透镜,光线经过镜片时会发生折射,导致实际像点和理想像点之间产生偏移。最典型的是径向畸变——离光轴越远,畸变越明显,画面边缘的直线会变弯。广角镜头典型的桶形畸变(边缘向外鼓),长焦镜头常见的枕形畸变(边缘向内收),都属于这一类。

数学上,径向畸变用多项式来描述。先把像素坐标转换到归一化坐标 (x, y),也就是去掉 fx、fy、cx、cy 的影响,令 r² = x² + y²,然后:

x_radial = x * (1 + k1r² + k2r⁴ + k3r⁶) y_radial = y * (1 + k1r² + k2r⁴ + k3r⁶)

k1 主导大尺度的畸变形态,k2 修正边缘的二次变形,k3 是更高阶的修正项,通常只有畸变特别大的超广角镜头才需要。k1 的符号决定了桶形还是枕形:负 k1 对应桶形,正 k1 对应枕形。

2.2 切向畸变:装配误差的痕迹

第二类是切向畸变,它的成因和光学设计关系不大,主要是机械装配问题——镜头光轴与传感器平面不严格垂直,或者镜头组之间存在偏心。结果是画面出现不对称的拉伸变形,中心区域也会有细微的梯形感。

切向畸变模型有两个参数 p1、p2:

x_tangential = 2p1xy + p2(r² + 2x²) y_tangential = p1(r² + 2y²) + 2p2xy

把径向和切向加起来,就得到完整的畸变模型,也就是业界常用的 Brown-Conrady 模型:

x_distorted = x_radial + x_tangential y_distorted = y_radial + y_tangential

这一整套计算是在归一化坐标下进行的,所以畸变系数本身没有单位,只反映畸变的形态。同一台相机用不同分辨率模式去标定,fx、fy、cx、cy 会按像素数成比例变化,但畸变系数基本不变——归一化坐标已经抹掉了分辨率的影响。

2.3 OpenCV 的畸变系数排列顺序

OpenCV 的 calibrateCamera 返回的 dist 是一个一维数组,顺序必须是 [k1, k2, p1, p2, k3]。这是新手最容易搞错的地方,没有之一。很多人从别的库或者别人的项目里拷贝代码,把顺序当成 [k1, k2, k3, p1, p2],结果去畸变出来的图像完全不对——不是波浪形就是线条拧成麻花。

注意:从任何来源拷贝标定代码时,先确认 dist 向量的排列顺序。顺序错误时标定不会报错,但去畸变结果几乎必然异常。

如果畸变很大,可以给 calibrateCamera 加上 CALIB_RATIONAL_MODEL 标志,把模型扩展到 k4、k5、k6,此时 dist 的顺序变成 [k1, k2, p1, p2, k3, k4, k5, k6]。但高阶模型对图像数量和质量的要求明显更高,图像不足时很容易过拟合,我一般不建议一上来就用。

另外,鱼眼镜头不要用这套普通针孔模型去标。OpenCV 有单独的 fisheye 模块,用的是等距投影那一类的模型,普通针孔加径向畸变扛不住鱼眼的超大畸变。我见过有人拿普通模型硬标鱼眼,重投影误差压到了 0.3 像素以内,看着挺漂亮,结果一拿去畸变,边缘画面直接拧成麻花。模型选错了,误差数字再好看也没意义。

3. 标定实操:从棋盘格到可靠内参的完整流程

3.1 图像采集的要领

标定的本质是求解一个非线性优化问题,输入是"已知角点的世界坐标"和"角点在图像中的像素坐标"之间的对应关系。棋盘格之所以流行,是因为每个角点都是黑白边界的交点,可以用亚像素级精度定位,而且成本极低。

采集环节直接决定结果质量,我的经验原则是:

  • 数量:15 到 25 张比较合适,少于 10 张往往欠约束。
  • 位姿:从正对相机到倾斜 30 到 45 度都要覆盖,左右上下四个画面区域都要有,尽量让棋盘格出现在画面各个位置,特别是边缘。
  • 距离:不同拍摄距离各来几张,远的近的都要,覆盖实际使用中可能的深度范围。
  • 细节:棋盘格必须平整,别用卷边受潮的纸;避免反光、过曝和运动模糊,模糊的图像宁可删掉。

我自己的习惯是拍摄时让棋盘格占画面的四分之一到三分之一,这样既能保证角点定位精度,又方便把棋盘格移动到画面的不同区域。光圈收到中等,保证整幅画面都在景深范围内。

3.2 一套可以直接用的标定脚本

下面是我常用的 OpenCV 标定脚本,关键参数都写了注释:

import cv2 import numpy as np import glob # 内角点数量:10x7 的棋盘,内部角点是 9x6 pattern_size = (9, 6) # 每个格子的物理边长,单位是米,必须量准 square_size = 0.025 criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 1e-6) obj_points = [] # 世界坐标,尺度由 square_size 决定 img_points = [] # 图像坐标,单位是像素 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 for fname in sorted(glob.glob("calib_images/*.jpg")): img = cv2.imread(fname) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, corners = cv2.findChessboardCorners(gray, pattern_size, None) if ret: # 亚像素精化,能明显提升角点坐标精度 corners_refined = cv2.cornerSubPix( gray, corners, (11, 11), (-1, -1), criteria) obj_points.append(objp) img_points.append(corners_refined) rms, mtx, dist, rvecs, tvecs = cv2.calibrateCamera( obj_points, img_points, gray.shape[::-1], None, None) print("重投影误差 RMS:", rms) print("内参矩阵:", mtx) print("畸变系数:", dist.ravel())

这里有两个特别容易踩的坑。第一,pattern_size 填的是内角点数量,不是棋盘的行列数。一张 10x7 的棋盘,内角点是 9x6,填反了或者填成外角点数量,标定过程不会报错,但结果不可用。第二,square_size 必须填真实物理边长,这个值不影响 fx、fy 等内参本身,但直接影响外参平移量,进而影响测距、三维重建的绝对尺度。我曾经见过有人懒得量边长直接填 1,标出来的 fx、fy 看着正常,单目测距却完全对不上——问题就出在比例尺上。

3.3 重投影误差怎么看

calibrateCamera 返回的 rms 是重投影误差的均方根,单位是像素,衡量的是"用求出来的内外参把世界坐标角点投影回图像,和实际检测到的角点相差多远"。

我的经验值是这样划分的:RMS 小于 0.1 像素,说明图像质量和位姿覆盖都很棒,适合高精度测量;0.1 到 0.3 像素,对视觉定位、AR、目标检测这类任务完全够用;超过 0.5 像素,基本可以怀疑采集环节出了问题——有模糊图混进来了,或者棋盘格不平,又或者 pattern_size 填错了。

需要强调的是,RMS 只是拟合残差,拟合残差低不等于内参真的准。标定图像如果位姿覆盖不足,模型可以靠外推硬凑出一个很低的训练残差,但真实场景一测就露馅。验证的问题放到第四节单独讲。

拿到内参后,去畸变的代码长这样:

img = cv2.imread("test.jpg") h, w = img.shape[:2] new_mtx, roi = cv2.getOptimalNewCameraMatrix( mtx, dist, (w, h), alpha=1, newSize=(w, h)) undistorted = cv2.undistort(img, mtx, dist, None, new_mtx)

alpha 参数控制去畸变后保留多少像素:alpha=1 保留全部像素,图像边缘会留下黑色弧形区域,这是正常的,说明畸变真的被拉直了;alpha=0 会裁剪出最大内接矩形,画面边缘的信息会损失一部分。做测量建议用 alpha=1 再手工按 ROI 裁剪,做视觉效果用 alpha=0 更省事。

4. 验证标定质量:拿到内参后别急着进入算法

4.1 重投影误差的盲区

很多项目的流程是:标定 -> 看 RMS 小于 0.1 -> 开香槟 -> 上真机 -> 翻车。RMS 低只能说明模型很好地拟合了这批标定图像,但如果标定图像的位姿都集中在画面中间区域,画面边缘的畸变全靠外推猜出来,RMS 根本反映不出边缘的真实误差。所以验证环节必须独立于标定过程。

4.2 直线检测法

最直观也最简单的验证方法:找一栋楼的墙角、门框边线、或者一把足够直的尺子,放在画面边缘区域,拍一张图,去畸变之后检查它是否还是直线。具体做法是在去畸变后的图上取点,拟合一条直线,看最大残差。如果明显的大直线还是弯的,或者边缘出现波浪状扭曲,说明内参或畸变模型有问题。

这个测试里有一个常见现象:画面中心的直线很直,边缘的直线在去畸变后被拉过头,变成反向弯曲。这通常不是标定算错了,而是三阶径向畸变模型在硬扛一个出厂畸变就很大的镜头——模型容量不够,边缘怎么优化都拉不直。遇到这种情况,要么换更好的镜头,要么接受边缘区域精度差的事实。

4.3 交叉验证和实测检验

更严苛的做法是交叉验证:采集 20 张图,先用其中 12 张做标定,再用剩下 8 张只检测角点,用标定出的内参和这 8 张图的位姿计算独立的重投影误差。如果训练集 RMS 是 0.08,验证集却到了 0.4,基本可以断定过拟合,要么降低畸变阶数,要么补充覆盖更全面的图像重新标。

如果项目对绝对尺度有要求,尤其是单目测距、测量、重建这类方向,最后一关一定是实测:在目标距离摆一个已知长度的标记物,用像素尺寸结合内参反算物理长度,和真实值比对。这一步能发现很多 RMS 发现不了的问题,比如 square_size 填错、单位搞混、标定板实际打印尺寸和标称不一致。

5. 实战翻车记录:我从标定里踩过的坑

5.1 变焦镜头和自动对焦

第一个坑,变焦镜头。变焦镜头的内参随焦段变化,哪怕锁定同一焦段,每次变焦后主点也会轻微漂移。正确做法是标定前用胶带把变焦环固定死,整个项目期间不准再动。自动对焦同理,做测量项目必须锁死对焦——要么用定焦镜头,要么关掉 AF 手动对焦到目标距离。相机一旦在运行中自动对焦,焦距变了,你手里那套内参就是废纸一张。这个坑在巡检机器人、无人机挂载相机上特别常见,跑起来之后画面偶尔抽动一下,很多人下意识以为是算法抖动,其实是对焦在变。

5.2 传感器裁剪模式

第二个坑,传感器裁剪模式。不少相机在 4K、1080p、慢动作之间切换时,实际参与成像的传感器区域不一样,有的甚至是整幅读出后裁剪缩放。内参是按像素坐标定义的,裁剪模式一变,fx、fy、cx、cy 就全变了。如果项目里需要切换分辨率或画幅模式,别图省事,每个模式单独标定一套内参,并在代码里按当前模式加载对应的参数。

5.3 标定板的状态比你想的更重要

第三个坑,标定板本身。打印在普通纸上的棋盘格,一旦卷边、折角、受潮,角点的世界坐标就不再共面,误差直接进入优化方程。我见过有人用 A4 纸随手打一个棋盘格给工业测量项目做标定,标出来的 fx 和真实值差了快百分之二,这个误差换算到地面测距里,每十米偏二十厘米。视觉测量项目的标定板精度,必须和项目本身的精度要求匹配,别在消耗品上省事。

鱼眼镜头和超广角镜头还有一个特殊问题:边缘角点在图像里畸变得太厉害,findChessboardCorners 偶尔会跳点或者错位,导致整张图不能用。这种情况可以考虑换 ChArUco 板——它每个角点带独立编码 ID,即使部分角点被遮挡或者识别失败,剩下的角点照样能提供约束,鲁棒性比纯棋盘格好很多。

5.4 内参的"保鲜期"

最后提醒一句,内参是有保鲜期的。环境温度剧烈变化,会让相机机械结构热胀冷缩,尤其是装在金属结构上的长焦镜头,温差大时主点能移动好几个像素。要求严格的测量项目,每次开工前花两分钟,用同样的标定板找个固定距离快速拍几张,重新优化一遍畸变系数和主点,比事后再排查数据漂移省心得多。

我在实际项目里养成的习惯是:标定完先把棋盘格在画面九个位置(四角、四边中点、中心)各拍一张留作验证集,每次复标定之后,先用这张验证集跑一遍独立重投影误差。误差涨了,就先别急着上线,回去检查是镜头动了、板子弯了还是光线变了。这套流程看起来多花了几分钟,但帮我在现场躲过了不少耗时数小时的大返工。

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

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

立即咨询