一、核PCA

核PCA的核心思想是将高维空间的运算转化为低维空间的核函数,这一思想称为kernel trick。

1.法向量

法向量指用曲面拟合点云数据时,曲面上任意点切平面的垂直向量。其应用场景包括:

  • 平面检测:例如自动驾驶中地面法向量通常朝上
  • 分割算法:圆柱体底面法向量朝左右,侧面法向量朝上下,可通过法向量差异实现部件分离
    在这里插入图片描述
1) 法向量的计算

法向量计算是PCA的逆向应用

  • 选取目标点及其邻域点(需定义邻域范围)
  • 对邻域点进行PCA分析
  • 最小特征值对应的特征向量作为法向量,PCA求最大特征值,法向量求最小特征值
  • 曲率计算:最小特征值除以特征值总和
  • 数学本质:寻找使点投影方差最小的向量
2) 法向量估计的定义

法向量估计的数学定义为:

  • 输入:N维空间点集

  • 目标:寻找平面使各点投影到法向量n的数值之和最小
    在这里插入图片描述
    在这里插入图片描述

  • 约束条件:

    • 平面中心点c为点集均值
    • 数据需中心化处理(xq = x - c)
      在这里插入图片描述
6) 法向量估计

实际应用中存在两大挑战:

  • 噪声干扰:点云数据存在波动,难以完美拟合平面

  • 数据稀疏性:邻域内有效点数不足(如5-10个点/立方米)

  • 选领域
    邻域选择的影响因素:

    邻域半径 优势 劣势
    较大 抗噪性强,法向量平滑 丢失精细结构(如桌角特征)
    较小 保留局部特征 易受噪声干扰
  • 加权法向量估算

加权法向量估计方法:

  • 权重设计:基于颜色/反射率相似度构建对角权重矩阵W

  • 优化目标:min nᵀXWXᵀn

  • 求解方法:计算XWXᵀ的最小特征值对应特征向量

  • 关键点:权重矩阵设计直接影响估计精度

二、PCA Normal 算法代码详细解释

这份代码实现了一个基于 PCA(主成分分析)的点云法向量计算算法,使用 Python 编写。主要用于分析 3D 点云的主方向和计算每个点的表面法向量。代码依赖 NumPy、Open3D 和 PyntCloud 库,用于点云处理和可视化。下面我逐段解释代码的含义,包括每部分的功能和逻辑。

1. 导入语句
import argparse
import os
import numpy as np
import open3d as o3d 
from pyntcloud import PyntCloud
  • argparse: 用于解析命令行参数(e.g., 输入文件路径)。
  • os: 操作系统接口(代码中未直接使用,但可能用于文件操作)。
  • numpy as np: 数值计算库,用于矩阵操作、特征值分解等。
  • open3d as o3d: Open3D 库,用于 3D 数据处理、可视化和 KD 树搜索。
  • from pyntcloud import PyntCloud: PyntCloud 库,用于点云数据加载和转换。

这些库提供了点云处理的基础工具。

2. PCA 函数(主成分分析)
def PCA(data, correlation=False, sort=True):
    """ Calculate PCA for given point cloud

    Parameters
    ----------
        data:点云,NX3的矩阵
        correlation:区分np的cov和corrcoef,不输入时默认为False
        sort: 特征值排序,排序是为了其他功能方便使用,不输入时默认为True
    
    Returns
    ----------
        eigenvalues:特征值
        eigenvectors:特征向量
    """

    # TODO 01: PCA
    # format as numpy array:
    N = data.shape[0]
    X = data.to_numpy()

    # normalize by center:
    mu = np.mean(X, axis=0)
    X_normalized = X - mu

    # get function:
    func = np.cov if not correlation else np.corrcoef
    H = func(X_normalized, rowvar=False, bias=True)

    # get eigen pairs:
    eigenvalues, eigenvectors = np.linalg.eig(H)

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

    return eigenvalues, eigenvectors
  • 目的: 对点云数据进行 PCA,提取主成分(特征值和特征向量)。
  • 输入:
    • data: Pandas DataFrame,N×3 的点云数据(N 个点,每个 3 维)。
    • correlation: 布尔值,选择协方差矩阵 (np.cov) 或相关系数矩阵 (np.corrcoef)。
    • sort: 布尔值,是否按特征值降序排序。
  • 步骤:
    • 将 DataFrame 转换为 NumPy 数组。
    • 中心化数据(减去均值)。
    • 计算协方差矩阵(或相关矩阵)。
    • 特征值分解,得到特征值和特征向量。
    • 如果排序,按特征值降序排列(主成分优先)。
  • 返回: 特征值数组和特征向量矩阵(3×3)。
