☰
MATLAB相机内参坐标系详解:投影、反投影与OpenCV对接避坑指南
2026/10/13 21:40:04 网站建设 项目流程

你打开 MATLAB 标定完相机,导出一个cameraParams或者新的cameraIntrinsics,看着里面的IntrinsicMatrix、FocalLength、PrincipalPoint这些字段,感觉好像理解了,但真拿它去写投影代码、像素转射线、或者跟 OpenCV 的代码对接时,经常对不上。这事我刚开始做的时候也是反复折腾。相机内参坐标系说到底就是一条从三维点变成像素点的链路,而 MATLAB 在表示这条链路时,用了和教科书不完全一样的约定。这篇就围绕相机内参坐标系和它在 MATLAB 里的实际表示,把投影、反投影、常见坑位和一些能直接落地的验证方法讲清楚。

1. 内参坐标系是怎么把三维点变成像素的

1.1 从世界坐标到像素坐标,中间隔了几层

相机成像可以被拆成一条坐标变换链。一个三维点先在世界坐标系里,通过旋转矩阵和平移向量转到相机坐标系,然后用内参矩阵把相机坐标系下的点投影到图像像素平面。很多人把内参和外参混在一起说,其实两者分工非常明确:

  • 世界坐标系到相机坐标系:由外参(旋转矩阵 R、平移向量 t)决定。
  • 相机坐标系到像素坐标系:由内参矩阵 K 和畸变系数决定。

相机坐标系的原点在镜头光心,z 轴指向相机前方,x 轴朝右,y 轴朝下。这个约定在 MATLAB 和 OpenCV 里基本一致。但很多人在做机器人或者三维视觉时会习惯用右手坐标系,z 轴朝前、y 轴朝上,所以一旦看到 y 轴朝下的定义就会觉得别扭。实际图像里,像素的 v 坐标本来就是从上往下增大的,让相机坐标系的 y 轴朝下,反而跟图像的 row 方向顺过来了。

内参矩阵通常写成:

K = [fx 0 cx; 0 fy cy; 0 0 1]

它做的事情可以拆成两步。先把相机坐标系里的点除以 z 得到归一化坐标:

x_n = Xc / Zc y_n = Yc / Zc

再变成像素坐标:

u = fx * x_n + cx v = fy * y_n + cy

这里的fx、fy不是物理焦距,而是以像素为单位的等效焦距。如果相机的物理焦距是f,单个像素的物理尺寸在 x 方向是dpx、y 方向是dpy,那么:

fx = f / dpx fy = f / dpy

弄清楚这个链路,后面所有问题都清楚了。世界坐标系和相机坐标系之间是刚体变换,变换完之后,内参只负责“从相机坐标系映射到像素”。

1.2 K 矩阵每一项背后都有物理含义

很多人背 K 矩阵的把式是:左上角两个是焦距,右上角是主点。但如果追问一句fx和fy为什么不相等、主点为什么不在图像正中心,可能就答不上了。

fx和fy的差异主要来自像素不是正方形。现在的工业相机在很多分辨率下,像素宽高比接近 1,但不代表完全相等。镜头畸变、传感器装配、图像缩放处理都可能让 fx 和 fy 产生细微差别。如果做高精度测量,不能直接假设 fx 等于 fy。

cx、cy是主点的像素坐标。主点理论上是光轴穿过图像平面的位置,因为镜头和传感器的装配不可能绝对完美,主点通常在图像中心附近,但不等于图像中心。标定出来的cx、cy落在几十像素以内的偏移,是非常正常的。

还有一个容易被忽略的项是 skew 系数,也就是标准 K 矩阵里的s,位置在 fx 和 fy 之间:

K = [fx s cx; 0 fy cy; 0 0 1]

现代相机传感器基本可以认为 skew 为 0,但 MATLAB 的老版本cameraParameters里还是保留了估计 skew 的选项。如果标定时选了估计 skew,导出的IntrinsicMatrix里第二行第一列可能会有一个非零的s,这是转置后的位置,别搞错。

