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 两个相机视差匹配,三角测量计算深度 室内外都可用,成本低 弱纹理区域失效,计算量大
我的建议:做机器人抓取,室内环境优先选结构光或ToF,精度和稳定性都够。做室外导航,老老实实用双目或激光雷达。别拿结构光去怼太阳光,我试过,全是噪点。

这里有个关键点:深度图和彩色图是不同坐标系下的。深度图来自红外相机,彩色图来自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 — 带法向量
避坑指南:PCL的PointXYZRGB里,RGB是打包成一个float存的。赋值时别直接写 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全
可视化 一般 优秀,交互式
社区活跃度 老牌,但更新慢 活跃,更新快
我的习惯:做产品级C++项目用PCL,做算法验证和教学用Open3D。两个都学,不吃亏。

4.3 点云滤波与下采样

原始点云数据量巨大,而且充满噪声。一台RealSense D435i,VGA分辨率下每秒产生30万个点。直接处理?你的CPU会哭的。

所以,滤波和下采样是点云处理的“第一道工序”。

4.3.1 常用滤波方法

我挑几个最常用的,也是你在项目中一定会遇到的:

  1. 直通滤波 (PassThrough Filter) — 按坐标轴范围裁剪点云。比如我只关心相机前方0.5m到2m的物体,一刀切掉其他点。
  2. 体素滤波 (Voxel Grid Filter) — 下采样的核心方法。把空间划分成小立方体(体素),每个体素内只保留一个重心点。体素边长越大,点云越稀疏。
  3. 统计滤波 (Statistical Outlier Removal) — 去除离群点。计算每个点到邻居的平均距离,如果距离超过均值+若干倍标准差,就认为是噪声点。
  4. 半径滤波 (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])
注意:体素滤波的体素大小很关键。设太大,细节全丢;设太小,点云还是太密。我一般从5mm开始试,根据场景调整。抓取场景通常5-10mm够用,重建场景可能需要1-2mm。

4.3.2 滤波流程设计

在实际项目中,滤波不是随便堆几个函数就完事。你需要设计一个合理的pipeline。我画了张图,帮你理清思路:

点云滤波与下采样流程 点云预处理 Pipeline 原始点云 直通滤波(裁剪ROI) 体素下采样(降密度) 统计滤波(去离群点) 干净点云(输出) 为什么要这个顺序? 先裁剪ROI减少数据量 再下采样降低计算负担 最后滤波保证质量

这个流程我用了很多年。顺序很重要:先裁剪,再降采样,最后去噪。你想想看,如果先做统计滤波,点云有几十万个点,计算邻居距离的复杂度是O(n²),慢得离谱。先降采样到几万个点,再滤波,速度能快一个数量级。

经验之谈:我曾经在一个项目里,直接对100万点的原始点云做统计滤波,等了3分钟没出结果。后来改成先体素下采样到5万点,再滤波,整个过程不到1秒。记住:先降维,再精修。

4.4 本章小结

这一章我们聊了:

  • 三种主流RGB-D相机的原理和选型建议
  • 点云数据从深度图到三维坐标的转换
  • PCL和Open3D两个库的对比与选择
  • 四种常用滤波方法及其在Open3D中的实现
  • 一个经过实战检验的点云预处理pipeline

点云是3D视觉的“原材料”。原材料质量不好,后面算法再牛也白搭。所以,花时间把滤波和下采样做好,值。

课后动手:拿你自己的深度相机录一段点云数据,按照上面的pipeline走一遍。试试不同的体素大小和滤波参数,看看对结果的影响。这是培养“点云手感”的最好方式。