3D点云处理1:PCA,法向量估计,体素滤波

目标

使用modelnet40数据集,对数据集中的数据进行原始点云展示、PCA投影、法向量估计、Centroid降采样与Random select 降采样
github链接

结果展示

从上至下分别为:airplane0001, plant0001, person0001

从左至右分别为: 原始点云、PCA投影、法向量估计、Centroid降采样、Random select降采样
在这里插入图片描述

PCA 与 法向量估计

算法原理

参考:

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

http://blog.codinglabs.org/articles/pca-tutorial.html

https://blog.csdn.net/u012162613/article/details/42177327

PCA

过程:

数据-> 去中心化

​ -> 协方差矩阵

​ -> 特征向量 -> 坐标轴方向

​ -> 特征值 -> 坐标轴方向的方差

降维:基的数量小于向量本身的维数

如何选择最优基?

N维向量 -> K维向量 , 如何选择K个基使得信息损失最小

  • 方差

    表示数值分散程度,变量的方差为每个元素与变量均值的差的平方和的均值
    V a r ( a ) = 1 m ∑ i = 1 m ( a i − μ ) 2 Var(a) = \frac{1}{m}\sum_{i=1}^{m}(a_{i}-\mu)^{2} Var(a)=m1i=1m(aiμ)2
    方便处理 -> 均值化为0
    V a r ( a ) = 1 m ∑ i = 1 m a i 2 Var(a) = \frac{1}{m}\sum_{i=1}^{m}a_{i}^{2} Var(a)=m1i=1mai2

  • 协方差

    表示两个样本的相关性

    希望表示尽可能多的信息 -> 不存在线性相关性
    C o v ( a , b ) = 1 m − 1 ∑ i = 1 m ( a i − μ a ) ( b i − μ b ) Cov(a,b)=\frac{1}{m-1}\sum_{i=1}^{m}(a_{i}-\mu_{a})(b_{i}-\mu_{b}) Cov(a,b)=m11i=1m(aiμa)(biμb)
    均值为0:
    C o v ( a , b ) = 1 m ∑ i = 1 m a i b i Cov(a,b)=\frac{1}{m}\sum_{i=1}^{m}a_{i}b_{i} Cov(a,b)=m1i=1maibi

    优化目标:将一组 N 维向量降为 K 维,其目标是选择 K 个单位正交基,使得原始数据变换到这组基上后,各变量两两间协方差为 0,而变量方差则尽可能大(在正交的约束下,取最大的 K 个方差)。

  • 协方差矩阵

    散度矩阵:
    C = X X T C=XX^{T} C=XXT
    对角线分别对应各个变量的方差第 i 行 j 列和 j 行 i 列元素相同,表示 i 和 j 两个变量的协方差

    优化目标:将除对角元外的其余元素化为0,在对角线上将元素按大小从上到下排列(方差大)

  • 矩阵对角化

    设原始数据矩阵 X X X 对应的协方差矩阵为 C C C

    设矩阵 P P P 为一组基按行组成的矩阵

    设矩阵 Y Y Y X X X P P P 做基变换后的数据, Y = P X Y=PX Y=PX

    设矩阵 Y Y Y 的协方差矩阵为 D D D

    D D D C C C 的关系可推导如下:

在这里插入图片描述

优化目标:寻找矩阵 P P P 使得 P C P T PCP^{T} PCPT 为对角矩阵,且对角元按大小从上到下排列。矩阵 P P P 的前 k k k 行组成的矩阵与 X X X 相乘即可得到降维结果 Y Y Y

C n × n C_{n \times n} Cn×n 为实对称矩阵可知,必可找到 n n n 个单位正交特征向量,求之即可。

法向量估计
  1. 选择一点P
  2. 找到P的邻居节点
  3. 对邻域节点PCA
  4. 法向量->最不显著->最小的特征向量

核心代码

import os
import open3d as o3d
import numpy as np


def data_loader():
    folder_path = "./40_sample_data/"
    files = os.listdir(folder_path)
    points_data = []
    for file in files:
        file_path = folder_path + file
        points_data.append(np.loadtxt(file_path, delimiter=","))
    return points_data

def test_data_loader():
    file_path = "./40_sample_data/person_0001.txt"
    points_data = []
    points_data.append(np.loadtxt(file_path, delimiter=","))
    return points_data

def PCA(point_cloud_group, n):
    col_mean_val = np.mean(point_cloud_group, axis = 0)
    zero_mean_matrix = point_cloud_group - col_mean_val
    cov_matrix = np.cov(zero_mean_matrix, rowvar = 0)
    eig_val, eig_vector = np.linalg.eig(cov_matrix)
    origin_eig_val_index = np.argsort(eig_val)[::-1]
    origin_max_n_eig_val_index = origin_eig_val_index[:n]
    n_vector = eig_vector[:,origin_max_n_eig_val_index]
    low_point_data = zero_mean_matrix * np.mat(n_vector)
    return low_point_data, eig_vector[:,origin_eig_val_index[-1]]


