PCL点云库入门(第22讲)——PCL库点云特征之RIFT 局部特征描述Rotation Invariant Feature Transform descriptors(RIFT)

一、算法原理

1、RIFT 特征简介

RIFT(轮换不变特征变换)最初由 Chao Dong 等人在 2009 年提出,用于多模态图像匹配。与 SIFT 的“方向归一化”思路不同,RIFT 通过 相对梯度方向直方图 建立旋转不变性,无需估计关键点主方向。随后该思想被推广到 3 D 点云(PCL 中的 RIFTEstimation)。下面以二维灰度图像为例讲述原理,再说明其扩展到 3 D 的要点。
在这里插入图片描述


2、原理讲述

1. 局部坐标系与旋转不变性
  • 关键点中心 p:从检测器(MSER、DoG、Harris…)获得。

  • 局部邻域半径 R:通常与尺度关联,确保包含足够上下文。

  • 旋转不变策略

    1. 在极坐标系内,只记录梯度方向与径向方向之间的夹角

      θq=∠(∇I(q), q−p) \theta_q=\angle\bigl(\nabla I(q),\,q-p\bigr) θq=(I(q),qp)

    2. 当整个补丁旋转时,径向方向随之旋转,而二者夹角保持不变 ⇒ 描述子天然轮换不变。

    3. 无需像 SIFT 那样估计主方向并旋转补丁,因此 对噪声与遮挡更稳健


2. 描述子结构
组成说明
drd_rdr径向环(radial bins)依距离 r/Rr/Rr/R 均分(典型 3‒5 环)
dθd_\thetadθ方向 bins[0,2π)[0, 2\pi)[0,2π) 划为等角区(典型 8‒12 bin)
长度dr×dθd_r\times d_\thetadr×dθ常见 24、32、40 维

每个环建立一个方向直方图 → 把所有直方图串联成最终向量,并可再 L2 归一化 提升鲁棒性。


3. 计算步骤(二维图像)
  1. 梯度计算
    Sobel / Scharr 得到每像素梯度 ∇I=(Ix,Iy)\nabla I=(I_x,I_y)I=(Ix,Iy),存储幅值 ∣∇I∣|\nabla I|∣∇I 与方向 ϕ\phiϕ

  2. 环分区 & 方向量化

    • 将邻域以中心为圆心划分为 drd_rdr 个同面积环。

    • 对于环内每像素 q,求

      θq=ϕq−arctan⁡2(yq−yp,  xq−xp) \theta_q = \phi_q - \arctan2(y_q - y_p,\; x_q - x_p) θq=ϕqarctan2(yqyp,xqxp)

      并映射到 [0,2π)[0,2\pi)[0,2π) 后落入某方向 bin。

  3. 加权投票

    • 幅值权重:投射到对应方向 bin。
    • 距离高斯权:离中心越近权重越大(可选)。
    • 双线性插值:减小量化误差。
  4. 特征向量拼接 + 归一化

    • 拼接所有环的直方图 → 得到 RIFT 向量。
    • L1/L2 归一化可提升光照鲁棒性。

3、 扩展到三维点云

步骤二维对应三维实现要点
梯度源灰度梯度Intensity Gradient 或曲面法向变化
邻域圆形球形(KD‑Tree or 半径搜索)
径向方向像素位移向量 q−pq-pqp
夹角2 D 角差计算 θ=arccos⁡(∇I⋅v^)\theta=\arccos\bigl(\nabla I \cdot \hat v\bigr)θ=arccos(Iv^)v^\hat vv^ 为归一化径向)
方向 bins0‒2π0‒π(由于夹角对称),常用 8 bin
结果Hist × 环数同理,维数 = dr×dθd_r\times d_\thetadr×dθ

PCL 默认参数:nr_distance_bins = 4, nr_gradient_bins = 8 → 32 维向量。


1.3D RIFT 特征提取算法流程

步骤概览

  1. 计算强度梯度图(Gradient)
  2. 确定每个关键点的邻域
  3. 构建方向直方图
  4. 构建多环形结构(radial bins)
  5. 计算 RIFT 描述子

