4. 点云生成与坐标变换:相机内参标定、深度图转点云、坐标系变换(相机系到世界系)、点云下采样
各位同学,欢迎来到深度相机部署实战的第四讲。今天我们要聊的,是3D视觉里最基础也最绕不开的一环——点云生成与坐标变换。
说实话,我刚开始做嵌入式视觉那会儿,觉得点云不就是把深度图的像素值换算成三维坐标嘛,有什么难的?结果第一次在ARM板子上跑,帧率直接掉到个位数,点云还全是扭曲的。嗯,这里面的坑,我今天一个一个帮你们填上。
4.1 相机内参标定:你的相机到底长什么样?
要生成点云,首先得知道相机内部的光学参数。说白了,就是搞清楚像素坐标和物理坐标之间怎么换算。
相机内参矩阵K长这样:
K = [fx, 0, cx;
0, fy, cy;
0, 0, 1]
其中:
- fx, fy:焦距(以像素为单位)
- cx, cy:光心偏移(主点坐标)
我建议你直接用OpenCV的棋盘格标定法。但注意,嵌入式平台的摄像头往往有畸变,尤其是鱼眼镜头。我曾经在RK3588上调试一个双目模组,标定完发现点云边缘全是弯曲的——后来才发现是径向畸变参数没加进去。
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倍。
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万个点,处理起来轻松很多。
4.5 本章知识体系
下面这张图,是我自己总结的点云生成与坐标变换全流程。你可以把它当作一个检查清单:
4.6 实战建议
最后,给你几个实战中的小建议:
- 先离线标定,再在线部署:内参和外参最好在PC上标定好,固化到配置文件中。嵌入式平台只做加载和计算。
- 点云数据量要控制:我建议根据应用场景动态调整下采样参数。比如建图时用密一点的点云,定位时用稀疏的。
- 多线程处理:深度图转点云和坐标系变换可以放在一个线程,下采样和后续处理放在另一个线程。流水线作业,效率更高。
好了,这一章的内容就到这里。点云生成看似简单,但每一步都有细节。希望你能动手实践一下,把代码跑通,看看效果。有问题随时交流。