if __name__ == "__main__":
    pcd_list = test_data_loader()
    for point in pcd_list:
        useful_point = point[:, :3]
        pcd = o3d.geometry.PointCloud()
        pcd.points = o3d.utility.Vector3dVector(useful_point)
        o3d.visualization.draw_geometries([pcd])
        # PCA
        PCA_point,_ = PCA(useful_point, 2)
        PCA_point = np.asarray(PCA_point)
        pcd_pca = o3d.geometry.PointCloud()
        tmp = np.zeros(np.shape(PCA_point)[0])
        PCA_3d_point = np.column_stack((PCA_point,tmp))
        pcd_pca.points = o3d.utility.Vector3dVector(PCA_3d_point)
        o3d.visualization.draw_geometries([pcd_pca])
        # estimate normals
        pcd_tree = o3d.geometry.KDTreeFlann(pcd) 
        normals = []
        points_array = np.asarray(pcd.points)
        for point in pcd.points:
            [_, idx, _] = pcd_tree.search_knn_vector_3d(point, knn = 30)
            # Tuple[int, open3d.utility.IntVector, open3d.utility.DoubleVector]
            knn_points = points_array[idx, : ]
            _, now_normal = PCA(knn_points, 3)
            normals.append(now_normal)
        normals = np.array(normals, dtype=np.float64)
        pcd.normals = o3d.utility.Vector3dVector(normals)
        o3d.visualization.draw_geometries([pcd], point_show_normal=True) # visualization

体素滤波

算法原理

参考:

https://blog.csdn.net/weixin_47309740/article/details/121908709