Step 1: 强度梯度计算(使用法线 + intensity)

  • 使用 pcl::IntensityGradientEstimation 或用户自定义方式获取每个点的强度梯度 ∇I\nabla II
  • 梯度可以从反射强度、灰度或其他标量字段中计算。

得到每个点的梯度向量:

∇I=(∂I∂x,∂I∂y,∂I∂z) \nabla I = \left( \frac{\partial I}{\partial x}, \frac{\partial I}{\partial y}, \frac{\partial I}{\partial z} \right) I=(xI,yI,zI)


Step 2: 定义关键点邻域

  • 对每个关键点 ppp,在其周围定义一个球形邻域半径 rrr
  • 所有在这个邻域内的点 q∈N(p)q \in \mathcal{N}(p)qN(p) 都会参与特征描述子计算。

Step 3: 计算方向直方图(gradient orientation histogram)

对每个邻域内点,计算其相对位置向量 v⃗=q−p\vec{v} = q - pv=qp,然后计算梯度方向与向量方向之间的夹角

梯度方向编码成若干个 角度 bin(常见如 8 个方向 bin):

  • 将方向空间(0 到 2π)划分成 dθd_\thetadθ 个角度 bin
  • 每个点的梯度方向 θq\theta_qθq 落入某个 bin,并根据梯度幅值对该 bin 累计投票

Step 4: 构建多环区域(radial bins)

  • 将邻域按距离关键点的距离划分为若干个 环形区域(radial bins)

  • 常用的划分为 drd_rdr 个半径分段,例如:

    • 第1环:0 ~ 1/3*r
    • 第2环:1/3r ~ 2/3r
    • 第3环:2/3*r ~ r

Step 5: 构造 RIFT 描述子

  • 每个 radial bin 中统计一组 gradient orientation histogram(方向直方图)
  • 整体 RIFT 描述子是一个维度为 dr×dθd_r \times d_\thetadr×dθ 的向量

例如,设定:

  • nr_distance_bins = 4
  • nr_gradient_bins = 8

则 RIFT 描述子长度为 4×8=324 \times 8 = 324×8=32


4、RIFT 描述子特点总结

特性描述
旋转不变性只考虑梯度方向与径向方向的夹角
尺度敏感邻域半径需调参,固定尺度
对噪声鲁棒性一般梯度方向对噪声较敏感
适合反射强度丰富数据如激光雷达点云中的 intensity 字段
适用于稠密点云因为梯度估计依赖邻域结构

RIFT 描述通常用于:

  • 点云配准前的特征匹配
  • Loop closure detection
  • 场景识别等

二、主要成员变量和成员函数

1.类模板定义:

