对于点云特征点提取,主要有如下一些算法:
- 基于曲率的特征点提取,对于曲率大的地方可能是特征点,比如关节、耳朵边缘
- 基于法向量变化的特征点提取,对于法向量变化大的地方就是特征点
- ISS特征点,对噪声鲁棒,适合曲面特征
- 3D SIFT,基于尺度空间,适合不同尺度的特征
- 深度学习方法,如PointNet自动学习特征
对于法向量夹角算法,首先计算点云中每个点的法向量,然后计算邻域内法向量的夹角,根据夹角的大小判断是否为特征点。其中法向量计算需要用到PCA,而邻域搜索可以使用k近邻或半径邻域。
其中的数据集来自斯坦福大学的bunny模型,利用其中的bun315.ply
数据。
下面是其关键步骤:
- 点云加载与预处理,包括利用下采样减少计算量和统计滤波去噪提高精度
- 法向量的计算,利用Open3D的
estimate_normals
计算法向量,通过orient_normals_towards_camera_location
统一法向量方向 - 特征点提取,构建KDTree进行高效的邻域搜索,并计算每个点与其邻域点的法向量夹角均值,最后根据设定的角度阈值筛选特征点,其中夹角均值越大,表明该点越可能是特征点
- 结果可视化,将原始点云显示为灰色,而特征点显示为红色,并绘制法向量夹角均值分布直方图,从而帮助确定合适的阈值
下面是其其实现代码:
import open3d as o3d
import numpy as np
import matplotlib.pyplot as plt
def load_point_cloud(file_path):
"""加载点云数据"""
pcd = o3d.io.read_point_cloud(file_path)
if pcd.is_empty():
raise ValueError("无法加载点云文件,请检查路径是否正确")
print(f"成功加载点云,包含 {len(pcd.points)} 个点")
return pcd
def preprocess_point_cloud(pcd, voxel_size=0.008):
"""
点云预处理:针对脊背调整参数
脊背细节丰富,采用较小的体素大小保留特征
"""
# 下采样:体素尺寸减小(默认0.008m),保留脊背细节
down_pcd = pcd.voxel_down_sample(voxel_size=voxel_size)
# 统计滤波:放宽去噪阈值,避免过滤掉脊背边缘点
cl, ind = down_pcd.remove_statistical_outlier(nb_neighbors=15, std_ratio=2.5)
inlier_cloud = down_pcd.select_by_index(ind)
print(f"预处理后点云包含 {len(inlier_cloud.points)} 个点")
return inlier_cloud
def extract_spine_roi(pcd, z_min_ratio=0.6):
"""
提取脊背ROI(感兴趣区域):利用高度信息筛选背部区域
脊背位于身体较高位置,通过z轴坐标过滤
"""
points = np.asarray(pcd.points)
# 计算z轴坐标范围,取上部区域作为ROI
z_max = np.max(points[:, 2])
z_min = z_max * z_min_ratio # 可根据实际数据调整比例(0.5-0.7)
roi_indices = np.where(points[:, 2] > z_min)[0]
spine_roi = pcd.select_by_index(roi_indices)
print(f"脊背ROI包含 {len(spine_roi.points)} 个点")
return spine_roi
def compute_normals(pcd, radius=0.04, max_nn=20):
"""
计算法向量:针对脊背平缓曲面调整参数
减小搜索半径和邻域点数,提高法向量对局部曲率的敏感性
"""
pcd.estimate_normals(
search_param=o3d.geometry.KDTreeSearchParamHybrid(radius=radius, max_nn=max_nn)
)
# 法向量方向统一指向身体外侧(脊背朝上,法向量向上)
pcd.orient_normals_towards_camera_location(camera_location=np.array([0, 0, z_max*1.5]))
return pcd
def extract_feature_points_by_normal_angle(pcd, k=15, angle_thresh=20):
"""
提取特征点:针对脊背调整参数
脊背曲率变化较平缓,降低角度阈值以捕捉细微特征
"""
kdtree = o3d.geometry.KDTreeFlann(pcd)
points = np.asarray(pcd.points)
normals = np.asarray(pcd.normals)
num_points = len(points)
angle_means = []
for i in range(num_points):
[_, idx, _] = kdtree.search_knn_vector_3d(points[i], k)
neighbor_normals = normals[idx[1:], :]
current_normal = normals[i, :]
# 计算夹角(脊背特征点的邻域法向量夹角较小但有明显变化)
dots = np.dot(neighbor_normals, current_normal)
dots = np.clip(dots, -1.0, 1.0)
angles = np.arccos(dots)
angle_mean = np.mean(np.rad2deg(angles))
angle_means.append(angle_mean)
# 脊背特征点的夹角均值通常比四肢小,降低阈值
angle_means = np.array(angle_means)
feature_indices = np.where(angle_means > angle_thresh)[0]
feature_pcd = pcd.select_by_index(feature_indices)
print(f"从脊背ROI中提取到 {len(feature_pcd.points)} 个特征点")
return feature_pcd, angle_means
def visualize_results(original_pcd, roi_pcd, feature_pcd):
"""可视化:区分原始点云、ROI和特征点"""
original_pcd.paint_uniform_color([0.8, 0.8, 0.8]) # 原始点云:浅灰
roi_pcd.paint_uniform_color([0.5, 0.5, 0.5]) # 脊背ROI:深灰
feature_pcd.paint_uniform_color([1.0, 0.0, 0.0]) # 特征点:红色
o3d.visualization.draw_geometries(
[original_pcd, roi_pcd, feature_pcd],
window_name="脊背特征点提取结果",
width=1200,
height=800,
point_show_normal=False
)
def plot_angle_distribution(angle_means, angle_thresh):
"""绘制脊背区域的夹角分布,辅助调整阈值"""
plt.figure(figsize=(10, 6))
plt.hist(angle_means, bins=30, alpha=0.7, color='green')
plt.axvline(x=angle_thresh, color='r', linestyle='--', label=f'Threshold: {angle_thresh}°')
plt.xlabel('Mean Angle')
plt.ylabel('Points')
plt.title('Distribution')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()
def main():
# 针对脊背的参数设置(单位:米)
file_path = "bun315.ply" # 点云文件路径
voxel_size = 0.008 # 体素大小:较小值保留脊背细节
z_min_ratio = 0.65 # 脊背ROI的z轴下限比例
normal_radius = 0.04 # 法向量计算半径:适应脊背宽度
k_neighbors = 15 # 邻域点数:减少平滑效应
angle_threshold = 22 # 夹角阈值:降低以捕捉平缓变化
try:
# 1. 加载点云
pcd = load_point_cloud(file_path)
global z_max # 全局变量,用于法向量方向调整
z_max = np.max(np.asarray(pcd.points)[:, 2])
# 2. 预处理(保留细节)
processed_pcd = preprocess_point_cloud(pcd, voxel_size)
# 3. 提取脊背ROI(关键步骤:减少无关区域干扰)
spine_roi = extract_spine_roi(processed_pcd, z_min_ratio)
# 4. 计算法向量(适应脊背曲面)
spine_roi = compute_normals(spine_roi, normal_radius)
# 5. 提取脊背特征点
feature_pcd, angle_means = extract_feature_points_by_normal_angle(
spine_roi,
k=k_neighbors,
angle_thresh=angle_threshold
)
# 6. 保存结果
o3d.io.write_point_cloud("spine_feature_points.ply", feature_pcd)
print("脊背特征点已保存为 pig_spine_feature_points.ply")
# 7. 可视化
visualize_results(processed_pcd, spine_roi, feature_pcd)
# 8. 分析夹角分布
plot_angle_distribution(angle_means, angle_threshold)
except Exception as e:
print(f"处理过程中发生错误: {str(e)}")
其输出如下:
成功加载点云,包含 35336 个点 预处理后点云包含 587 个点 脊背ROI包含 198 个点 从脊背ROI中提取到 74 个特征点
其最终效果如下:
从上图可以看出,我们成功将其兔子脊背特征点标注了出来。
如果喜欢这篇文章或对您有帮助,可以:[☕] 请我喝杯咖啡 | [💓] 小额赞助


