1. 从一张照片到一个三维点:为什么我们需要坐标系?
想象一下,你站在一个陌生的城市广场,用手机拍了一张照片。照片里,远处有一座钟楼,近处有一个喷泉。现在,我问你:“喷泉的底座,在真实世界里,距离钟楼的尖顶有多远?” 你可能会愣住,因为照片是二维的,它丢失了深度信息。你无法直接从照片上的像素位置,回答这个三维空间的距离问题。
这就是SLAM(同步定位与地图构建)技术要解决的核心问题之一。无论是扫地机器人、自动驾驶汽车,还是AR眼镜里的虚拟恐龙,它们都需要通过摄像头“看”到的二维图像,反过来理解自己所处的三维世界,并知道自己在这个世界里的位置。这个“从二维到三维,再从三维理解全局”的过程,就像一个精密的坐标转换流水线。
这个流水线的起点,就是你手机摄像头传感器上那几百万个微小的感光单元,也就是像素。终点,则是我们约定俗成的一个虚拟的、固定的三维空间框架,也就是世界坐标系。中间,需要经过几个关键的“中转站”:像素坐标系、归一化坐标系和相机坐标系。每一个坐标系都有自己的“语言”和“规则”,而将它们串联起来的,就是一系列数学变换。
我刚开始接触SLAM时,最头疼的就是这些坐标系和变换矩阵。书上公式一堆,但到底先转哪个,后转哪个,在代码里怎么体现,总是糊里糊涂。后来在项目里踩过几次坑才明白,理解这条坐标转换链,是打通SLAM任督二脉的关键。今天,我就用一个最简单的视觉里程计任务作为场景,带你手把手走一遍这个流程:如何把图像中的一个像素点,一步步“翻译”成真实世界中的一个三维坐标点。我们会用代码片段把每个步骤“钉死”,让你看完就能在自己的程序里复现。
2. 四大坐标系:从二维到三维的接力赛
理解SLAM的坐标系统,最好的方式不是死记硬背定义,而是想象一场4x100米的接力赛。数据(一个空间点)就像接力棒,被四个运动员(四个坐标系)依次传递,最终到达终点(世界坐标)。
2.1 第一棒:像素坐标系 (u, v) – 图像的“身份证号”
这是什么? 这是你最熟悉的坐标系。任何一张数字图像,本质上就是一个巨大的网格。每个网格单元就是一个像素,有它唯一的行号和列号。在OpenCV等库中,我们通常用 (u, v) 来表示一个像素的位置。u 是列索引(水平方向,x轴),v 是行索引(垂直方向,y轴),原点 (0, 0) 通常在图像的左上角。
它能做什么? 它唯一的作用就是告诉你:“特征点或者物体的角点,在照片的第几行第几列。” 它只有二维信息,没有深度,没有物理单位(就是“第几个像素”)。当我们用特征检测算法(如SIFT、ORB)找到一张图里的关键点时,得到的初始坐标就是像素坐标。
一个容易踩的坑: 很多新手会直接把像素坐标 (u, v) 当成物理距离来用,这是错误的。像素坐标没有物理意义,图像中心点的 (u, v) 值取决于图像分辨率(比如1920x1080的中心是(960, 540)),跟相机镜头没有任何关系。
2.2 第二棒:归一化坐标系 (x_n, y_n) – 剥离深度的“方向标”
这是什么? 这是从像素坐标系到物理相机坐标系的关键一跳。我们需要把“第几个像素”这个信息,转换成“相对于相机光心,这个点的方向是什么”。归一化坐标系是一个虚拟的、位于相机前方1个单位距离(通常是1米)的平面上的坐标。它用 (x_n, y_n) 表示,是一个无量纲的二维坐标。
转换怎么实现? 这里就需要引入相机的内参矩阵 K。你可以把内参矩阵理解为相机的“身份证”,它描述了相机自身的几何和光学特性,主要包括:
- 焦距
fx,fy: 把物理世界中的距离转换成像素数量的比例因子。fx = F * sx,其中F是物理焦距,sx是每个像素在x方向的物理尺寸。通常fx和fy很接近。 - 主点
cx, cy: 理论上图像的中心点,即光轴与图像平面的交点。由于制造工艺,它可能不在正中心。
内参矩阵K通常长这样:
K = [ fx, 0, cx;
0, fy, cy;
0, 0, 1 ]
那么,从像素坐标 (u, v) 到归一化坐标 (x_n, y_n) 的转换公式为:
x_n = (u - cx) / fx
y_n = (v - cy) / fy
这个操作在数学上等价于乘以内参矩阵K的逆矩阵:[x_n, y_n, 1]^T = K^{-1} * [u, v, 1]^T。
实战代码片段(C++ with OpenCV):
// 假设已知相机内参矩阵 K
cv::Mat K = (cv::Mat_<double>(3,3) << 520.9, 0, 325.1, 0, 521.0, 249.7, 0, 0, 1);
double fx = K.at<double>(0,0);
double fy = K.at<double>(1,1);
double cx = K.at<double>(0,2);
double cy = K.at<double>(1,2);
// 一个像素点,例如检测到的特征点
cv::Point2d pixel_point(300.0, 200.0);
// 转换到归一化平面坐标
double x_n = (pixel_point.x - cx) / fx;
double y_n = (pixel_point.y


1284

被折叠的 条评论
为什么被折叠?



