点云数据处理初识
主成分分析
主成分分析(PCA)是一种常用于数据降维的技术,通过计算数据的主成分(即数据中方差最大的方向),可以找到数据的主要方向。它在点云处理中的应用主要是用于确定点云的主要方向和提取点云的几何特征。PCA 通过将数据中心化(即减去均值),然后计算协方差矩阵,再进行奇异值分解(SVD),从中提取出特征向量和特征值,特征向量表示数据的主方向,特征值表示各个主方向的方差大小。
具体步骤:
- 计算点云数据的均值:计算所有点在每个维度上的均值(例如,x, y, z 坐标的均值)。
- 数据中心化:将数据减去均值,使得数据的中心位于原点。
- 构造协方差矩阵:通过数据的协方差矩阵来捕捉点云中各个维度之间的关系。
- 进行奇异值分解(SVD):对协方差矩阵进行奇异值分解,提取出特征向量和特征值。特征向量指示数据的主方向,特征值表示这些方向的方差大小。
- 排序特征值:根据特征值的大小对特征向量进行排序,确保主方向在前。
核心代码
def PCA(data, correlation=False, sort=True):
# 作业1
# 计算均值
data_mean = np.mean(data,axis=0)
# 归一化
normalize_data = data - data_mean
# 构造协方差矩阵
H = np.dot(normalize_data.T,normalize_data)
# SVD分解
eigenvectors, eigenvalues, eigenvectors_t = np.linalg.svd(H)
if sort:
sort = eigenvalues.argsort()[::-1]
eigenvalues = eigenvalues[sort]
eigenvectors = eigenvectors[:, sort]
return eigenvalues, eigenvectors
v,u = PCA(points)
# 对u特征值矩阵进行缩放
scale = 100
u = u * scale
print('the main orientation of this pointcloud is:',u[:,0])
point = [[0,0,0],u[:,0],u[:,1]]
lines = [[0,1],[0,2]]
colors = [[1,0,0],[0,1,0]]
line_set = o3d.geometry.LineSet(
points = o3d.utility.Vector3dVector(point),
lines = o3d.utility.Vector2iVector(lines)
)
line_set.colors = o3d.utility.Vector3dVector(colors)
o3d.visualization.draw_geometries([point_cloud_o3d,line_set])
结果展示:
不显示主成分问题
问题描述:排查主成分计算问题仍无法正常显示
解决:考虑主成分缩放
scale = 100
u = u * scale
法向量
算法描述:
法向量是表示点云表面朝向的一个重要几何特征。通过计算点云中每个点的法向量,可以更好地理解点云的局部表面特征,进而用于表面重建、物体识别、碰撞检测等任务。法向量的计算通常基于邻域点的局部几何特征,常用的方法是基于 PCA(主成分分析)来估计法向量。
在该算法中,我们首先通过 KD 树来寻找每个点的邻域点,然后使用 PCA 对邻域点进行分析,提取主成分,法向量通常对应于最小的特征值方向(即点云表面的法向量)。
具体步骤:
- 构建 KD 树:通过 KD 树(K-Nearest Neighbor Tree)加速最近邻点的查询。
- 计算每个点的邻域:对于每个点,找到其最近的 K 个邻域点。
- PCA 计算法向量:对每个点的邻域点使用 PCA 计算主成分,法向量通常是对应于最小特征值的特征向量。
- 法向量归一化:得到法向量后,根据需要进行缩放或归一化。
- 可视化:将法向量绘制在点云上,帮助直观展示点云的表面方向。
核心代码
# 循环计算每个点的法向量
pcd_tree = o3d.geometry.KDTreeFlann(point_cloud_o3d)
normals = []
for i in range(points.shape[0]):
[_, idx, _] = pcd_tree.search_knn_vector_3d(point_cloud_o3d.points[i],15)
k_nearest_point = np.asarray(point_cloud_o3d.points)[idx, :]
v, u = PCA(k_nearest_point)
normals.append(u[:,2])
normals = np.array(normals, dtype=np.float64)
normals_factor = 1
normals = normals * normals_factor
# TODO: 此处把法向量存放在了normals中
point_cloud_o3d.normals = o3d.utility.Vector3dVector(normals)
print(normals)
o3d.visualization.draw_geometries([point_cloud_o3d, line_set],point_show_normal=True)
# point_show_normal=True 绘制展示法向量
结果展示

法向量不显示问题
问题描述:排查法向量计算问题,无法正常显示
调用o3d.visualization.draw_geometries 函数时加上,point_show_normal=True(如代码最后一行)
法向量不垂直

