基于三维点云数据的主成分分析方法(PCA)的python实现

发布时间 2023-11-09 09:43:42作者: MKT-porter

https://github.com/mengxingshifen1218/learning-pointcloud/blob/master/%E6%B7%B1%E8%93%9D/CH1/PointCloudHomework1/pca_normal.py

 

 

KD-Tree原理详解

https://zhuanlan.zhihu.com/p/112246942

构建算法:

Input:  无序化的点云,维度k
Output:点云对应的kd-tree
Algorithm1、初始化分割轴:对每个维度的数据进行方差的计算,取最大方差的维度作为分割轴,标记为r2、确定节点:对当前数据按分割轴维度进行检索,找到中位数数据,并将其放入到当前节点上;
3、划分双支:
    划分左支:在当前分割轴维度,所有小于中位数的值划分到左支中;
    划分右支:在当前分割轴维度,所有大于等于中位数的值划分到右支中。
4、更新分割轴:r = (r + 1) % k;
5、确定子节点:
    确定左节点:在左支的数据中进行步骤2确定右节点:在右支的数据中进行步骤2

拿个例子说明为:

二维样例:{(2,3),(5,4),(9,6),(4,7),(8,1),(7,2)}

构建步骤:

1、初始化分割轴:

发现x轴的方差较大,所以,最开始的分割轴为x轴。

2、确定当前节点:

对{2,5,9,4,8,7}找中位数,发现{5,7}都可以,这里我们选择7,也就是(7,2);

3、划分双支数据:

在x轴维度上,比较和7的大小,进行划分:

左支:{(2,3),(5,4),(4,7)}

右支:{(9,6),(8,1)}

4、更新分割轴:

一共就两个维度,所以,下一个维度是y轴。

5、确定子节点:

左节点:在左支中找到y轴的中位数(5,4),左支数据更新为{(2,3)},右支数据更新为{(4,7)}

右节点:在右支中找到y轴的中位数(9,6),左支数据更新为{(8,1)},右支数据为null。

6、更新分割轴:

下一个维度为x轴。

7、确定(5,4)的子节点:

左节点:由于只有一个数据,所以,左节点为(2,3)

右节点:由于只有一个数据,所以,右节点为(4,7)

8、确定(9,6)的子节点:

左节点:由于只有一个数据,所以,左节点为(8,1)

右节点:右节点为空。

最终,就可以构建整个的kd-tree了。

示意图如下所示 [1] :

二维空间表示:

二维坐标系下的分割示意图

kd-tree表示:

2.3、最近邻检索

在构建了完整的kd-tree之后,我们想要使用他来进行高维空间的检索。所以,这里讲解一下比较常用的最近邻检索,其中范围检索也是同样的道理。

最近邻搜索,其实和之前我们曾经学习过的KNN很像。不过,在激光点云章,如果使用常规的KNN算法的话,时间复杂度会空前高涨。因此,为了减少时间消耗,在工程上,一般使用kd-tree进行最近邻检索。

由于kd-tree已经按照维度进行划分了,所以,我们在进行比较的时候,也可以通过维度进行比较,来快速定位到与其最接近的点。由于可能会进行多个最近邻点的检索,所以,算法也可能会发生变化。因此,我将从最简单的一个最近邻开始说起。

 

 

整体求解

 实现PCA分析和法向量计算,并加载数据集中的文件进行验证

import open3d as o3d 
# import os
import numpy as np
from scipy.spatial import KDTree

# from pyntcloud import PyntCloud

# 功能:计算PCA的函数
# 输入:
#     data:点云,NX3的矩阵
#     correlation:区分np的cov和corrcoef,不输入时默认为False
#     sort: 特征值排序,排序是为了其他功能方便使用,不输入时默认为True
# 输出:
#     eigenvalues:特征值
#     eigenvectors:特征向量
def PCA(data, correlation=False, sort=True):
    # normalize 归一化
    mean_data = np.mean(data, axis=0)
    normal_data = data - mean_data
    # 计算对称的协方差矩阵
    H = np.dot(normal_data.T, normal_data)
    # SVD奇异值分解,得到H矩阵的特征值和特征向量
    eigenvectors, eigenvalues, _ = np.linalg.svd(H)

    if sort:
        sort = eigenvalues.argsort()[::-1]
        eigenvalues = eigenvalues[sort]
        eigenvectors = eigenvectors[:, sort]

    return eigenvalues, eigenvectors

def main():

    # 从txt文件获取点,只对点进行处理
    filename = "D:\pointcloud_processing\modelnet40_normal_resampled\\airplane\\airplane_0001.txt"
    points = np.loadtxt(filename, delimiter=',')[:, 0:3] # 导入txt数据到np.array,这里只需导入前3列
    print('total points number is:', points.shape[0])

    # 用PCA分析点云主方向
    w, v = PCA(points) # PCA方法得到对应的特征值和特征向量
    point_cloud_vector = v[:, 0] #点云主方向对应的向量为最大特征值对应的特征向量
    print('the main orientation of this pointcloud is: ', point_cloud_vector)
    # 三个特征向量组成了三个坐标轴
    axis = o3d.geometry.TriangleMesh.create_coordinate_frame().rotate(v, center=(0, 0, 0))

    # 循环计算每个点的法向量
    leafsize = 32   # 切换为暴力搜索的最小数量
    KDTree_radius = 0.1 # 设置邻域半径
    tree = KDTree(points, leafsize=leafsize) # 构建KDTree
    radius_neighbor_idx = tree.query_ball_point(points, KDTree_radius) # 得到每个点的邻近索引
    normals = [] # 定义一个空list

    # -------------寻找法线---------------
    # 首先寻找邻域内的点
    for i in range(len(radius_neighbor_idx)):
        neighbor_idx = radius_neighbor_idx[i] # 得到第i个点的邻近点索引,邻近点包括自己
        neighbor_data = points[neighbor_idx] # 得到邻近点,在求邻近法线时没必要归一化,在PCA函数中归一化就行了
        eigenvalues, eigenvectors = PCA(neighbor_data) # 对邻近点做PCA,得到特征值和特征向量
        normals.append(eigenvectors[:, 2]) # 最小特征值对应的方向就是法线方向
    # ------------法线查找结束---------------
    normals = np.array(normals, dtype=np.float64) # 把法线放在了normals中
    # o3d.geometry.PointCloud,返回了PointCloud类型
    pc_view = o3d.geometry.PointCloud(points=o3d.utility.Vector3dVector(points))
    # 向PointCloud对象中添加法线
    pc_view.normals = o3d.utility.Vector3dVector(normals)
    # 可视化
    o3d.visualization.draw_geometries([pc_view, axis], point_show_normal=True)

if __name__ == '__main__':
    main()