1.3 为什么 MATLAB 里的 IntrinsicMatrix 看起来像被“转置”了

这是新手最容易踩的坑。MATLAB 的几何变换函数大量使用行向量约定,而教科书里的投影公式基本是列向量约定。先看教科书写法:

[ u ] [Xc] [ v ] = K * [Yc] [ 1 ] [Zc]

这里三维点是列向量,K 左乘。MATLAB 的cameraIntrinsics对象里也有一个IntrinsicMatrix属性,但它存的是教科书 K 的转置:

K_matlab = K_std'

也就是:

K_matlab = [fx 0 0; s fy 0; cx cy 1]

如果你直接拿IntrinsicMatrix去乘列向量,结果当然是错的。正确做法要么先转置回来,要么按照行向量约定去写。

我见过不少人把 MATLAB 的IntrinsicMatrix原封不动搬到 Python 代码里,然后怎么算都不对,最后发现就差一个转置。这个问题看着小,但会浪费一整天。

2. 用 MATLAB 对象承载内参:字段、构造与读取

2.1 cameraIntrinsics 的字段到底在说什么

MATLAB 的 Computer Vision Toolbox 里,内参最常用的表达是cameraIntrinsics对象。它的核心字段如下:

字段含义注意点
FocalLength1x2 向量[fx fy],单位是像素不是毫米
PrincipalPoint1x2 向量[cx cy],单位是像素不是图像中心
ImageSize1x2 向量[nrows ncols]先是高,后是宽
RadialDistortion[k1 k2 k3]可能只有两个系数
TangentialDistortion[p1 p2]顺序和 OpenCV 不同
IntrinsicMatrix3x3 矩阵标准 K 的转置

注意ImageSize是[rows, cols],也就是先高后宽。很多人在做投影验证或者图像缩放时,拿width和height的顺序去取,结果全反了。这是我帮别人调代码时最常看到的问题。

FocalLength和PrincipalPoint都有明确的像素单位,而RadialDistortion跟像素单位无关,只和归一化坐标有关。这决定了你在做去畸变时,必须先算归一化坐标,再套畸变公式。

2.2 手动构造一个内参对象,验证自己的理解

理解cameraIntrinsics最好的方式就是手动构造一个。假设你已知相机的fx=800、fy=800、cx=320、cy=240,图像分辨率是 480 行、640 列,畸变系数初步设为零,可以这样构造:

focalLength = [800 800]; principalPoint = [320 240]; imageSize = [480 640]; intrinsics = cameraIntrinsics(focalLength, principalPoint, imageSize);

构造完以后,看看里面的IntrinsicMatrix:

K_matlab = intrinsics.IntrinsicMatrix

输出会是:

K_matlab = 800 0 0 0 800 0 320 240 1

如果你想得到教科书里那种标准 K,就需要转置:

K_std = intrinsics.IntrinsicMatrix'

这时输出才是:

K_std = 800 0 320 0 800 240 0 0 1

这一来一回的转置关系,直接决定了你后面写投影代码的方式。我建议在做标定和写代码之前,先这样手动构造一次,把所有字段的排列方向都验证清楚,比任何文档都直观。

2.3 从 Camera Calibrator 导出后应该读哪些字段

如果用的是 MATLAB 的Camera CalibratorApp,标定完可以导出两类对象。比较老的是cameraParameters,里面除了内参还有标定用的所有外参信息,包括每个棋盘格图像的旋转矩阵、平移向量、世界点坐标、重投影误差等。

从cameraParameters里读内参时,重点看这几个字段:

  • IntrinsicMatrix:同样是标准 K 的转置。
  • FocalLength:1x2 向量。
  • PrincipalPoint:1x2 向量。
  • RadialDistortion、TangentialDistortion:畸变系数。
  • MeanReprojectionError:平均重投影误差,单位是像素。

我一直建议把标定结果保存成.mat文件,而不是只截图记录。因为后面做三维重建、测距、去畸变时,随时需要这些对象的方法和属性。直接存对象比存散乱的数字可靠得多。

