4. 点云生成与坐标变换:相机内参标定、深度图转点云、坐标系变换(相机系到世界系)、点云下采样

各位同学,欢迎来到深度相机部署实战的第四讲。今天我们要聊的,是3D视觉里最基础也最绕不开的一环——点云生成与坐标变换

说实话,我刚开始做嵌入式视觉那会儿,觉得点云不就是把深度图的像素值换算成三维坐标嘛,有什么难的?结果第一次在ARM板子上跑,帧率直接掉到个位数,点云还全是扭曲的。嗯,这里面的坑,我今天一个一个帮你们填上。

4.1 相机内参标定:你的相机到底长什么样?

要生成点云,首先得知道相机内部的光学参数。说白了,就是搞清楚像素坐标和物理坐标之间怎么换算

相机内参矩阵K长这样:

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

其中:

  • fx, fy:焦距(以像素为单位)
  • cx, cy:光心偏移(主点坐标)

我建议你直接用OpenCV的棋盘格标定法。但注意,嵌入式平台的摄像头往往有畸变,尤其是鱼眼镜头。我曾经在RK3588上调试一个双目模组,标定完发现点云边缘全是弯曲的——后来才发现是径向畸变参数没加进去。

核心经验:标定板不要只用一张图。至少拍15-20张不同角度、不同距离的棋盘格照片。我习惯在标定前先检查一下重投影误差,超过0.5像素就重新拍。

4.2 深度图转点云:从像素到三维坐标

有了内参,深度图转点云就是个纯数学活了。公式很简单:

Z = depth(u, v) / scale
X = (u - cx) * Z / fx
Y = (v - cy) * Z / fy

其中(u, v)是像素坐标,depth(u, v)是深度值,scale是深度单位转换因子(比如毫米转米)。

你想想看,这个公式本质上就是把像素射线延长到深度距离。但实际部署时,有几个坑要注意:

  • 无效深度值处理:很多深度相机在边缘或反光区域会输出0或65535。我建议直接过滤掉这些点,别让它们污染点云。
  • 数据类型选择:嵌入式平台上,用float32算点云坐标比较耗内存。我习惯用uint16存储深度,计算时再转float。但注意精度损失。
  • SIMD加速:如果你在ARM上跑,可以用NEON指令集一次处理4个像素。我在树莓派上试过,速度能快3倍。
小技巧:深度图转点云时,别逐像素循环。用numpy的向量化操作或者OpenCV的reprojectImageTo3D函数,效率高得多。但嵌入式平台没有numpy?那就手写C++循环,配合编译器优化。

4.3 坐标系变换:从相机系到世界系

点云生成后,默认是在相机坐标系下。但实际应用中,我们需要把点云变换到世界坐标系(比如机器人基座坐标系)。

变换公式:

P_world = R * P_camera + T

其中R是旋转矩阵,T是平移向量。这俩参数通常通过手眼标定外参标定得到。

我遇到过最头疼的情况是:相机装在机械臂末端,每次机械臂动,外参就得跟着变。这时候你需要实时获取机械臂的位姿,然后动态更新R和T。

注意:旋转矩阵的表示方式有很多种(欧拉角、四元数、旋转矩阵)。我建议统一用四元数,避免万向锁。而且四元数插值平滑,适合做轨迹规划。

举个例子,假设你有一个深度相机固定在AGV小车上,相机坐标系到小车坐标系的变换矩阵是T_cam_to_robot。那么:

// 伪代码
Eigen::Matrix4f T_cam_to_robot;
T_cam_to_robot << 0, -1, 0, 0.1,
                    1,  0, 0, 0.2,
                    0,  0, 1, 0.5,
                    0,  0, 0, 1;

for (auto& point : pointcloud) {
    Eigen::Vector4f p_cam(point.x, point.y, point.z, 1.0);
    Eigen::Vector4f p_robot = T_cam_to_robot * p_cam;
    // 现在p_robot就是小车坐标系下的点
}

4.4 点云下采样:别让数据淹死你

一个VGA分辨率(640x480)的深度图,转成点云就是30万个点。在嵌入式平台上,这数据量太大了。我建议做下采样

常用的方法:

  • 体素滤波(Voxel Grid):把空间划分成小立方体,每个立方体只保留一个点(取重心或中心)。这是最常用的方法。
  • 随机采样:简单粗暴,但可能丢失关键特征。
  • 均匀采样:按像素网格均匀取点,适合结构光相机。

我个人习惯用体素滤波,体素大小设为0.01米(10毫米)。这样既能保留物体轮廓,又能把点云数量降到1-2万个点,处理起来轻松很多。

避坑指南:我曾经在NVIDIA Jetson上做体素滤波,直接用PCL库的VoxelGrid,结果发现内存占用飙升。后来改用自己写的哈希表实现,内存降了40%。嵌入式平台,能自己写的就别用大库。

4.5 本章知识体系

下面这张图,是我自己总结的点云生成与坐标变换全流程。你可以把它当作一个检查清单:

点云生成与坐标变换全流程 1. 相机内参标定 获取fx, fy, cx, cy 2. 深度图获取 ToF / 双目 / 结构光 3. 深度图转点云 像素坐标 → 三维坐标 4. 坐标系变换 相机系 → 世界系 5. 点云下采样 体素滤波 / 随机采样 6. 输出点云 用于定位 / 建图 / 识别 ⚠️ 常见问题与优化建议 • 标定:至少15张棋盘格,重投影误差 < 0.5像素 • 深度图转点云:过滤无效深度值,使用SIMD加速 • 坐标系变换:统一用四元数,避免万向锁 • 下采样:体素大小0.01m,自己实现哈希表降内存

4.6 实战建议

最后,给你几个实战中的小建议:

  • 先离线标定,再在线部署:内参和外参最好在PC上标定好,固化到配置文件中。嵌入式平台只做加载和计算。
  • 点云数据量要控制:我建议根据应用场景动态调整下采样参数。比如建图时用密一点的点云,定位时用稀疏的。
  • 多线程处理:深度图转点云和坐标系变换可以放在一个线程,下采样和后续处理放在另一个线程。流水线作业,效率更高。
我的习惯:每次部署新平台,我都会先跑一个简单的点云生成demo,看看帧率和内存。如果发现瓶颈,优先优化深度图转点云这一步——它通常是计算量最大的。

好了,这一章的内容就到这里。点云生成看似简单,但每一步都有细节。希望你能动手实践一下,把代码跑通,看看效果。有问题随时交流。

专注资料整理