3. get_pca_o3d 函数(PCA 可视化几何)
def get_pca_o3d(w, v, points):
    """ Build open3D geometry for PCA

    Parameters
    ----------
        w: eigenvalues in descending order
        v: eigenvectors in descending order
    
    Returns
    ----------
        pca_set: o3d line set for pca visualization
    """
    # calculate centroid & variation along main axis:
    centroid = points.mean()
    projs = np.dot(points.to_numpy(), v[:,0])
    scale = projs.max() - projs.min()

    points = centroid.to_numpy() + np.vstack(
        (
            np.asarray([0.0, 0.0, 0.0]),
            scale * v.T
        )
    ).tolist()
    lines = [
        [0, 1],
        [0, 2],
        [0, 3]
    ]
    # from the largest to the smallest: RGB
    colors = np.identity(3).tolist()

    # build pca line set:
    pca_o3d = o3d.geometry.LineSet(
        points=o3d.utility.Vector3dVector(points),
        lines=o3d.utility.Vector2iVector(lines),
    )
    pca_o3d.colors = o3d.utility.Vector3dVector(colors)

    return pca_o3d
  • 目的: 创建 Open3D 几何对象,用于可视化 PCA 主成分(三个轴)。
  • 输入:
    • w: 特征值(降序)。
    • v: 特征向量(降序)。
    • points: 点云 DataFrame。
  • 步骤:
    • 计算质心(均值)。
    • 计算沿主轴的投影范围,作为尺度。
    • 生成线段端点:质心 + 尺度 * 特征向量(三个方向)。
    • 定义线段连接(从质心到每个轴端点)。
    • 颜色:RGB 对应三个轴。
  • 返回: Open3D LineSet 对象,用于可视化。
4. get_surface_normals 函数(计算法向量)
def get_surface_normals(pcd, points, knn=5):
    # create search tree:
    pcd_tree = o3d.geometry.KDTreeFlann(pcd)

    # init:
    N = len(pcd.points)
    normals = []

    for i in range(N):
        # find knn:
        [k, idx, _] = pcd_tree.search_knn_vector_3d(pcd.points[i], knn)
        # get normal:
        w, v = PCA(points.iloc[idx])
        normals.append(v[:, 0])

    return np.array(normals, dtype=np.float64)
  • 目的: 为每个点计算表面法向量,使用局部 PCA。
  • 输入:
    • pcd: Open3D 点云对象。
    • points: Pandas DataFrame 点云。
    • knn: K 近邻数量(默认 5)。
  • 步骤:
    • 创建 KD 树用于快速邻域搜索。
    • 对每个点:
      • 找到 K 个最近邻。
      • 对邻域点进行 PCA,取最小特征值对应的特征向量作为法向量(假设表面是平坦的,最小方差方向是法线)。
    • 收集所有法向量。
  • 返回: N×3 的法向量数组。