save('camera_params.mat', 'cameraParams');

下次加载:

load('camera_params.mat');

3. 像素坐标和三维点之间的往返实操

3.1 手算投影:把 K 用回教科书形式

无论是验证算法还是调试 bug,手算一次投影都是值得的。假设相机坐标系下有一个点:

Xc = [100; 50; 2000]; % 单位毫米

用标准 K 投影到像素坐标:

K_std = intrinsics.IntrinsicMatrix'; % 先转置回来 uv_h = K_std * Xc; u = uv_h(1) / uv_h(3); v = uv_h(2) / uv_h(3);

如果你不想转置,也可以用 MATLAB 的IntrinsicMatrix配合行向量:

uv_h_row = Xc' * intrinsics.IntrinsicMatrix; u = uv_h_row(1) / uv_h_row(3); v = uv_h_row(2) / uv_h_row(3);

这两种写法结果一样,但第二种符合 MATLAB 的存储习惯。关键是别混着用。列向量配IntrinsicMatrix,或者行向量配K_std,都会得到莫名其妙的结果。

实际标定得到的 K 通常还带畸变,所以上面这个投影是理想针孔模型的结果。如果要考虑畸变,得先把归一化坐标算出来,加上径向和切向畸变,再映射到像素。MATLAB 去畸变比较简单的方式是直接做图像级别处理:

undistortedImage = undistortImage(I, intrinsics);

但如果你是处理标定点而不是整张图,最好自己掌握点坐标的去畸变流程,因为undistortImage只能给你一张新图,不能直接给你某个点的新坐标。

3.2 反投影出一条空间射线

内参坐标系的另一个重要用途是反投影。给定一个像素坐标(u, v),你不能直接得到三维点,但能得到一条空间射线。这条射线的方向在相机坐标系里可以用 K 的逆矩阵计算:

pixel = [u; v; 1]; direction = K_std \ pixel; direction = direction / norm(direction);

这里的direction就是相机坐标系下从光心出发的射线方向。拿到射线之后,再结合外参,就能把射线变换到世界坐标系。

很多应用场景,比如单目测距、目标定位,本质上都是“像素坐标 + 已知平面约束”的问题。只知道内参,你只能得到一条线;要确定点落在线上哪个位置,必须再给一个约束,比如地面平面、目标高度、已知深度范围等等。不是拿内参就能直接测距。

3.3 一个利用平面约束的伪测距小例子

假设相机水平安装,离地高度是 1.2 米,相机坐标系 y 轴朝下。地面上任意一点在相机坐标系里的 y 分量就是 1.2 米。现在图像里有一个像素(u, v),我们想求它在相机坐标系下的坐标。

先算射线方向:

u = 400; v = 300; pixel = [u; v; 1]; direction = K_std \ pixel; lambda = 1.2 / direction(2); point_cam = lambda * direction;

这时point_cam(1)是横向偏移,point_cam(3)是它离相机平面的水平距离。这个例子看着简单,但非常实用。它清楚地说明了内参、射线、平面约束三者是怎么配合的。

注意这里我用的是理想无畸变模型。如果有畸变,必须先对(u, v)做去畸变处理,再代入反投影。很多单目测距代码算出来误差大,不是模型问题,而是漏了去畸变。

4. 坐标系和参数约定里的高危雷区

4.1 y 轴方向:一个很容易自我矛盾的约定

前面说过,MATLAB 的相机坐标系是 x 轴朝右、y 轴朝下、z 轴朝前。第一次听到这个约定的人总会犹豫,因为很多几何课本里 y 轴朝上。如果应用的后续环节需要转换到世界坐标系,这个方向不一致很容易导致旋转矩阵写错。

举个例子,相机水平摆放时,地面上一个点相对相机的高度差在相机坐标系里体现为 y 分量大于零;而如果按传统的 y 轴向上约定,这个点的 y 分量应该是负的。你在用pointsToWorld函数或者自己做平面交点计算时,如果搞反了正负号,测出来的距离会完全错误。

