三维点云处理 1.4 PCA应用_法向量计算
·
一、核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 函数。
- 逻辑: 如果作为脚本运行,获取输入文件路径,执行主逻辑。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐



所有评论(0)