4. 点云生成与可视化:从深度图到三维世界的桥梁
说实话,点云生成这块,是我在AR项目中踩坑最多的地方之一。你想想看,深度相机给你一张灰度图,每个像素代表距离,但怎么把它变成真实世界中的三维点?这里面的门道,咱们今天一次说清楚。
4.1 相机内参矩阵——点云生成的“翻译官”
深度图本身只是一堆数值。比如像素(320, 240)处的值是850mm,这只能告诉你“那个方向有个东西在850mm处”。但具体在三维空间哪个位置?不知道。
这时候就需要相机内参矩阵了。我习惯把它理解成一个“翻译官”——把像素坐标翻译成相机坐标系下的三维坐标。
内参矩阵长这样:
K = [[fx, 0, cx],
[ 0, fy, cy],
[ 0, 0, 1]]
其中:
- fx, fy:焦距(以像素为单位),决定了成像的缩放比例
- cx, cy:光心坐标,也就是镜头中心在图像上的投影位置
这些参数怎么来的?标定。我建议你用棋盘格标定板,OpenCV里就有现成的工具。记得我刚开始做的时候,拿着A4纸打印的棋盘格,对着相机拍了二三十张不同角度的照片,跑一遍标定程序就出来了。
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统一。我遇到过有人用毫米的深度值配像素单位的焦距,结果点云全变形了。
4.3 使用Open3D可视化点云
点云生成后,总得看看效果吧?Open3D是我最常用的工具,轻量、好用,而且Python接口很友好。
基本流程就三步:
- 创建点云对象
- 填入坐标和颜色
- 调用可视化窗口
代码示例:
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])
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 |
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交互中几乎是必做的——少了它们,你的点云要么跑不动,要么全是噪点。我个人建议,每次拿到新的深度相机,先跑一遍这个流程,把参数摸透了再开始做上层应用。