我自己的习惯是,在代码文件头部写清楚当前坐标系约定:

% Camera coordinate: x right, y down, z forward % Image coordinate: u right, v down

这样过几个月回来再看代码,也不会因为忘记约定而出错。

4.2 主点不是图像中心,但它和图像中心有关

PrincipalPoint是标定出来的主点像素坐标,不等于ImageSize的一半。比如一张 640x480 的图像,理论图像中心在(320, 240),但标定出来的cx可能是 310,cy可能是 245。这很正常。

主点不仅取决于相机本身,还会随着图像裁剪和缩放变化。如果把原图中心裁剪掉一块,新的主点坐标要减去裁剪偏移;如果把图像缩放一半,fx、fy、cx、cy全部要乘 0.5。这条规则在标定板重拍、图像预处理、模型部署时特别重要。

scale = 0.5; newFocalLength = scale * oldFocalLength; newPrincipalPoint = scale * oldPrincipalPoint;

不更新这些值,内参就跟新图像不匹配,后续所有投影和反投影结果都会偏。

4.3 像素焦距和物理焦距不要混用

很多时候资料里会给出镜头焦距 8 毫米、像素尺寸 3 微米这类数据,然后让你算内参。换算公式:

fx = 8 / 0.003

注意单位统一,8 毫米除以 0.003 毫米得到的是像素。但这种方式只能作为初值。真正准确的做法是通过标定得到fx、fy,因为镜头实际焦距和标称值有偏差,物理像素尺寸也可能不是整数微米。标定出来的值才是最接近真实系统的。

物理焦距和像素焦距转换还有一种常见坑:从 EXIF 信息读到的焦距单位是毫米,直接拿来当像素用。除非你知道准确的传感器像素尺寸和图像缩放因子,否则不要做这个假设。使用 MATLABcameraIntrinsics时,所有内参都是像素单位,这一点是最不需要怀疑的。

4.4 畸变系数的顺序,两套模型差一个元素

MATLAB 的径向畸变系数是[k1, k2, k3],切向畸变是[p1, p2]。OpenCV 的distCoeffs向量顺序是[k1, k2, p1, p2, k3]。也就是说,在 MATLAB 里同样是切向畸变,你可能写p1、p2,而到 OpenCV 里要放到第三、第四位。

如果要把 MATLAB 的内参转给 OpenCV 使用,一定要重新排列畸变系数:

k1 = intrinsics.RadialDistortion(1); k2 = intrinsics.RadialDistortion(2); k3 = 0; if numel(intrinsics.RadialDistortion) == 3 k3 = intrinsics.RadialDistortion(3); end p1 = intrinsics.TangentialDistortion(1); p2 = intrinsics.TangentialDistortion(2); distCoeffsOpenCV = [k1, k2, p1, p2, k3];

此外,两边的坐标方向约定也有细微差别。虽然大体都是 y 轴朝下,但在某些相机模型、图像坐标系定义上仍可能不一致,转出去之后最好用一组已知点做交叉验证,别只靠看代码。

5. 把 MATLAB 内参用到其他语言前,必须做的换算

5.1 转到 OpenCV 的完整例子

如果你在 MATLAB 里标定完,想在 Python/OpenCV 里做去畸变或者投影,需要做的其实只有两件事:恢复标准 K 矩阵,重新排列畸变系数。

假设已经加载了cameraParams或者intrinsics,在 MATLAB 侧先导出数据:

K_std = intrinsics.IntrinsicMatrix'; dist = [k1, k2, p1, p2, k3];

然后在 Python 侧读取:

import numpy as np K = np.array([ [800.0, 0.0, 320.0], [0.0, 800.0, 240.0], [0.0, 0.0, 1.0] ]) dist = np.array([k1, k2, p1, p2, k3])

这里最容易出错的还是 K 矩阵。很多人从 MATLAB 的IntrinsicMatrix里直接复制数字出来,忘记转置,结果在 OpenCV 里图像坐标全对不上。