template <typename PointInT, typename GradientT, typename PointOutT>
class RIFTEstimation : public Feature<PointInT, PointOutT>
  • PointInT:输入点类型(通常带有强度/灰度通道)
  • GradientT:梯度类型(一般为 pcl::GradientXY 或用户自定义)
  • PointOutT:输出特征点类型(如 pcl::Histogram<N>

2.成员变量:

PointCloudGradientConstPtr gradient_;
  • 输入梯度点云的常量指针
  • 每个点包含梯度方向信息(如强度梯度 vector)。
int nr_distance_bins_;
  • RIFT 描述子的“半径方向”的分箱数量
  • 默认值为 4,表示描述子将邻域划分为 4 个同心圆环。
int nr_gradient_bins_;
  • RIFT 描述子的“梯度方向”维度的分箱数量
  • 默认值为 8,表示将方向划分为 8 个扇形方向(45° 角度分辨率)。

3.成员函数:

1.公有成员函数:

RIFTEstimation () : gradient_ (), nr_distance_bins_ (4), nr_gradient_bins_ (8)
  • 构造函数,设置默认距离和梯度分箱数。
  • 设置特征名称为 "RIFTEstimation"

2.输入设置相关:

inline void setInputGradient (const PointCloudGradientConstPtr &gradient)
  • 设置输入的梯度图(一般由 pcl::IntensityGradientEstimation 预处理生成)。
inline PointCloudGradientConstPtr getInputGradient () const
  • 返回输入的梯度图指针。
inline void setNrDistanceBins (int nr_distance_bins)
inline int getNrDistanceBins () const
  • 设置 / 获取 RIFT 描述子的径向(环形)分区数。
inline void setNrGradientBins (int nr_gradient_bins)
inline int getNrGradientBins () const
  • 设置 / 获取 RIFT 描述子的方向(角度)分区数。

3.特征计算相关:

void computeRIFT (const PointCloudIn &cloud, const PointCloudGradient &gradient,
                  int p_idx, float radius,
                  const std::vector<int> &indices,
                  const std::vector<float> &squared_distances,
                  Eigen::MatrixXf &rift_descriptor);
  • 核心函数:计算某一点的 RIFT 特征描述子。

  • 参数含义:

    • cloud:输入点云。
    • gradient:梯度图。
    • p_idx:查询点索引。
    • radius:邻域半径。
    • indices:查询点邻域点索引。
    • squared_distances:邻域内各点到中心点的距离平方。
    • rift_descriptor:输出的 RIFT 描述子矩阵(大小为 nr_distance_bins_ * nr_gradient_bins_)。

4.保护函数

void computeFeature (PointCloudOut &output) override;
  • 重载 Feature 接口中的 computeFeature() 函数

  • 该函数:

    • 遍历每个 interest point;
    • 查找邻域;
    • 调用 computeRIFT 填充输出特征;
    • 输出为 PointOutT 格式(通常是 pcl::Histogram<N>)。

三、关键部分代码注释

1.计算单点RIFT特征

template <typename PointInT, typename GradientT, typename PointOutT> 
void pcl::RIFTEstimation<PointInT, GradientT, PointOutT>::computeRIFT (
      const PointCloudIn &cloud, const PointCloudGradient &gradient, 
      int p_idx, float radius, const std::vector<int> &indices, 
      const std::vector<float> &sqr_distances, Eigen::MatrixXf &rift_descriptor)
{
  // 若邻域索引为空,则报错退出
  if (indices.empty ())
  {
    PCL_ERROR ("[pcl::RIFTEstimation] Null indices points passed!\n");
    return;
  }

  // 获取 RIFT 描述子的行数和列数,对应距离和梯度方向的 bin 数
  int nr_distance_bins = static_cast<int> (rift_descriptor.rows ());
  int nr_gradient_bins = static_cast<int> (rift_descriptor.cols ());

  // 获取中心点 p_idx 的 3D 坐标向量(Eigen::Vector3fMap 类型)
  pcl::Vector3fMapConst p0 = cloud.points[p_idx].getVector3fMap ();

  // 将 RIFT 描述子初始化为零矩阵
  rift_descriptor.setZero ();

  // 遍历每个邻域点
  for (std::size_t idx = 0; idx < indices.size (); ++idx)
  {
    // 获取当前点的 3D 坐标
    pcl::Vector3fMapConst point = cloud.points[indices[idx]].getVector3fMap ();

    // 获取梯度向量(转换为 Eigen::Vector3f)
    Eigen::Map<const Eigen::Vector3f> gradient_vector (& (gradient.points[indices[idx]].gradient[0]));

    // 计算梯度幅值
    float gradient_magnitude = gradient_vector.norm ();

    // 计算该点梯度与中心点连线方向之间的夹角(弧度)
    float gradient_angle_from_center = std::acos (
        gradient_vector.dot ((point - p0).normalized ()) / gradient_magnitude);
    
    // 检查角度值是否为有效浮点数
    if (!std::isfinite (gradient_angle_from_center))
      gradient_angle_from_center = 0.0;

    // 引入一个非常小的数防止除以零
    const float eps = std::numeric_limits<float>::epsilon ();

    // 将距离和角度归一化并映射到 bin 编号
    float d = static_cast<float> (nr_distance_bins) * std::sqrt (sqr_distances[idx]) / (radius + eps);
    float g = static_cast<float> (nr_gradient_bins) * gradient_angle_from_center / (static_cast<float> (M_PI) + eps);

    // 计算距离维度和角度维度上影响的 bin 范围,准备进行插值加权
    int d_idx_min = (std::max)(static_cast<int> (std::ceil (d - 1)), 0);
    int d_idx_max = (std::min)(static_cast<int> (std::floor (d + 1)), nr_distance_bins - 1);
    int g_idx_min = static_cast<int> (std::ceil (g - 1));
    int g_idx_max = static_cast<int> (std::floor (g + 1));

    // 遍历角度 bin 范围
    for (int g_idx = g_idx_min; g_idx <= g_idx_max; ++g_idx)  
    {
      // 因为角度是周期性的,所以需要对角度 bin 索引进行 wrap-around 处理
      int g_idx_wrapped = ((g_idx + nr_gradient_bins) % nr_gradient_bins); 

      // 遍历距离 bin 范围
      for (int d_idx = d_idx_min; d_idx <= d_idx_max; ++d_idx)
      {
        // 使用双线性插值(线性权重)更新对应 bin 值
        float w = (1.0f - std::abs (d - static_cast<float> (d_idx))) *
                  (1.0f - std::abs (g - static_cast<float> (g_idx)));

        // 将加权后的梯度幅值加入到对应的 RIFT 描述子 bin 中
        rift_descriptor (d_idx, g_idx_wrapped) += w * gradient_magnitude;
      }
    }
  }

  // 对描述子进行 L2 归一化(使得向量模长为 1),增强鲁棒性
  rift_descriptor.normalize ();
}

简要总结核心逻辑

  1. 输入数据:中心点、其邻域点索引、半径、邻域点到中心点的平方距离、以及对应的梯度。

  2. 核心处理

    • 根据每个邻域点的位置和梯度方向,计算其“距离 bin”与“方向 bin”。
    • 对每个有效 bin 进行插值加权累加。
  3. 输出结果

    • 得到一个二维的直方图 rift_descriptor,其每个元素为梯度幅值的加权和。
    • 最后归一化该描述子。

2.计算所有点RIFT特征

template <typename PointInT, typename GradientT, typename PointOutT> void
pcl::RIFTEstimation<PointInT, GradientT, PointOutT>::computeFeature (PointCloudOut &output)
{
  // 检查是否设置了搜索半径,如果为 0 则报错并清空输出
  if (search_radius_ == 0.0)
  {
    PCL_ERROR ("[pcl::%s::computeFeature] The search radius must be set before computing the feature!\n",
               getClassName ().c_str ());
    output.width = output.height = 0;
    output.points.clear ();
    return;
  }

  // 检查梯度方向的 bin 数是否有效(必须 > 0)
  if (nr_gradient_bins_ <= 0)
  {
    PCL_ERROR ("[pcl::%s::computeFeature] The number of gradient bins must be greater than zero!\n",
               getClassName ().c_str ());
    output.width = output.height = 0;
    output.points.clear ();
    return;
  }

  // 检查距离的 bin 数是否有效(必须 > 0)
  if (nr_distance_bins_ <= 0)
  {
    PCL_ERROR ("[pcl::%s::computeFeature] The number of distance bins must be greater than zero!\n",
               getClassName ().c_str ());
    output.width = output.height = 0;
    output.points.clear ();
    return;
  }

  // 检查输入是否设置了梯度图
  if (!gradient_)
  {
    PCL_ERROR ("[pcl::%s::computeFeature] No input gradient was given!\n", getClassName ().c_str ());
    output.width = output.height = 0;
    output.points.clear ();
    return;
  }

  // 检查梯度图中的点数与输入点云是否一致
  if (gradient_->points.size () != surface_->points.size ())
  {
    PCL_ERROR ("[pcl::%s::computeFeature] ", getClassName ().c_str ());
    PCL_ERROR ("The number of points in the input dataset differs from the number of points in the gradient!\n");
    output.width = output.height = 0;
    output.points.clear ();
    return;
  }

  // 创建一个用于存储 RIFT 描述子的矩阵(行:距离 bin,列:梯度 bin)
  Eigen::MatrixXf rift_descriptor (nr_distance_bins_, nr_gradient_bins_);
  
  // 存储邻域内的点索引
  std::vector<int> nn_indices;
  // 存储邻域内点与中心点之间的平方距离
  std::vector<float> nn_dist_sqr;

  // 遍历每个有效索引点(通常是采样下来的点)
  for (std::size_t idx = 0; idx < indices_->size (); ++idx)
  {
    // 在 search_radius_ 范围内查找当前点的邻居点
    tree_->radiusSearch ((*indices_)[idx], search_radius_, nn_indices, nn_dist_sqr);

    // 对当前点计算 RIFT 描述子
    computeRIFT (*surface_, *gradient_, (*indices_)[idx], static_cast<float> (search_radius_), nn_indices, nn_dist_sqr, rift_descriptor);

    // 将计算出的描述子内容复制到输出点云对应点的 histogram 数组中
    std::size_t bin = 0;
    for (Eigen::Index g_bin = 0; g_bin < rift_descriptor.cols (); ++g_bin)
      for (Eigen::Index d_bin = 0; d_bin < rift_descriptor.rows (); ++d_bin)
        output.points[idx].histogram[bin++] = rift_descriptor (d_bin, g_bin);
  }
}

总结

该函数的核心流程如下:

  1. 参数检查:确保搜索半径、距离与梯度方向 bin 数有效,梯度图存在且维度匹配。
  2. 邻域搜索:对每个采样点,使用 KdTree 进行半径邻域搜索。
  3. 特征提取:调用 computeRIFT 函数计算当前点的 RIFT 描述子。
  4. 写入输出:将描述子矩阵扁平化填入到输出点云的 histogram 数组中。

四、使用代码示例



/*****************************************************************//**
 * \file   PCLLearnRIFTFeathermain.cpp
 * \brief  
 * 
 * \details    
 * \author ZhengShi.Yang
 * \date   July 2025
 * \version   1.0
 * \dependencies  
 *********************************************************************/

#include <pcl/point_types.h>
#include <pcl/register_point_struct.h>
#include <pcl/io/pcd_io.h>
#include <pcl/common/common.h>
#include <pcl/features/normal_3d.h>
#include <pcl/features/rift.h>
#include <pcl/features/intensity_gradient.h>
#include <pcl/search/kdtree.h>
#include <pcl/kdtree/kdtree_flann.h>
#include <pcl/features/narf_descriptor.h>

#include <iostream>
#include <Eigen/Dense>

using namespace std;


int TestRIFT(int argc, char** argv)
{
	// 1. 读取点云数据
	std::string file = "E:/db_data/PCLLearnData/cloud_bin_1.pcd";
	// 加载输入点云(要求具有强度字段)
	pcl::PointCloud<pcl::PointXYZI>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZI>());
	if (pcl::io::loadPCDFile(file, *cloud) < 0)
	{
		PCL_ERROR("Cannot load PCD file!\n");
		return -1;
	}

	// 创建 KdTree 搜索结构(用于邻域查询)
	pcl::search::KdTree<pcl::PointXYZI>::Ptr tree(new pcl::search::KdTree<pcl::PointXYZI>());
	// 计算强度梯度
	pcl::PointCloud<pcl::IntensityGradient>::Ptr gradients(new pcl::PointCloud<pcl::IntensityGradient>());
	pcl::IntensityGradientEstimation<pcl::PointXYZI, pcl::Normal, pcl::IntensityGradient> gradient_est;
	gradient_est.setInputCloud(cloud);

	//// 法线计算器(用于梯度估计)
	pcl::PointCloud<pcl::Normal>::Ptr normals(new pcl::PointCloud<pcl::Normal>());
	pcl::NormalEstimation<pcl::PointXYZI, pcl::Normal> normal_est;
	normal_est.setInputCloud(cloud);
	normal_est.setSearchMethod(tree);
	normal_est.setRadiusSearch(0.05); // 根据场景调整

	normal_est.compute(*normals);
	gradient_est.setInputNormals(normals);
	gradient_est.setSearchMethod(tree);
	gradient_est.setRadiusSearch(0.05);
	gradient_est.compute(*gradients);

	// 设置 RIFT 特征估计器
	pcl::RIFTEstimation<pcl::PointXYZI, pcl::IntensityGradient, pcl::Histogram<32>> rift_est;
	rift_est.setInputCloud(cloud);
	rift_est.setSearchSurface(cloud); // 可选:完整点云用于搜索
	rift_est.setInputGradient(gradients);
	rift_est.setSearchMethod(tree);
	rift_est.setRadiusSearch(0.05);  // 搜索半径
	rift_est.setNrDistanceBins(4);     // 空间划分距离 bins
	rift_est.setNrGradientBins(8);     // 梯度方向 bins

	// 输出特征点云
    pcl::PointCloud<pcl::Histogram<32>>::Ptr rift_descriptors(new pcl::PointCloud<pcl::Histogram<32>>());
	rift_est.compute(*rift_descriptors);

	std::cout << "RIFT descriptor computed for " << rift_descriptors->size() << " points." << std::endl;

	return 0;
}