5. get_surface_normals_o3d 函数(法向量可视化几何)
def get_surface_normals_o3d(normals, points, scale=2):
    """ Build open3D geometry for surface normals

    Parameters
    ----------
        normals(numpy.ndarray): surface normals for each point
        points(pandas.DataFrame): points in the point cloud
        scale(float): the length of each surface normal vector

    Returns
    ----------
        surface_normals_o3d: o3d line set for surface normal visualization
    """
    # total number of points:
    N = points.shape[0]

    points = np.vstack(
        (points.to_numpy(), points.to_numpy() + scale * normals)
    )
    lines = [[i, i+N] for i in range(N)]
    colors = np.zeros((N, 3)).tolist()

    # build pca line set:
    surface_normals_o3d = o3d.geometry.LineSet(
        points=o3d.utility.Vector3dVector(points),
        lines=o3d.utility.Vector2iVector(lines),
    )
    surface_normals_o3d.colors = o3d.utility.Vector3dVector(colors)

    return surface_normals_o3d
  • 目的: 创建 Open3D 几何对象,用于可视化法向量(从每个点出发的线段)。
  • 输入:
    • normals: 法向量数组。
    • points: 点云 DataFrame。
    • scale: 法向量长度缩放(默认 2)。
  • 步骤:
    • 堆叠点和点 + 缩放法向量作为线段端点。
    • 定义线段(每个点到其法向量端点)。
    • 颜色:全黑。
  • 返回: Open3D LineSet 对象。
6. main 函数(主逻辑)
def main(point_cloud_filename):
    # load point cloud:
    point_cloud_pynt = PyntCloud.from_file(point_cloud_filename)
    point_cloud_o3d = point_cloud_pynt.to_instance("open3d", mesh=False)

    # 从点云中获取点,只对点进行处理
    points = point_cloud_pynt.points
    print('[PCA Normal]: Total number of points:', points.shape[0])

    # 用PCA分析点云主方向
    w, v = PCA(points)
    point_cloud_vector = v[:, 2] #点云主方向对应的向量
    print('the main orientation of this pointcloud is: ', point_cloud_vector)
    # get PCA geometry:
    pca_o3d = get_pca_o3d(w, v, points)
    
    # TODO 02:
    # 循环计算每个点的法向量
    normals = get_surface_normals(point_cloud_o3d, points)
    # 此处把法向量存放在了normals中
    point_cloud_o3d.normals = o3d.utility.Vector3dVector(normals)
    # get surface normals geometry:
    surface_normals_o3d = get_surface_normals_o3d(normals, points)
    
    # visualize point clouds with PCA and surface normals:
    o3d.visualization.draw_geometries([point_cloud_o3d, pca_o3d, surface_normals_o3d])
  • 目的: 主函数,加载点云、计算 PCA 和法向量、可视化。
  • 输入: 点云文件路径(PLY 格式)。
  • 步骤:
    • 加载点云(PyntCloud 和 Open3D)。
    • 提取点数据。
    • 计算全局 PCA,打印主方向向量。
    • 计算每个点的法向量。
    • 设置 Open3D 点云的法向量属性。
    • 创建可视化几何。
    • 显示点云、PCA 轴和法向量。
7. get_arguments 函数(命令行参数解析)
def get_arguments():
    """ Get command-line arguments
    """

    # init parser:
    parser = argparse.ArgumentParser("Get PCA and surface normals for given point cloud.")

    # add required and optional groups:
    required = parser.add_argument_group('Required')

    # add required:
    required.add_argument(
        "-i", dest="input", help="Input path of point cloud in ply format.",
        required=True
    )

    # parse arguments:
    return parser.parse_args()
  • 目的: 解析命令行参数。
  • 逻辑: 使用 argparse 定义必需参数 -i(输入文件路径)。
  • 返回: 解析后的参数对象。
8. 主程序(入口点)
if __name__ == '__main__':
    # parse arguments:
    arguments = get_arguments()

    main(arguments.input)
  • 目的: 脚本入口,解析参数并调用 main 函数。
  • 逻辑: 如果作为脚本运行,获取输入文件路径,执行主逻辑。
Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