4. 点云生成与可视化:从深度图到三维世界的桥梁

说实话,点云生成这块,是我在AR项目中踩坑最多的地方之一。你想想看,深度相机给你一张灰度图,每个像素代表距离,但怎么把它变成真实世界中的三维点?这里面的门道,咱们今天一次说清楚。

4.1 相机内参矩阵——点云生成的“翻译官”

深度图本身只是一堆数值。比如像素(320, 240)处的值是850mm,这只能告诉你“那个方向有个东西在850mm处”。但具体在三维空间哪个位置?不知道。

这时候就需要相机内参矩阵了。我习惯把它理解成一个“翻译官”——把像素坐标翻译成相机坐标系下的三维坐标。

内参矩阵长这样:

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

其中:

  • fx, fy:焦距(以像素为单位),决定了成像的缩放比例
  • cx, cy:光心坐标,也就是镜头中心在图像上的投影位置

这些参数怎么来的?标定。我建议你用棋盘格标定板,OpenCV里就有现成的工具。记得我刚开始做的时候,拿着A4纸打印的棋盘格,对着相机拍了二三十张不同角度的照片,跑一遍标定程序就出来了。

小技巧: 如果你用的是Azure Kinect或RealSense这类深度相机,厂家通常会提供出厂标定好的内参。但别完全信任它——温度变化、运输震动都可能让参数漂移。我一般每三个月重新标定一次。

4.2 深度图转点云公式——核心推导

好,现在我们有内参了。怎么把深度图转成点云?公式其实很简单:

Z = depth[u][v]  # 深度值
X = (u - cx) * Z / fx
Y = (v - cy) * Z / fy

每个像素(u, v)对应一个三维点(X, Y, Z)。说白了,就是利用相似三角形原理,把像素坐标反投影到三维空间。

为什么会这样?你想想看,相机成像时,三维点投影到像素平面是:

u = fx * X / Z + cx
v = fy * Y / Z + cy

现在我们有Z和(u, v),反解X和Y就行了。嗯,这里要注意——深度值Z的单位要和fx、fy统一。我遇到过有人用毫米的深度值配像素单位的焦距,结果点云全变形了。

避坑指南: 我曾经在项目里直接用深度图的原始值,没注意它是16位整数(单位0.1mm),而内参是毫米单位。结果生成的点云缩放了10倍,排查了半天才发现。所以一定要确认深度值的单位!

4.3 使用Open3D可视化点云

点云生成后,总得看看效果吧?Open3D是我最常用的工具,轻量、好用,而且Python接口很友好。

基本流程就三步:

  1. 创建点云对象
  2. 填入坐标和颜色
  3. 调用可视化窗口

代码示例:

import open3d as o3d
import numpy as np

# 假设points是Nx3的数组,colors是Nx3的RGB数组(0-1范围)
pcd = o3d.geometry.PointCloud()
pcd.points = o3d.utility.Vector3dVector(points)
pcd.colors = o3d.utility.Vector3dVector(colors)

# 可视化
o3d.visualization.draw_geometries([pcd], 
                                  window_name="深度点云",
                                  width=800, height=600)

我个人习惯在可视化时加上坐标轴,方便观察方向:

# 添加坐标轴
axis = o3d.geometry.TriangleMesh.create_coordinate_frame(size=0.5)
o3d.visualization.draw_geometries([pcd, axis])
经验之谈: 如果点云显示出来全是黑的,大概率是颜色值范围不对。Open3D要求RGB在0-1之间,而深度相机给的通常是0-255。记得除以255。

4.4 点云下采样——给点云“减肥”

原始点云数据量太大了。一个640×480的深度图,就是30万个点。实时处理时,这会让CPU和GPU都吃不消。

下采样就是给点云“减肥”,同时尽量保留形状特征。最常用的方法是体素滤波(Voxel Grid Filter):

# 体素下采样,voxel_size=0.01表示每个体素边长1厘米
downsampled_pcd = pcd.voxel_down_sample(voxel_size=0.01)

体素大小怎么选?我一般这样定:

  • 精细场景(如桌面物体):0.005-0.01米
  • 中等场景(如室内房间):0.02-0.05米
  • 大场景(如室外):0.1米以上

你想想看,体素越小,保留的细节越多,但点数也越多。这是个权衡。我做过一个AR家具摆放的项目,体素设成0.008米,既能看清家具轮廓,又能跑到30帧。

4.5 离群点移除——清理“噪点”

深度相机不是完美的。边缘处、反光表面、远距离区域,经常会出现一些“飞点”——明明那里没有物体,却冒出一个点来。

这些离群点必须去掉,否则AR交互时会出现莫名其妙的碰撞。我常用的方法是统计滤波:

# 统计滤波,nb_neighbors=20表示检查每个点最近的20个邻居
# std_ratio=2.0表示超过2倍标准差的点视为离群点
cl, ind = downsampled_pcd.remove_statistical_outlier(
    nb_neighbors=20, std_ratio=2.0)
cleaned_pcd = downsampled_pcd.select_by_index(ind)

参数调优建议:

场景 nb_neighbors std_ratio
室内桌面 10-20 1.5-2.0
室内房间 20-30 2.0-3.0
室外 30-50 3.0-5.0
注意: 我曾经在一个人体扫描项目里,把std_ratio设得太小(1.0),结果把手指尖这种真实细节也当成离群点去掉了。后来发现,人体边缘的点本来就稀疏,标准差的阈值要适当放宽。

4.6 完整流程串联

把上面这些串起来,就是一个完整的点云处理管线:

def depth_to_pointcloud(depth_img, color_img, K, voxel_size=0.01):
    # 1. 深度图转点云
    h, w = depth_img.shape
    fx, fy = K[0,0], K[1,1]
    cx, cy = K[0,2], K[1,2]
    
    u, v = np.meshgrid(np.arange(w), np.arange(h))
    Z = depth_img.astype(np.float32) / 1000.0  # 转成米
    X = (u - cx) * Z / fx
    Y = (v - cy) * Z / fy
    
    points = np.stack((X, Y, Z), axis=-1).reshape(-1, 3)
    colors = color_img.reshape(-1, 3) / 255.0
    
    # 2. 创建点云
    pcd = o3d.geometry.PointCloud()
    pcd.points = o3d.utility.Vector3dVector(points)
    pcd.colors = o3d.utility.Vector3dVector(colors)
    
    # 3. 下采样
    pcd = pcd.voxel_down_sample(voxel_size)
    
    # 4. 移除离群点
    pcd, _ = pcd.remove_statistical_outlier(20, 2.0)
    
    return pcd

这段代码我用了好几年,基本框架没变过。你拿去用的时候,注意调整一下深度值的缩放系数——不同相机不一样。

核心要点: 点云生成的关键是内参准确,点云处理的关键是参数调优。别指望一套参数打天下,每个场景都要微调。

好了,点云生成与可视化的内容就这些。下采样和离群点移除这两个操作,在AR交互中几乎是必做的——少了它们,你的点云要么跑不动,要么全是噪点。我个人建议,每次拿到新的深度相机,先跑一遍这个流程,把参数摸透了再开始做上层应用。