int main(int argc,char *argv[])
{

	TestRIFT(argc, argv);
  std::cout<<"Hello PCL!"<<std::endl;
  std::system("pause");
  return 0;
}


注意事项

  1. 点类型必须包含强度:RIFT 是基于强度梯度的描述子,因此必须是 PointXYZI
  2. 法线影响梯度质量:确保法线估计合理,radius 参数应匹配数据密度。
  3. 匹配点对:RIFT 通常配合对应点对匹配方案使用,例如 FLANN 最近邻搜索。

结果展示


至此完成第二十二节PCL库点云特征之RIFI特征描述,下一节我们将进入《PCL库中点云特征之NARF (Normal Aligned Radial Features)特征描述》的学习,欢迎喜欢的朋友订阅。

访问真八字排盘系统介绍: 1、八字排盘 八字排盘是将年月日时按照天干地支的形式进行排列,一个时间单位代表了一个柱。八字由年柱、月柱、日柱和时柱共四个柱组成,也被称为四柱八字。八字学中基于中国阴阳五行、天干地支与刑冲克害、以及民间盲派的神煞论等方式,进一步预测爱情顺遂、工作高低、姻缘好坏、财富高低、学业成就、身体健康等事的学问。 八字排盘由以下元素组合:年月日时四柱、大运干支、胎元 、流年干支、十神、地势、神煞等。生辰八字不只是把干支历计算出来,而还要遵守月令、节令的强弱,时辰的阴阳变化进行校正。排盘分析,就是根据出生者的性别、天干地支的阴阳五行关系、进一步推算出来的一套方法论,给预测者做人生吉凶的参考数据,在未来事业、财运、婚姻、家庭等问题时,能做出风险较低的决策。 八字排盘怎么看 八字排盘由年、月、日、时四柱组成,每柱包含一个天干和一个地支,共八个字。年柱代表出生的年份,月柱代表出生的月份,日柱代表出生的日期,时柱则代表出生的时辰。每个柱的干支组合都会对个人的命运产生影响。天干地支旁边标注的正财、偏财、偏印、正印、比肩、劫财、食神、伤官、正官、七杀等,称为十神。 2、八字排盘软件介绍 我们是腾讯市场金牌合作伙伴,广州正规软件开发公司,开发的八字排盘系统数据最全面精,我们八字排盘采用最精确的排盘程序,而且运用“真太阳时”,进行更精确的时间划分。大家都知道我们使用的北京时间,是统一规定的标时间。而八字排盘需要相对于太阳方位的天文时间,即平太阳时。我们国家地大物博,北京时间19时,哈尔滨已经夜幕降临,而新疆却还是太阳高挂,这时哈尔滨的天文时间可能在20:00以后,而新疆的天文时间可能在16时以前。北京时间是东经120度经线的平太阳时,如果您出生地的经度与北京时间所处的经度差异较大,或者处于单数时间点的前后,比如6点差一刻,8点,10点15分等
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

点云SLAM

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值