我个人建议,跨语言使用前写一个很小的投影验证脚本:在 MATLAB 里选几个相机坐标系的点,投影成像素坐标;再到 OpenCV 里用同一套 K 做同样的事情,对比结果。这一步只要做一次,后面就放心了。

5.2 相机坐标系约定不统一时的处理

不同框架对相机坐标系的定义并不完全一样。除了常见视觉框架的 y 轴朝下,机器人领域经常出现 z 轴朝前、y 轴朝上的坐标系。这时如果直接把视觉里的旋转矩阵拿过去用,三维坐标会整体错乱。

处理方式是把外参也一起做一次坐标系变换,而不仅仅是转置 K 矩阵。比如机器人常用坐标系的 y 轴向上,视觉相机坐标系的 y 轴向下,两者之间相差一个绕 x 轴的 180 度旋转。具体怎么处理要看你自己的应用定义,但最稳妥的方法永远是:在标定完之后,做一个真实场景的三维验证,比如已知距离的点,反算回来是否一致。

5.3 一个用重投影误差验内参的小办法

标定完成后,最简单的验证方法是用平均重投影误差。如果MeanReprojectionError大于 0.5 像素,我会觉得标定质量不够稳。优秀的标定通常在 0.1 到 0.3 像素之间。

但这个数字只是参考。更接近实际的做法是,用标定出的内参把棋盘格角点重新投影回去,看看所有角点误差的分布。不要只看平均值,要看有没有个别角落误差特别大。如果有,通常是标定板拍糊了、图像序列覆盖不够,或者角点提取偏差。

在 MATLAB 的cameraCalibratorApp 里可以逐张查看每张图的误差。如果某张图的误差明显高于其他图,直接把这张图剔除,重新标定,效果往往马上改善。

6. 我处理内参坐标系时的一些工作习惯

6.1 保存内参时,连坐标约定一起存

我最早保存标定结果时只存一个cameraParams.mat,后来几个月后再打开,已经忘了里面矩阵到底是行向量约定还是列向量约定。现在我习惯在.mat文件里同时放一个说明变量:

cameraConvention.intrinsicMatrixIsTranspose = true; cameraConvention.cameraCoordYDown = true; cameraConvention.imageCoordOriginTopLeft = true;

别觉得多余,过段时间再看代码,这个说明能帮你省下大量查文档的时间。甚至可以进一步把标定时的图片分辨率、是否裁剪、是否缩放也一起存进去。

6.2 先用合成图像跑通投影代码

接手一个新相机、新内参、新代码框架时,我最先做的不是直接拿真实图像测,而是造一个虚拟相机:给定一个 K,造出几个三维点,投影成像素坐标,然后再用这些像素坐标去反推射线。如果最终能回到原来的三维点方向,说明整条链路没问题。

这个习惯帮我排掉了很多低级错误。比如某个函数的参数顺序是[x, y]还是[row, col],图像坐标是(u, v)还是(v, u),在合成数据上一测就露馅。真实场景里错误会被噪声、标定误差、环境因素掩盖,反而不容易定位。

6.3 标定图片的采集策略比算法参数更影响内参

最后说一个标定本身的经验。内参坐标系再怎么理解得清楚,标定数据质量差,一切都白搭。我建议采集 15 到 30 张以上图片,覆盖图像中心、四角、上下左右各个位置,同时要有明显的角度倾斜和远近变化。不要让棋盘格只出现在画面中央,那样会把内参约束得很差,尤其是主点和畸变系数容易不稳定。

拍标定板时,每张图的棋盘格不要太靠近图像边缘,但也要让边缘位置有标定板覆盖。边缘对畸变系数的估计特别重要,如果棋盘格永远只在图像中央附近移动,标出来的边缘畸变就会完全失真。

另外,一定避免运动的运动模糊。标定板边角模糊时,角点提取精度会下降,标出来的主点和焦距也会跟着飘。按照这个习惯采集图片,配合 MATLAB 标定工具,内参坐标系这部分的基础就能打得比较扎实了。

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

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

立即咨询