3. 深度相机与点云获取:RGB-D相机原理与点云基础
好,咱们进入第三章。这一章要聊的是机器人视觉里最基础、也最绕不开的东西——深度相机和点云。
说白了,你让机器人去抓一个杯子,它得先知道杯子在哪儿、长什么样、离自己多远。这些信息,就靠深度相机来给。我个人习惯把深度相机比作机器人的“眼睛+尺子”——它不光能看见颜色,还能测出每个像素对应的三维距离。
3.1 RGB-D相机三大主流方案
市面上主流的RGB-D相机,原理上分三类:结构光、ToF(飞行时间)、双目立体视觉。我当年刚入行时,也傻傻分不清它们到底有啥区别。后来踩过坑,才慢慢摸透了各自的脾气。
| 方案 | 原理 | 典型代表 | 优势 | 劣势 |
|---|---|---|---|---|
| 结构光 | 投射已知编码图案,通过图案变形计算深度 | Kinect v1, Intel RealSense D415 | 室内精度高,纹理丰富场景表现好 | 易受环境光干扰,室外基本不能用 |
| ToF | 发射光脉冲,测量反射时间差 | Kinect v2, Azure Kinect, 手机LiDAR | 帧率高,远距离表现好,抗环境光稍强 | 分辨率偏低,边缘有飞点噪声 |
| 双目立体 | 双摄像头视差匹配,三角测量 | ZED, RealSense D435i | 室外可用,成本低,无主动光 | 弱纹理区域失效,计算量大 |
选型建议:室内抓取场景,我首推结构光或ToF;室外移动机器人,双目更靠谱。别指望一个方案打天下。
3.1.1 结构光——靠“投影”算深度
结构光的思路很巧妙。相机先往场景里投一个已知的红外光斑图案(比如散斑或条纹码),然后用红外摄像头拍下这个图案。如果场景里有物体,图案就会发生变形。通过分析变形量,就能算出每个像素的深度。
我曾经在项目里用RealSense D415做桌面抓取,室内光照稳定,精度能到毫米级。但有一次搬到窗边,阳光一照,深度图直接崩了——全是黑洞。嗯,这里要注意:结构光对强环境光非常敏感,室外基本歇菜。
3.1.2 ToF——靠“时间”算深度
ToF的原理更直接:发射一束调制过的红外光,打到物体上反射回来,测量光飞行的时间差,乘以光速再除以2,就是距离。
ToF的好处是帧率高,能做到30fps甚至60fps,适合动态场景。但它的分辨率普遍不高,VGA(640×480)就算不错了。而且物体边缘容易产生“飞点”——说白了就是深度值突然跳变,像鬼影一样。我处理过一批ToF数据,飞点滤波写了好几个版本才压住。
3.1.3 双目立体视觉——靠“视差”算深度
双目模仿的是人眼。两个摄像头相隔一定距离(基线),同时拍下左右两张图。通过匹配两幅图像中的对应点,算出视差,再用三角测量原理反推深度。
双目最大的优势是不需要主动光源,室外也能用。但它的致命弱点是:遇到白墙、光滑表面这类弱纹理区域,匹配算法直接抓瞎。我当年调ZED相机时,对着实验室的白墙,深度图全是噪点。后来加了个投影仪打纹理,才勉强能用。
小技巧:双目相机标定非常关键。基线越长,远距离精度越高,但近距离盲区也越大。选型时得权衡。
3.2 点云数据结构
深度相机输出的原始数据,经过处理后,就变成了点云。点云是什么?说白了,就是一大堆三维点的集合。每个点包含(x, y, z)坐标,有时还带上RGB颜色或法向量信息。
点云的数据结构,常见的有两种:
- 有序点云:按图像的行列排列,每个像素对应一个三维点。优点是索引快,邻域查询方便。RealSense、Kinect输出的就是这种。
- 无序点云:点没有固定排列顺序,就是一个列表。优点是灵活,可以任意增删点。激光雷达扫出来的点云通常是无序的。
我个人习惯在预处理阶段尽量保留有序结构,因为做滤波和分割时,邻域信息能省不少计算量。
注意:点云不是“完美”的。深度相机有噪声、有缺失、有离群点。拿到原始点云后,第一件事永远是预处理——滤波、降采样、去噪。别直接拿去用,否则抓取时定位偏差会让你怀疑人生。
3.3 PCL与Open3D基础
处理点云,绕不开两个库:PCL(Point Cloud Library)和Open3D。我两个都用过,各有千秋。
| 特性 | PCL | Open3D |
|---|---|---|
| 语言 | C++为主,Python绑定较弱 | Python原生,C++后端 |
| 安装难度 | 依赖多,编译麻烦 | pip install 搞定 |
| 算法丰富度 | 极其丰富,滤波/配准/分割/识别全覆盖 | 常用算法都有,但不如PCL全 |
| 可视化 | VTK,功能强但代码啰嗦 | 内置可视化,简洁好用 |
| 上手速度 | 学习曲线陡 | 对新手友好 |
我的建议是:快速原型用Open3D,生产部署用PCL。我自己做实验时,先用Open3D调通流程,再转成PCL C++版本做性能优化。
3.3.1 PCL快速上手示例
下面是一个PCL读取点云、做体素滤波、再可视化的典型流程。代码不长,但涵盖了日常最常用的操作。
#include <pcl/io/pcd_io.h>
#include <pcl/point_types.h>
#include <pcl/filters/voxel_grid.h>
#include <pcl/visualization/pcl_visualizer.h>
int main() {
// 1. 读取点云
pcl::PointCloud<pcl::PointXYZRGB>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZRGB>);
pcl::io::loadPCDFile("scene.pcd", *cloud);
std::cout << "原始点云点数: " << cloud->size() << std::endl;
// 2. 体素滤波降采样
pcl::VoxelGrid<pcl::PointXYZRGB> voxel;
voxel.setInputCloud(cloud);
voxel.setLeafSize(0.01f, 0.01f, 0.01f); // 1cm体素
pcl::PointCloud<pcl::PointXYZRGB>::Ptr cloud_filtered(new pcl::PointCloud<pcl::PointXYZRGB>);
voxel.filter(*cloud_filtered);
std::cout << "滤波后点数: " << cloud_filtered->size() << std::endl;
// 3. 可视化
pcl::visualization::PCLVisualizer viewer("PCL Viewer");
viewer.addPointCloud(cloud_filtered, "cloud");
while (!viewer.wasStopped()) {
viewer.spinOnce();
}
return 0;
}
避坑指南:体素滤波的leaf size很关键。设太大,细节全丢;设太小,降采样效果不明显。我一般从0.01m开始试,根据场景调整。
3.3.2 Open3D快速上手示例
Open3D的Python接口非常简洁。同样的流程,代码量少一半。
import open3d as o3d
import numpy as np
# 1. 读取点云
cloud = o3d.io.read_point_cloud("scene.pcd")
print(f"原始点云点数: {len(cloud.points)}")
# 2. 体素滤波降采样
voxel_size = 0.01
cloud_down = cloud.voxel_down_sample(voxel_size)
print(f"滤波后点数: {len(cloud_down.points)}")
# 3. 统计滤波去噪
cl, ind = cloud_down.remove_statistical_outlier(nb_neighbors=20, std_ratio=2.0)
cloud_clean = cloud_down.select_by_index(ind)
# 4. 可视化
o3d.visualization.draw_geometries([cloud_clean])
你看,Open3D几行就搞定了。而且它的可视化窗口支持交互操作——旋转、缩放、选点,非常方便调试。
3.4 本章知识体系总览
为了帮你理清脉络,我画了一张结构图。它把深度相机原理、点云数据结构、以及PCL/Open3D的关系串在了一起。
从图上你能看到,整个流程是线性的:深度相机 → 点云生成 → 数据结构 → 工具库 → 下游应用。每一步都有坑,但每一步也都有成熟的解法。
核心要点回顾:
- 结构光精度高但怕光,ToF帧率高但分辨率低,双目室外强但怕白墙。
- 点云分有序和无序,预处理是必修课。
- PCL适合生产,Open3D适合快速验证。两个都学,不吃亏。