Clustered Viewpoint Feature Histogram(CVFH)是 一种全局点云描述子,主要用于物体识别与姿态估计。它是 Viewpoint Feature Histogram(VFH) 的扩展,旨在解决物体形状复杂时 VFH 产生的模糊性问题。CVFH 通过对点云进行分簇处理,将复杂物体表面分解为若干个“形状稳定区域”,从而提取更鲁棒、区分度更高的全局特征。
一、CVFH 算法原理概述
CVFH 的核心思路是将点云按照曲率变化分成多个区域,每个区域计算一个局部 VFH 描述子,最终形成整个物体的多个描述子集合,从而达到更稳定的描述效果。
CVFH 包括以下关键步骤:
- 估计法线和曲率
- 确定视角方向
- 分割物体点云为多个区域(通过曲率分割 + 连通性)
- 为每个区域计算一个 VFH 描述子
- 对所有区域的描述子进行聚类或编码,形成整体描述
1、数学公式与推导细节
1.1、 法线与曲率估计
使用 PCA 对点邻域 N ( p i ) \mathcal{N}(p_i) N(pi) 求协方差矩阵:
C = 1 ∣ N ∣ ∑ q ∈ N ( q − q ˉ ) ( q − q ˉ ) T \mathbf{C} = \frac{1}{|\mathcal{N}|} \sum_{q \in \mathcal{N}} (q - \bar{q})(q - \bar{q})^T C=∣N∣1q∈N∑(q−qˉ)(q−qˉ)T
其中 q ˉ \bar{q} qˉ 是邻域点的均值。对 C \mathbf{C} C 做特征值分解,取最小特征值对应的特征向量为法线 n i \mathbf{n}_i ni。
表面曲率 κ \kappa κ 可由特征值估计:
κ i = λ 0 λ 0 + λ 1 + λ 2 \kappa_i = \frac{\lambda_0}{\lambda_0 + \lambda_1 + \lambda_2} κi=λ0+λ1+λ2λ0
1.2、 视点方向确定
视点(camera position)设为 v p \mathbf{v}_p vp,对于每个点 p i p_i pi,定义其 视点方向向量:
v i = v p − p i \mathbf{v}_i = \mathbf{v}_p - \mathbf{p}_i vi=vp−pi
用于计算 VFH 的角度特征。
1.3、分割为多个区域(Clustering)
使用曲率变化与法线夹角来划分点云区域。两点 p i p_i pi、 p j p_j pj 之间是否属于同一区域的判断依据:
法线夹角小于阈值 θ \theta θ:
arccos ( n i ⋅ n j ) < θ \arccos(\mathbf{n}_i \cdot \mathbf{n}_j) < \theta arccos(ni⋅nj)<θ
曲率差值小于阈值
并通过 区域增长算法(Region Growing) 合并满足条件的邻近点,形成多个“稳定区域” { R 1 , R 2 , … , R k } \{ R_1, R_2, \dots, R_k \} {R1,R2,…,Rk}。
1.4、 对每个区域计算 VFH
VFH 是对 每个点对其中心点(质心)和法线之间的相对几何信息进行统计构建直方图。
对于区域 R j R_j Rj,计算参考方向:
- u = n c \mathbf{u} = \mathbf{n}_c u=nc(质心法线)
- v = ( p i − c ) × u \mathbf{v} = (\mathbf{p}_i - \mathbf{c}) \times \mathbf{u} v=(pi−c)×u
- w = u × v \mathbf{w} = \mathbf{u} \times \mathbf{v} w=u×v
对于区域内每个点 p i p_i pi,其法线为 n i \mathbf{n}_i ni,计算以下角度特征:
- α = arccos ( v ⋅ n i ) \alpha = \arccos(\mathbf{v} \cdot \mathbf{n}_i) α=arccos(v⋅ni)
- ϕ = arccos ( u ⋅ p i − c ∥ p i − c ∥ ) \phi = \arccos(\mathbf{u} \cdot \frac{\mathbf{p}_i - \mathbf{c}}{\|\mathbf{p}_i - \mathbf{c}\|}) ϕ=arccos(u⋅∥pi−c∥pi−c)
- θ = arctan 2 ( w ⋅ n i , u ⋅ n i ) \theta = \arctan2(w \cdot \mathbf{n}_i, u \cdot \mathbf{n}_i) θ=arctan2(w⋅ni,u⋅ni)
每个特征被量化成直方图,再拼接成一个局部描述子。
1.5、 聚类后的全局特征
将所有区域的 VFH 拼接为一组:
CVFH = { VFH R 1 , VFH R 2 , … , VFH R k } \text{CVFH} = \{ \text{VFH}_{R_1}, \text{VFH}_{R_2}, \dots, \text{VFH}_{R_k} \} CVFH={VFHR1,VFHR2,…,VFHRk}
可以使用 聚类中心 + 直方图统计 或在检索时将每个子 VFH 与数据库中所有子 VFH 比较取最小距离,形成鲁棒匹配方式。
2、CVFH 与 VFH 的对比
特性 | VFH | CVFH |
---|---|---|
视角敏感性 | 高 | 低 |
对形状复杂性适应 | 差 | 好(通过区域分割) |
特征数量 | 单一全局描述子 | 多个区域子描述子 |
计算复杂度 | 较低 | 较高(需区域分割 + 多次计算) |
适用场景 | 简单对象识别 | 复杂物体识别、姿态估计 |
3、CVFH 算法实现流程
1. 估计每个点的法线与曲率(使用 PCL::NormalEstimation)
2. 提取物体的边界轮廓(可使用 PCL::Boundary)
3. 基于法线夹角和曲率变化进行区域分割(区域增长)
4. 对每个区域:
a. 计算区域质心及法线
b. 计算角度特征 α、φ、θ
c. 构建直方图(通常 3 个角度 × 45 bin)
5. 将所有区域描述子组合,构成最终 CVFH
二、成员变量和成员函数
1、成员变量解析(private & protected)
成员变量名 | 类型 | 含义 |
---|---|---|
float vpx_, vpy_, vpz_ |
float |
视点位置(Viewpoint),用于描述观察点(假设为针孔相机模型)的位置。 |
float leaf_size_ |
float |
用于体素滤波的体素大小。注意:应与训练数据一致,否则必须启用 normalize_bins_。 |
bool normalize_bins_ |
bool |
是否对最终的 CVFH 直方图进行归一化(默认:false )。 |
float curv_threshold_ |
float |
曲率阈值。用于剔除曲率过大的法向量(表示边界或噪声)。 |
float cluster_tolerance_ |
float |
欧氏聚类的距离阈值,控制点云中点间的最大聚类距离。 |
float eps_angle_threshold_ |
float |
法线夹角阈值(弧度)。超过该夹角的法向量不被认为属于同一聚类。 |
std::size_t min_points_ |
size_t |
聚类中最小点数量,低于此数量的聚类会被舍弃。 |
float radius_normals_ |
float |
法向量估计时的搜索半径。 |
std::vector<Eigen::Vector3f> centroids_dominant_orientations_ |
向量 | 每个有效聚类的中心点,用于描述 CVFH 的方向特征。 |
std::vector<Eigen::Vector3f> dominant_normals_ |
向量 | 每个聚类的法向量中心,用于后续分析。 |
2、成员函数解析(public)
1. 构造函数
CVFHEstimation()
初始化各参数,包括聚类容差、曲率阈值、法线估计半径等,并设置 feature_name_ = "CVFHEstimation"
。
2. 法线剔除函数
filterNormalsWithHighCurvature(...)
过滤掉曲率大于设定阈值的法线点(可能是边缘或噪声)。
3. 视点相关函数
setViewPoint(float x, float y, float z)
getViewPoint(float &x, float &y, float &z)
设置和获取视点坐标。视点会影响特征的方向性判断。
4. 参数设置函数
setRadiusNormals(float r)
setClusterTolerance(float d)
setEPSAngleThreshold(float d)
setCurvatureThreshold(float d)
setMinPoints(std::size_t min)
setNormalizeBins(bool normalize)
设置各类算法参数,包括聚类阈值、法线计算半径、法线夹角阈值、是否进行归一化等。
5. 输出聚类信息
getCentroidClusters(std::vector<Eigen::Vector3f> & centroids)
getCentroidNormalClusters(std::vector<Eigen::Vector3f> & centroids)
输出当前计算中每个聚类的几何质心和法向量质心。
6. 特征计算函数(重载)
compute(PointCloudOut &output)
调用基类特征接口,外部调用入口。
computeFeature(PointCloudOut &output) override
核心函数,内部实现 CVFH 特征计算流程,包括体素滤波、聚类、法线角度判断、特征直方图生成等。由 compute()
调用。
7.私有辅助函数(private)
extractEuclideanClustersSmooth(...)
基于欧氏距离 + 法线夹角约束的区域生长聚类方法,结合 KdTree
搜索实现,用于将点云划分为多个平滑的小区域,作为后续 CVFH 特征的输入。
8.继承关系说明
template<typename PointInT, typename PointNT, typename PointOutT = pcl::VFHSignature308>
class CVFHEstimation : public FeatureFromNormals<PointInT, PointNT, PointOutT>
继承自 FeatureFromNormals
,意味着该类:
- 支持基于法向量的特征估计(需要输入 normals)。
- 遵循 PCL 的
compute()
/computeFeature()
调用框架。 - 默认输出的特征类型为
pcl::VFHSignature308
(即 308 维的 CVFH 描述子)。
使用流程小结:
- 调用
setInputCloud
、setInputNormals
、setSearchSurface
等设置输入。 - 设置参数(视点、聚类阈值、法线半径等)。
- 调用
compute(output)
。 - 获得输出的
PointCloud<pcl::VFHSignature308>
。
三、关键部分代码注释
1、平滑欧几里得聚类(Smooth Euclidean Clustering)实现,用于结合法向量信息进行点云分割
// 定义模板函数,用于从点云中提取带有法向量平滑性的欧几里得聚类
template<typename PointInT, typename PointNT, typename PointOutT>
void pcl::CVFHEstimation<PointInT, PointNT, PointOutT>::extractEuclideanClustersSmooth(
const pcl::PointCloud<pcl::PointNormal> &cloud, // 输入点云,包含点和法向量
const pcl::PointCloud<pcl::PointNormal> &normals, // 法向量点云(通常与 cloud 相同)
float tolerance, // 聚类时邻域搜索半径
const pcl::search::Search<pcl::PointNormal>::Ptr &tree, // 用于加速搜索的空间索引树(如 kd-tree)
std::vector<pcl::PointIndices> &clusters, // 输出的聚类结果
double eps_angle, // 法向量之间的最大夹角(弧度),用于判断平滑性
unsigned int min_pts_per_cluster, // 每个聚类最小点数
unsigned int max_pts_per_cluster) // 每个聚类最大点数
输入合法性检查
if (tree->getInputCloud ()->points.size () != cloud.points.size ())
{
// 搜索树的输入点云与当前输入点云大小不一致,报错返回
PCL_ERROR ("Tree built for a different point cloud dataset (%lu) than the input cloud (%lu)!\n",
tree->getInputCloud ()->points.size (), cloud.points.size ());
return;
}
if (cloud.points.size () != normals.points.size ())
{
// 点云与法向量数量不一致,报错返回
PCL_ERROR ("Number of points in the input point cloud (%lu) different than normals (%lu)!\n",
cloud.points.size (), normals.points.size ());
return;
}
初始化处理标记
// 初始化布尔数组,标记哪些点已被处理,初始为 false(未处理)
std::vector<bool> processed (cloud.points.size (), false);
临时变量声明
std::vector<int> nn_indices; // 邻域内点的索引
std::vector<float> nn_distances; // 邻域内点的距离
遍历所有点
for (std::size_t i = 0; i < cloud.points.size (); ++i)
{
if (processed[i]) // 如果当前点已处理,跳过
continue;
processed[i] = true; // 否则标记为已处理
pcl::PointIndices r; // 存储当前聚类点索引
r.header = cloud.header;
auto& seed_queue = r.indices; // 聚类种子队列,初始包含当前点
seed_queue.push_back (i);
聚类扩展循环(类似 BFS)
for (std::size_t idx = 0; idx != seed_queue.size (); ++idx)
{
// 对当前队列中的点,进行半径搜索找邻居
if (!tree->radiusSearch (seed_queue[idx], tolerance, nn_indices, nn_distances))
{
continue; // 没有邻居,跳过
}
// 从第一个邻居开始(索引0是自身,可略过)
for (std::size_t j = 1; j < nn_indices.size (); ++j)
{
if (processed[nn_indices[j]])
continue; // 已处理的点跳过
// 计算两个法向量之间的夹角(点积后取 arccos)
const double dot_p = normals.points[seed_queue[idx]].getNormalVector3fMap().dot(
normals.points[nn_indices[j]].getNormalVector3fMap());
if (std::acos(dot_p) < eps_angle) // 如果夹角小于阈值,认为法向量方向相近(平滑)
{
processed[nn_indices[j]] = true; // 标记为已处理
seed_queue.emplace_back(nn_indices[j]); // 加入队列继续扩展
}
}
}
判断并保存聚类结果
// 检查聚类大小是否满足设定的范围要求
if (seed_queue.size () >= min_pts_per_cluster && seed_queue.size () <= max_pts_per_cluster)
{
std::sort (r.indices.begin (), r.indices.end ());
r.indices.erase (std::unique (r.indices.begin (), r.indices.end ()), r.indices.end ());
// 将该聚类加入最终聚类列表,使用 std::move 避免复制
clusters.emplace_back (std::move(r));
}
}
小结说明
这个函数是 pcl::CVFHEstimation
类中的一个聚类辅助函数,其作用是结合点的位置关系与法向量方向的平滑性,进行基于区域增长的聚类(Region Growing)。其核心逻辑是:
- 利用
radiusSearch
在一定半径内找邻居; - 仅当邻居点的法向量与当前点方向相近(小于
eps_angle
)时才纳入聚类; - 最终将符合大小要求的点集合加入
clusters
。
2、过滤曲率过高的法向量点
// 模板函数定义,适用于任意类型的输入点、法向量点和输出点
template<typename PointInT, typename PointNT, typename PointOutT>
void pcl::CVFHEstimation<PointInT, PointNT, PointOutT>::filterNormalsWithHighCurvature (
const pcl::PointCloud<PointNT> & cloud, // 输入:包含法向量和曲率的点云(如 PointNormal 或 Normal)
std::vector<int> &indices_to_use, // 输入:需要检查的点索引列表
std::vector<int> &indices_out, // 输出:曲率高于阈值的点的索引(被“剔除”的点)
std::vector<int> &indices_in, // 输出:曲率低于或等于阈值的点的索引(保留的点)
float threshold) // 曲率阈值,超过该值则认为是高曲率点
初始化输出向量大小与计数器
indices_out.resize (cloud.points.size ());
indices_in.resize (cloud.points.size ());
std::size_t in, out;
in = out = 0;
- 预分配
indices_out
和indices_in
的容量为最大可能值(全部点),防止频繁 realloc; - 使用
in
和out
两个计数器分别记录当前写入的索引数量。
遍历所有要处理的索引
for (const int &index : indices_to_use)
{
if (cloud.points[index].curvature > threshold)
{
// 当前点的曲率大于阈值,认为是不规则或边缘点,放入剔除集合
indices_out[out] = index;
out++;
}
else
{
// 曲率小于等于阈值,认为是平滑区域,保留
indices_in[in] = index;
in++;
}
}
- 利用
curvature
属性对每个点判断; - 一般高曲率对应于边缘、不规则区域;
- 平滑区域(低曲率)通常更适合用于特征估计或主方向提取。
缩减输出向量大小
indices_out.resize (out);
indices_in.resize (in);
- 清除预分配过程中未使用的尾部元素;
- 保证
indices_out
和indices_in
中的大小刚好等于有效元素个数。
函数作用:
将给定点集根据曲率阈值进行二分类,分为:
- 高曲率点:通常用于剔除异常/边界点;
- 低曲率点:用于保留用于特征估计或聚类的稳定点。
3、计算所点的特征
template<typename PointInT, typename PointNT, typename PointOutT>
void pcl::CVFHEstimation<PointInT, PointNT, PointOutT>::computeFeature(PointCloudOut &output)
{
// 检查法向量输入是否存在
if (!normals_)
{
PCL_ERROR ("[pcl::%s::computeFeature] No input dataset containing normals was given!\n", getClassName ().c_str ());
output.width = output.height = 0;
output.points.clear ();
return;
}
// 检查法向量点数量是否与输入点云数量一致
if (normals_->points.size () != surface_->points.size ())
{
PCL_ERROR ("[pcl::%s::computeFeature] The number of points in the input dataset differs from the number of points in the dataset containing the normals!\n", getClassName ().c_str ());
output.width = output.height = 0;
output.points.clear ();
return;
}
// 清空之前计算的主方向质心结果
centroids_dominant_orientations_.clear ();
// ---[ 第0步:根据曲率阈值过滤法线(排除高曲率区域)
std::vector<int> indices_out, indices_in;
filterNormalsWithHighCurvature(*normals_, *indices_, indices_out, indices_in, curv_threshold_);
// 构建过滤后(低曲率)的点云
pcl::PointCloud<pcl::PointNormal>::Ptr normals_filtered_cloud(new pcl::PointCloud<pcl::PointNormal>());
normals_filtered_cloud->width = static_cast<std::uint32_t>(indices_in.size());
normals_filtered_cloud->height = 1;
normals_filtered_cloud->points.resize(normals_filtered_cloud->width);
for (std::size_t i = 0; i < indices_in.size(); ++i)
{
normals_filtered_cloud->points[i].x = surface_->points[indices_in[i]].x;
normals_filtered_cloud->points[i].y = surface_->points[indices_in[i]].y;
normals_filtered_cloud->points[i].z = surface_->points[indices_in[i]].z;
}
std::vector<pcl::PointIndices> clusters;
// 如果过滤后的点数大于设定的最小数量,执行基于法线的欧式聚类
if(normals_filtered_cloud->points.size() >= min_points_)
{
// 重新估计法线(对平滑性聚类更稳健)
KdTreePtr normals_tree_filtered(new pcl::search::KdTree<pcl::PointNormal>(false));
normals_tree_filtered->setInputCloud(normals_filtered_cloud);
pcl::NormalEstimation<PointNormal, PointNormal> n3d;
n3d.setRadiusSearch(radius_normals_);
n3d.setSearchMethod(normals_tree_filtered);
n3d.setInputCloud(normals_filtered_cloud);
n3d.compute(*normals_filtered_cloud);
// 对估计法线的点云进行平滑聚类(角度 + 距离)
KdTreePtr normals_tree(new pcl::search::KdTree<pcl::PointNormal>(false));
normals_tree->setInputCloud(normals_filtered_cloud);
extractEuclideanClustersSmooth(
*normals_filtered_cloud,
*normals_filtered_cloud,
cluster_tolerance_,
normals_tree,
clusters,
eps_angle_threshold_,
static_cast<unsigned int>(min_points_));
}
// 配置 VFH 特征估计器
VFHEstimator vfh;
vfh.setInputCloud(surface_);
vfh.setInputNormals(normals_);
vfh.setIndices(indices_);
vfh.setSearchMethod(this->tree_);
vfh.setUseGivenNormal(true);
vfh.setUseGivenCentroid(true);
vfh.setNormalizeBins(normalize_bins_);
vfh.setNormalizeDistance(true);
vfh.setFillSizeComponent(true);
output.height = 1;
// ---[ 第1步:判断是否聚类成功
if (!clusters.empty())
{
// ---[ 1.1 对每个主方向聚类,计算平均法线和质心
for (const auto &cluster : clusters)
{
Eigen::Vector4f avg_normal = Eigen::Vector4f::Zero();
Eigen::Vector4f avg_centroid = Eigen::Vector4f::Zero();
for (const auto &index : cluster.indices)
{
avg_normal += normals_filtered_cloud->points[index].getNormalVector4fMap();
avg_centroid += normals_filtered_cloud->points[index].getVector4fMap();
}
avg_normal /= static_cast<float>(cluster.indices.size());
avg_centroid /= static_cast<float>(cluster.indices.size());
Eigen::Vector4f centroid_test;
pcl::compute3DCentroid(*normals_filtered_cloud, centroid_test);
avg_normal.normalize();
Eigen::Vector3f avg_norm(avg_normal[0], avg_normal[1], avg_normal[2]);
Eigen::Vector3f avg_dominant_centroid(avg_centroid[0], avg_centroid[1], avg_centroid[2]);
// 保存主方向法线与质心
dominant_normals_.push_back(avg_norm);
centroids_dominant_orientations_.push_back(avg_dominant_centroid);
}
// ---[ 1.2 针对每个主方向,计算对应的 CVFH 特征
output.points.resize(dominant_normals_.size());
output.width = static_cast<std::uint32_t>(dominant_normals_.size());
for (std::size_t i = 0; i < dominant_normals_.size(); ++i)
{
vfh.setNormalToUse(dominant_normals_[i]);
vfh.setCentroidToUse(centroids_dominant_orientations_[i]);
pcl::PointCloud<pcl::VFHSignature308> vfh_signature;
vfh.compute(vfh_signature);
output.points[i] = vfh_signature.points[0];
}
}
else
{
// ---[ 1.3 若未聚类成功,则用整体点云估算 CVFH
Eigen::Vector4f avg_centroid;
pcl::compute3DCentroid(*surface_, avg_centroid);
Eigen::Vector3f cloud_centroid(avg_centroid[0], avg_centroid[1], avg_centroid[2]);
centroids_dominant_orientations_.push_back(cloud_centroid);
vfh.setCentroidToUse(cloud_centroid);
vfh.setUseGivenNormal(false); // 不使用预设法线
pcl::PointCloud<pcl::VFHSignature308> vfh_signature;
vfh.compute(vfh_signature);
output.points.resize(1);
output.width = 1;
output.points[0] = vfh_signature.points[0];
}
}
总结逻辑流程:
检查输入合法性(法线是否存在、数量是否匹配)。
过滤高曲率点(可提高稳定性)。
对低曲率区域重新估计法线并聚类。
对每个主方向聚类:
- 计算平均法线与质心。
- 使用该主方向进行 VFH 特征提取。
若未聚类成功,对整个点云估算 CVFH 特征。
将所有 CVFH 特征写入
output
。
四、使用代码示例
/*****************************************************************//**
* \file PCLLearnCVFHFeathermain.cpp
* \brief
*
* \author YZS
* \date Jun 2025
*********************************************************************/
#include <iostream>
#include <pcl/io/pcd_io.h>
#include <pcl/point_types.h>
#include <pcl/features/cvfh.h>
#include <pcl/features/normal_3d.h>
#include <pcl/search/kdtree.h>
int TestCVFH(int argc, char** argv)
{
// 加载点云
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);
std::string file = "E:/db_data/PCLLearnData/cloud_bin_1.pcd";
if (pcl::io::loadPCDFile<pcl::PointXYZ>(file, *cloud) == -1)
{
PCL_ERROR("Couldn't read input file input.pcd\n");
return -1;
}
// 法线估计
pcl::PointCloud<pcl::Normal>::Ptr normals(new pcl::PointCloud<pcl::Normal>);
pcl::NormalEstimation<pcl::PointXYZ, pcl::Normal> ne;
ne.setInputCloud(cloud);
pcl::search::KdTree<pcl::PointXYZ>::Ptr tree(new pcl::search::KdTree<pcl::PointXYZ>());
ne.setSearchMethod(tree);
ne.setRadiusSearch(0.03); // 半径可根据点云密度调整
ne.compute(*normals);
// CVFH 估计
pcl::CVFHEstimation<pcl::PointXYZ, pcl::Normal, pcl::VFHSignature308> cvfh;
cvfh.setInputCloud(cloud);
cvfh.setInputNormals(normals);
cvfh.setSearchMethod(tree);
cvfh.setRadiusNormals(0.03); // 法线估计半径
cvfh.setClusterTolerance(0.05); // 欧氏聚类容差
cvfh.setCurvatureThreshold(0.03); // 曲率阈值
cvfh.setEPSAngleThreshold(5.0f / 180.0f * static_cast<float>(M_PI)); // 法线夹角阈值
cvfh.setMinPoints(100); // 每个聚类的最小点数
pcl::PointCloud<pcl::VFHSignature308>::Ptr cvfh_signature(new pcl::PointCloud<pcl::VFHSignature308>);
cvfh.compute(*cvfh_signature);
// 输出 CVFH 特征维度和数量
std::cout << "Computed " << cvfh_signature->size() << " CVFH signature(s) with 308 dimensions each." << std::endl;
// 输出第一个特征向量
if (!cvfh_signature->empty()) {
std::cout << "First CVFH descriptor: " << std::endl;
for (int i = 0; i < 308; ++i)
std::cout << cvfh_signature->points[0].histogram[i] << " ";
std::cout << std::endl;
}
return 0;
}
int main(int argc, char** argv)
{
TestCVFH(argc, argv);
std::cout << "Hello PCL World!" << std::endl;
std::system("pause");
return 0;
}
至此完成第二十节PCL库点云特征之CVFH特征描述,下一节我们将进入《PCL库中点云特征之RSD(Radius-based Surface Descriptor)特征描述》的学习,欢迎喜欢的朋友订阅。