4. 深度相机与点云数据:RGB-D相机原理、点云数据结构(PCL/Open3D)、点云滤波与下采样
各位同学,欢迎来到第四章。
前面我们聊了相机模型和标定,那都是为了一件事——让相机能“看懂”三维世界。但普通RGB相机只能给你一张平面照片,它不知道物体离你有多远。这时候,深度相机就该登场了。
我个人觉得,深度相机是3D视觉里最性感的传感器之一。它直接把二维图像和三维距离信息绑在一起,输出RGB-D数据。有了它,你才能拿到点云,才能做后续的定位、抓取、避障。
4.1 RGB-D相机的工作原理
RGB-D相机,说白了就是“彩色相机+深度传感器”的组合。它输出的每一帧,都包含一张彩色图(RGB)和一张深度图(Depth)。深度图上每个像素的值,代表那个点到相机的距离。
市面上主流的RGB-D相机,原理分三种。我列个表,你一看就明白:
| 技术方案 | 代表产品 | 工作原理 | 优点 | 缺点 |
|---|---|---|---|---|
| 结构光 | Kinect v1, Intel RealSense D415 | 投射已知图案的红外光斑,通过光斑变形计算深度 | 室内精度高,纹理丰富场景表现好 | 易受环境光干扰,室外基本不能用 |
| 飞行时间(ToF) | Kinect v2/Azure, 苹果LiDAR | 发射红外脉冲,测量反射时间差 | 帧率高,抗环境光稍好,适合动态场景 | 分辨率偏低,多路径干扰 |
| 双目立体视觉 | ZED, Intel RealSense D435i | 两个相机视差匹配,三角测量计算深度 | 室内外都可用,成本低 | 弱纹理区域失效,计算量大 |
这里有个关键点:深度图和彩色图是不同坐标系下的。深度图来自红外相机,彩色图来自RGB相机。它们之间有个外参,需要标定对齐。很多SDK帮你做了这一步,但如果你自己拼传感器,就得手动标定。
4.2 点云数据结构
拿到深度图之后,怎么变成点云?
其实很简单。深度图每个像素(u, v)对应一个深度值d,结合相机内参,就能反投影到三维空间:
// 伪代码:深度图转点云
for v in 0..height:
for u in 0..width:
d = depth_img[v][u]
if d == 0: continue // 无效深度
z = d / scale_factor
x = (u - cx) * z / fx
y = (v - cy) * z / fy
point_cloud.push(Point(x, y, z, color[u][v]))
点云的数据结构,工业界和学术界最常用的两个库:PCL 和 Open3D。
4.2.1 PCL (Point Cloud Library)
PCL是老牌库,功能极其全面。从滤波、配准、分割到识别,几乎你能想到的点云算法它都有。但它的缺点也很明显——编译麻烦,API设计偏C++98风格,用起来有点“重”。
PCL里点云的基本类型是 pcl::PointCloud<T>,T可以是:
pcl::PointXYZ— 只有xyz坐标pcl::PointXYZRGB— 带RGB颜色pcl::PointXYZRGBA— 带RGBA颜色pcl::PointNormal— 带法向量
p.rgb = 255,要用 p.rgba 或者 uint32_t rgb = ((uint32_t)r << 16 | (uint32_t)g << 8 | (uint32_t)b); p.rgb = *reinterpret_cast<float*>(&rgb);。我当年被这个坑了整整一个下午。
4.2.2 Open3D
Open3D是后来者,由Intel实验室开发。它的设计理念是“现代、简洁、Python友好”。我个人现在做原型验证,基本都用Open3D。它的API非常直观:
import open3d as o3d
import numpy as np
# 从numpy数组创建点云
points = np.random.rand(100, 3) # 100个随机点
pcd = o3d.geometry.PointCloud()
pcd.points = o3d.utility.Vector3dVector(points)
# 给点云上色
colors = np.random.rand(100, 3)
pcd.colors = o3d.utility.Vector3dVector(colors)
# 可视化
o3d.visualization.draw_geometries([pcd])
你看,几行代码就能搞定。Open3D还内置了GUI、可视化、IO等功能,非常适合快速迭代。
| 对比项 | PCL | Open3D |
|---|---|---|
| 语言 | C++为主,Python绑定较弱 | Python原生,C++后端 |
| 编译难度 | 高(依赖多) | 低(pip install即可) |
| 算法丰富度 | 极丰富 | 丰富,但不如PCL全 |
| 可视化 | 一般 | 优秀,交互式 |
| 社区活跃度 | 老牌,但更新慢 | 活跃,更新快 |
4.3 点云滤波与下采样
原始点云数据量巨大,而且充满噪声。一台RealSense D435i,VGA分辨率下每秒产生30万个点。直接处理?你的CPU会哭的。
所以,滤波和下采样是点云处理的“第一道工序”。
4.3.1 常用滤波方法
我挑几个最常用的,也是你在项目中一定会遇到的:
- 直通滤波 (PassThrough Filter) — 按坐标轴范围裁剪点云。比如我只关心相机前方0.5m到2m的物体,一刀切掉其他点。
- 体素滤波 (Voxel Grid Filter) — 下采样的核心方法。把空间划分成小立方体(体素),每个体素内只保留一个重心点。体素边长越大,点云越稀疏。
- 统计滤波 (Statistical Outlier Removal) — 去除离群点。计算每个点到邻居的平均距离,如果距离超过均值+若干倍标准差,就认为是噪声点。
- 半径滤波 (Radius Outlier Removal) — 如果一个点周围指定半径内邻居数量少于阈值,就干掉它。
来看Open3D的实现:
import open3d as o3d
import numpy as np
# 读取点云
pcd = o3d.io.read_point_cloud("scene.ply")
# 1. 直通滤波:只保留z轴0.5~2.0m的点
bbox = o3d.geometry.AxisAlignedBoundingBox(
min_bound=(-np.inf, -np.inf, 0.5),
max_bound=(np.inf, np.inf, 2.0)
)
pcd_cropped = pcd.crop(bbox)
# 2. 体素下采样:体素边长5mm
pcd_down = pcd_cropped.voxel_down_sample(voxel_size=0.005)
# 3. 统计滤波:去除离群点
cl, ind = pcd_down.remove_statistical_outlier(
nb_neighbors=20,
std_ratio=2.0
)
pcd_filtered = pcd_down.select_by_index(ind)
# 可视化对比
o3d.visualization.draw_geometries([pcd_filtered])
4.3.2 滤波流程设计
在实际项目中,滤波不是随便堆几个函数就完事。你需要设计一个合理的pipeline。我画了张图,帮你理清思路:
这个流程我用了很多年。顺序很重要:先裁剪,再降采样,最后去噪。你想想看,如果先做统计滤波,点云有几十万个点,计算邻居距离的复杂度是O(n²),慢得离谱。先降采样到几万个点,再滤波,速度能快一个数量级。
4.4 本章小结
这一章我们聊了:
- 三种主流RGB-D相机的原理和选型建议
- 点云数据从深度图到三维坐标的转换
- PCL和Open3D两个库的对比与选择
- 四种常用滤波方法及其在Open3D中的实现
- 一个经过实战检验的点云预处理pipeline
点云是3D视觉的“原材料”。原材料质量不好,后面算法再牛也白搭。所以,花时间把滤波和下采样做好,值。