问题描述:法向量显示发现与平面不垂直
解决:可能是像素点过少,1.可以增大选取的K临近点数量;2.选取像素点更多的点云数据(占用空间大的)
栅格下采样
算法描述:
栅格下采样(Voxel Downsampling)是一种常用的点云简化方法,通过将点云数据划分为小的体素(Voxel)单元,然后对每个体素内的点进行某种形式的聚合(如随机采样或均值采样)。这种方法能够有效减少点云的大小,同时保留其主要的几何特征,广泛应用于点云处理、物体识别等领域。
常见的体素采样方法包括:
- 随机采样(Random Sampling):从每个体素内随机选取一个点。
- 均值采样(Centroid Sampling):从每个体素内计算所有点的均值,并将均值作为该体素的代表点。
具体步骤:
- 计算最小值和最大值:首先,计算点云数据的最小值和最大值,用于确定点云数据的边界。
- 划分体素网格:根据设定的体素大小(leaf_size),将点云划分为多个体素单元,每个体素包含一组相邻的点。
- 对体素内的点进行采样:根据选择的采样模式(random 或 centroid),在每个体素内选择一个代表点。
- random:随机选择一个点作为体素代表点。
- centroid:计算体素内所有点的均值,作为该体素的代表点。
- 返回下采样后的点云:最终返回经过采样后的点云数据。
核心代码
# 功能:对点云进行voxel滤波
# 输入:
# point_cloud:输入点云
# leaf_size: voxel尺寸
def voxel_filter(point_cloud, leaf_size, mode):
filtered_points = []
# 作业3
data = point_cloud.values # Pandas DataFrame 转换为 numpy 数组
# 求出xyz三轴各自的最小值最大值
min_d = data.min(axis=0)
max_d = data.max(axis=0)
D = (max_d - min_d) / leaf_size
# 求出所有点所在山歌序号h
point_x, point_y, point_z = np.array(point_cloud.x), np.array(point_cloud.y), np.array(point_cloud.z)
h_x, h_y, h_z = np.floor((point_x - min_d[0]) / leaf_size), \
np.floor((point_y - min_d[1]) / leaf_size), \
np.floor((point_z - min_d[2]) / leaf_size)
h = np.array(np.floor(h_x + h_y * D[0] + h_z * D[0] * D[1]), dtype=int)
# 根据h对点云进行排序
data = np.c_[h, point_x, point_y, point_z]
data = data[data[:, 0].argsort()]
if mode == 'random':
current_voxel = data[0][0] # 初始体素分区
voxel_points = [] # 当前体素内的所有点
for i in range(data.shape[0]):
# 判断是否相等
if data[i][0] != current_voxel:
# 如果遇到新体素分区,从当前体素分区内随机选一个点
if len(voxel_points) > 0:
random_point = voxel_points[np.random.randint(len(voxel_points))]
filtered_points.append(random_point)
# 清空当前体素内的点,开始新的体素分区
current_voxel = data[i][0]
voxel_points = []
# 添加当前点到体素分区内
voxel_points.append(data[i][1:])
# 最后一个体素分区的随机点
if len(voxel_points) > 0:
random_point = voxel_points[np.random.randint(len(voxel_points))]
filtered_points.append(random_point)
# 均值采样
if mode == 'centroid':
filtered_points = []
data_points = []
for i in range(data.shape[0] - 1):
# 判断是否相等
if data[i][0] != data[i + 1][0]:
if data_points: # 确保data_points不为空
filtered_points.append(np.mean(data_points, axis=0))
data_points = [] # 清空数据
data_points.append(data[i][1:])
# 处理最后一个分区
if data_points:
filtered_points.append(np.mean(data_points, axis=0))
# 把点云格式改成array,并对外返回
filtered_points = np.array(filtered_points, dtype=np.float64)
return filtered_points
def main():
# 指定点云路径
cat_index = 2 # 物体编号,范围是0-39,即对应数据集中40个物体
root_dir = os.path.join('E:', 'WY', 'Dataset', 'ModelNet', 'ply_data_points', 'ModelNet40') # 数据集路径
cat = os.listdir(root_dir)
filename = os.path.join(root_dir, cat[cat_index], 'train', cat[cat_index] + '_0355.ply') # 默认使用第一个点云
# 加载点云文件
point_cloud_pynt = PyntCloud.from_file(filename)
# 转成open3d能识别的格式
point_cloud_o3d = point_cloud_pynt.to_instance("open3d", mesh=False)
o3d.visualization.draw_geometries([point_cloud_o3d]) # 显示原始点云
# 调用voxel滤波函数,实现滤波
filtered_cloud = voxel_filter(point_cloud_pynt.points, 2.0, 'random')
point_cloud_o3d.points = o3d.utility.Vector3dVector(filtered_cloud)
# 显示滤波后的点云
o3d.visualization.draw_geometries([point_cloud_o3d])
结果展示:
原图:
均值采样 :
半径为2


随机采样:
半径为2
半径为4
可能出现的问题
numpy数组和pandas数据格式冲突可能会造成RuntimeError
解决方法: 统一转换成numpy数组进行数据处理