https://blog.csdn.net/HUASHUDEYANJING/article/details/125702480?spm=1001.2014.3001.5506

  1. 获取点云坐标集合,分别获取 X , Y , Z X,Y,Z X,Y,Z 三个坐标轴上的最大值 x m a x , y m a x , z m a x x_{max},y_{max},z_{max} xmax,ymax,zmax 和最小值 x m i n , y m i n , z m i n x_{min},y_{min},z_{min} xmin,ymin,zmin

  2. 设置体素小栅格边长 r r r

  3. 依据求得的 x m a x , y m a x , z m a x x_{max},y_{max},z_{max} xmax,ymax,zmax x m i n , y m i n , z m i n x_{min},y_{min},z_{min} xmin,ymin,zmin 可知点云集合最小包围盒的边长 l x , l y , l z l_{x},l_{y},l_{z} lx,ly,lz
    { l x = x m a x − x m i n l y = y m a x − y m i n l z = z m a x − z m i n \begin{equation} \left\{ \begin{array}{lr} l_{x}=x_{max}-x_{min} & \\ l_{y}=y_{max}-y_{min} \\ l_{z}=z_{max}-z_{min} & \end{array} \right. \end{equation} lx=xmaxxminly=ymaxyminlz=zmaxzmin

  4. 计算体素网格尺寸
    { D x = ⌊ l x / r ⌋ D y = ⌊ l y / r ⌋ D z = ⌊ l y / r ⌋ \begin{equation} \left \{\begin {array}{lr} D_{x}= \lfloor l_{x} / r \rfloor & \\ D_{y}= \lfloor l_{y} / r \rfloor \\ D_{z}= \lfloor l_{y} / r \rfloor & \end{array} \right. \end{equation} Dx=lx/rDy=ly/rDz=ly/r

  5. 计算点云集合中的点在体素小栅格内的索引 h h h
    { h x = ⌊ ( x − x m i n ) / r ⌋ h y = ⌊ ( y − y m i n ) / r ⌋ h z = ⌊ ( z − z m i n ) / r ⌋ h = h x + h y ∗ D x + h z ∗ D x ∗ D y \begin{equation}\left\{\begin{array}{lr} h_{x}= \lfloor (x-x_{min})/ r \rfloor & \\ h_{y}= \lfloor (y-y_{min}) / r \rfloor & \\ h_{z}= \lfloor (z-z_{min}) / r \rfloor & \\ h=h_{x}+h_{y}*D_{x}+h_{z}*D_{x}*D_{y} \end{array}\right.\end{equation} hx=⌊(xxmin)/rhy=⌊(yymin)/rhz=⌊(zzmin)/rh=hx+hyDx+hzDxDy

  6. h h h 中的元素按从小到大的顺序进行排序(可选)

  7. 根据Centroid或Random select进行采样

其中Centroid的实现方法为:

建立两个字典 dict_voxel num_in_voxel

遍历点云,计算点云集合中的点在体素小栅格内的索引 h h h

h h h 不是 dict_voxel 中的 k e y key key,则建立 k e y key key h h h , v a l u e value value p o i n t _ c l o u d [ i ] point\_ cloud[i] point_cloud[i] 的键值对加入字典 dict_voxel ,并建立 k e y key key h h h , v a l u e value value 1 1 1 的键值对加入字典 num_in_voxel ,表示 k e y key key h h h 已经出现过一次

h h hdict_voxel 中的 k e y key key,则首先取出其在字典 dict_voxel 中的 v a l u e value value ,即此前的平均点云数据,之后取出其在字典 num_in_voxel 中的 v a l u e value value,即冲突次数,重新取平均值,修改 h h h 在字典 dict_voxel 中对应的 v a l u e value value 为新的平均值,修改 h h h 在字典 num_in_voxel 中对应的 v a l u e value value 为原先的 出现次数 + 1 出现次数+1 出现次数+1

遍历整个集合后,将字典中的所有 v a l u e value value 保存,并进行格式转换即可

其中Random select的实现方法为:

建立一个字典 dict_voxel

遍历点云,计算点云集合中的点在体素小栅格内的索引 h h h

h h h 不是 dict_voxel 中的 k e y key key,则建立 k e y key key h h h , v a l u e value value [ p o i n t _ c l o u d [ i ] ] [point\_ cloud[i]] [point_cloud[i]] 的键值对加入字典 dict_voxel ,其中 [ p o i n t _ c l o u d [ i ] ] [point\_ cloud[i]] [point_cloud[i]] 表示一个仅有 p o i n t _ c l o u d [ i ] point\_ cloud[i] point_cloud[i] 的列表

h h hdict_voxel 中的 k e y key key,则首先取出其在字典 dict_voxel 中的 v a l u e value value ,即此前的点云列表,将现在的点云数据 p o i n t _ c l o u d [ i ] point\_ cloud[i] point_cloud[i] 加入此列表中,更新 h h h 在字典 dict_voxel 中对应的 v a l u e value value 为新的列表

遍历整个集合后,根据 k e y , v a l u e key, value key,value 遍历字典 dict_voxel ,其中每一个 v a l u e value value 应为一个至少包含一个元素的列表,使用 random.choice()方法随机选择列表中的一个元素(一个点云数据),将其加入最终的结果列表中

遍历字典结束后,进行格式转换即可

核心代码

import os
import open3d as o3d
import numpy as np
import random

def data_loader():
    folder_path = "./40_sample_data/"
    files = os.listdir(folder_path)
    points_data = []
    for file in files:
        file_path = folder_path + file
        points_data.append(np.loadtxt(file_path, delimiter=","))
    return points_data

def test_data_loader():
    file_path = "./40_sample_data/person_0001.txt"
    points_data = []
    points_data.append(np.loadtxt(file_path, delimiter=","))
    return points_data

def Voxel_Grid_Downsampling(point_cloud, leaf_size=0.05, centroid_or_random='centroid'):
    '''
    input :
        point_cloud:点云数据(n,3) leaf_size:体素栅格边长 centroid_or_random:方法选择
    method:
        1. get x_min,y_min,z_min; x_max,y_max,z_max
        2. r = leaf_size
        3. cal l_x,l_y,l_z即点云最小包围盒边长
        4. 计算体素网格大小D_x,D_y,D_z
        5. 计算索引h
        6. 排序(可选)
    output:
        point_filter_res:滤波后的点云数据
    '''
    x_min,y_min,z_min = np.amin(point_cloud, axis=0)
    x_max,y_max,z_max = np.amax(point_cloud, axis=0)
    l_x = x_max - x_min
    l_y = y_max - y_min
    l_z = z_max - z_min
    D_x = l_x // leaf_size + 1
    D_y = l_y // leaf_size + 1
    D_z = l_z // leaf_size + 1

    res_point_list = []
    point_cloud_size = len(point_cloud)
    if centroid_or_random == 'centroid':
        dict_voxel = { }
        num_in_voxel = {}
        for i in range(point_cloud_size):
            h_x = (point_cloud[i,0] - x_min) // leaf_size + 1
            h_y = (point_cloud[i,1] - y_min) // leaf_size + 1
            h_z = (point_cloud[i,2] - z_min) // leaf_size + 1
            h = h_x + h_y * D_x + h_z * D_x * D_y
            if h not in dict_voxel:
                dict_voxel[h] = point_cloud[i]
                num_in_voxel[h] = 1
            else:
                avg_pos_pre = dict_voxel.get(h)
                num_pre = num_in_voxel[h]
                avg_pos_now = (avg_pos_pre * num_pre + point_cloud[i]) / (num_pre + 1)
                dict_voxel[h] = avg_pos_now
                num_in_voxel[h] = num_pre + 1
        for key,val in dict_voxel.items():
            res_point_list.append(val)
        point_filter_res = np.asarray(res_point_list, dtype=np.float64)
        return point_filter_res
    elif centroid_or_random == 'random':
        dict_voxel = { }
        for i in range(point_cloud_size):
            h_x = (point_cloud[i,0] - x_min) // leaf_size + 1
            h_y = (point_cloud[i,1] - y_min) // leaf_size + 1
            h_z = (point_cloud[i,2] - z_min) // leaf_size + 1
            h = h_x + h_y * D_x + h_z * D_x * D_y
            if h not in dict_voxel:
                dict_voxel[h] = [point_cloud[i]]
            else:
                point_list = dict_voxel.get(h)
                point_list.append(point_cloud[i])
                dict_voxel[h] = point_list
        for key, val in dict_voxel.items():
            select_point = random.choice(val)
            res_point_list.append(select_point)
        point_filter_res = np.asarray(res_point_list, dtype=np.float64)
        return point_filter_res


if __name__ == "__main__":
    pcd_list = test_data_loader()
    for pic_point in pcd_list:
        useful_point = pic_point[:, :3]
        point_filter_res = Voxel_Grid_Downsampling(useful_point, 0.05, 'random')
        pcd = o3d.geometry.PointCloud()
        pcd.points = o3d.utility.Vector3dVector(point_filter_res)
        o3d.visualization.draw_geometries([pcd])

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 计算3D点云中每个点的向量是一项常见的任务,其中一种流行的方是使用素体(Voxel-based method)。 素体是将3D点云数据离散化成一个个小立方体,称为素体(Voxel),然后计算每个素体的向量。以下是一个基本的素体计算向量的步骤: 1. 将3D点云数据划分成一组小的、相邻的立方体,每个立方体被称为一个素体。 2. 对于每个素体,找到其所有包含的点。可以通过计算每个点的坐标和素体的位置来确定点是否属于该素体。 3. 对于每个素体,将其内部的所有点的向量进行平均,得到该素体的向量。可以使用例如最小二乘PCA(主成分分析)等方进行平均。 4. 将每个素体的向量赋值给其中包含的所有点。这可以通过简单地在每个素体周围的所有点上设置相同的向量来实现。 需要注意的是,素体处理大规模点云数据时可能会遇到计算和存储问题,因为需要存储和计算大量的素体和点的向量。因此,可以采用一些技巧,例如使用不同大小的素体,或者只对点云数据的局部区域进行素体计算,以降低计算和存储的要求。 ### 回答2: 3D点云素体向量的计算通常包括以下步骤: 首先,我们需要构建3D点云的素体(voxel)数据结构。素体是一种网格化表示方,将三维空间划分为一系列小立方体单元。每个立方体单元内部可以包含零个或多个点云点。 然后,在素体中遍历每个立方体单元,计算单元内点云点的向量向量表示了该点所在曲面的方向。一般来说,可以使用最小二乘等方来拟合点云点周围区域的曲面,从而计算出该点的向量。 在计算每个立方体单元内点云点的向量时,通常会考虑该点附近的周围点。一种常用的方是使用K近邻算,选择距离该点最近的K个点,然后通过最小二乘拟合这些点的向量。 最后,将计算得到的向量赋值给对应的点云点,并可视化展示。通过计算每个点云点的向量,我们可以得到整个点云数据集的向量分布信息,用于识别曲面特征、计算点云表面的向量等应用。 综上所述,3D点云素体向量的计算主要包括构建素体数据结构、遍历素体网格计算点云点的向量、使用最小二乘等方拟合曲面并计算向量等步骤。这些计算方可以帮助我们理解和分析3D点云数据的结构和特征。 ### 回答3: 3D点云素体向量的计算可以通过以下步骤实现: 1. 首先,选择一个合适的点作为目标点,假设其坐标为P(x, y, z)。 2. 然后,选取目标点P附近的一定数量的临近点集合,可以通过计算目标点与其他点之间的欧氏距离来选择。 3. 对于选定的临近点集合,以目标点P为中心,构建一个协方差矩阵。协方差矩阵是由点云中的每一个点与目标点之间的差乘积累加而得。 4. 对协方差矩阵进行特征值分解,得到特征向量和对应的特征值。 5. 通过特征值的大小,可以确定特征向量对应的重要程度。一般来说,根据最大特征值对应的特征向量,即可得到向量。 6. 最后,可以对向量进行归一化处理,使其长度为1。 通过以上步骤,我们可以得到3D点云素体向量。这些向量可以用于诸如点云重建、表面重建、点云配准等领域的计算和应用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值