LFSH 描述子

LFSH 描述子的特征组成

设 p 为待描述点,以 p 为球心,给定半径 R 计算出其球形邻域,LFSH 描述子由此球形邻域中的各点计算得出。

LFSH 描述子由三个特征组成:

① 局部深度

p_i 为待描述点,n_ip_i 的法线,平面 L 垂直于 n_i,并且与球形邻域相切 。将球形邻域中的点投影至平面 L,其中原点和投影点之间的距离 即为该点的局部深度

② 法线偏角

法线偏角即待描述点的法线与其球形邻域内各点法线的夹角。

③ 点密度

上述的投影点 中,以待描述点的投影为圆心,划分若干半径,统计在每个环形区域内的点。

特征结合

三个特征对应三个直方图,直方图的 bin size 设置如下:局部深度8-15,法线夹角10-17,点密度4-7。文献中采取的 bin size 分别为10、15、5。

局部深度: 将每个点与其投影点的距离排序,设最大距离和最小距离之差为 d,则将 d 划分为 10 份,即 min ~ min + (1/10)dmin + (1/10)d ~ min + (2/10)d...,分别统计在10个距离中的点的个数。

法线夹角: 将计算出的每个点与待描述点的法线夹角进行排序,计算最大角度和最小角度之差,将此差值划分 15 份,同样分别统计在每个区间的点的个数。

点密度: 在投影点中,计算与待描述点的投影最远的距离,将此距离划分为5份,同样分别统计在各个距离内的点的个数。

将上述统计值直接结合,得到一个 bin size = 30 的直方图,此即为 LFSH 描述符。

代码实现

LFSH.h

cpp 复制代码
#ifndef LFSH_H
#define LFSH_H

#include <pcl/point_types.h>
#include <pcl/io/pcd_io.h>
#include <vector>

class LFSH
{
public:
    LFSH(const pcl::PointXYZ& p, double r, const pcl::PointCloud<pcl::PointXYZ>::Ptr cloud, int N1, int N2, int N3, double normalR);

    const pcl::PointCloud<pcl::PointXYZ>::Ptr& getCloud() { return m_cloud; }

    const std::vector<double>& getDepth() { return m_depth; }

    const std::vector<double>& getDegress() { return m_degress; }

    const std::vector<int>& getDepthHistogram() { return m_depth_histogram; }

    const std::vector<int>& getDegressHistogram() { return m_degress_histogram; }

    const std::vector<int>& getDenistyHistogram() { return m_denisty_histogram; }


private:
    pcl::PointXYZ m_P;                                  // 待描述的点
    double m_R;                                         // 以 m_P 为圆心的球体的半径
    pcl::PointCloud<pcl::PointXYZ>::Ptr m_SourceCloud;  // 原始点云数据
    pcl::PointCloud<pcl::PointXYZ>::Ptr m_cloud;        // 以 m_P 为圆心的球体邻域
    pcl::PointCloud<pcl::Normal>::Ptr m_normals;        // 球体邻域内每个点的法线
    std::vector<double> m_depth;                        // 局部深度
    std::vector<int> m_depth_histogram;                 // 局部深度直方图
    std::vector<double> m_degress;                      // 法线夹角
    std::vector<int> m_degress_histogram;               // 法线夹角直方图
    std::vector<int> m_denisty_histogram;               // 密度直方图
    int m_N1, m_N2, m_N3;                               // 三个子直方图的 bin size
    double m_normal_R;                                  // 计算法线时使用的半径
};

#endif // LFSH_H

LFSH.cpp

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include "lfsh.h"

#include <set>
#include <pcl/octree/octree_search.h>
#include <pcl/features/normal_3d.h>
#include <pcl/kdtree/kdtree_flann.h>
#include <tuple>


/**
 * @brief			返回给定点、给定半径的球体中点的索引
 * @param r			球体半径
 * @param p			给定球心
 * @param cloud		操作的点云
 * @return			球体中点的索引
*/
std::set<int> sphereSearch(double r, pcl::PointXYZ p, pcl::PointCloud<pcl::PointXYZ>::Ptr cloud)
{
    // 创建octree对象用于点云的搜索
    pcl::octree::OctreePointCloudSearch<pcl::PointXYZ> octree(0.5);
    octree.setInputCloud(cloud);
    octree.addPointsFromInputCloud();

    // 存储球体内的点索引
    std::vector<int> pointIdx;
    std::vector<float> pointRadiusSquaredDistance;

    // 用set存放球体内点的索引,提高查找效率
    std::set<int> sphereIndices;

    // 执行半径搜索
    if (octree.radiusSearch(p, r, pointIdx, pointRadiusSquaredDistance) > 0)
    {
        sphereIndices.insert(pointIdx.begin(), pointIdx.end());
    }

    return sphereIndices;
}


/**
 * @brief						提取球体邻域中的点
 * @param sphereIndices         球体内点的索引
 * @param cloud					原始点云
 * @return						提取结果
*/
pcl::PointCloud<pcl::PointXYZ>::Ptr sphereCloud(std::set<int> sphereIndices,
    pcl::PointCloud<pcl::PointXYZ>::Ptr cloud)
{
    pcl::PointCloud<pcl::PointXYZ>::Ptr resultCloud(new pcl::PointCloud<pcl::PointXYZ>);
    for (const auto& idx : sphereIndices)
    {
        resultCloud->push_back(cloud->points[idx]);
    }

    // 更新点云信息
    resultCloud->width = resultCloud->points.size();
    resultCloud->height = 1;

    return resultCloud;
}


// 定义一个函数来计算与给定法线垂直并与球体相切的平面方程
std::vector<double> calculateTangentPlane(const pcl::PointXYZ& point, const pcl::Normal& normal, float radius)
{
    // 计算法线的长度
    float normal_length = std::sqrt(normal.normal_x * normal.normal_x +
        normal.normal_y * normal.normal_y +
        normal.normal_z * normal.normal_z);

    // 计算切点坐标 Q(x1, y1, z1)
    float x1 = point.x + radius * normal.normal_x / normal_length;
    float y1 = point.y + radius * normal.normal_y / normal_length;
    float z1 = point.z + radius * normal.normal_z / normal_length;

    // 平面法向量是给定法线的方向向量 (n_x, n_y, n_z)
    float nx = normal.normal_x;
    float ny = normal.normal_y;
    float nz = normal.normal_z;

    // 计算平面方程的 d 值:d = n_x * x1 + n_y * y1 + n_z * z1
    float d = nx * x1 + ny * y1 + nz * z1;

    // 输出平面方程 n_x * x + n_y * y + n_z * z = d
    std::cout << "平面方程: " << nx << " * x + " << ny << " * y + " << nz << " * z = " << d << std::endl;
    std::vector<double> res;
    res.push_back(nx);
    res.push_back(ny);
    res.push_back(nz);
    res.push_back(d);
    return res;
}


/**
 * @brief   计算给定点云的法线
 * @param   给定点云
 */
pcl::PointCloud<pcl::Normal>::Ptr calNormals(pcl::PointCloud<pcl::PointXYZ>::Ptr cloud, double radius)
{
    pcl::NormalEstimation<pcl::PointXYZ, pcl::Normal> ne;
    ne.setInputCloud(cloud);

    // 使用 KD-Tree 进行邻域搜索
    pcl::search::KdTree<pcl::PointXYZ>::Ptr tree(new pcl::search::KdTree<pcl::PointXYZ>());
    ne.setSearchMethod(tree);
    ne.setRadiusSearch(radius);

    // 保存计算的法向量
    pcl::PointCloud<pcl::Normal>::Ptr normals(new pcl::PointCloud<pcl::Normal>());
    ne.compute(*normals);

    return normals;
}


/**
 * @brief   获取给定坐标的法向量
 * @param   cloud
 * @param   normals
 * @param   p
 */
pcl::Normal getNormalAtPoint(pcl::PointCloud<pcl::PointXYZ>::Ptr cloud,
    pcl::PointCloud<pcl::Normal>::Ptr normals,
    const pcl::PointXYZ& p)
{
    pcl::KdTreeFLANN<pcl::PointXYZ> kdtree;
    kdtree.setInputCloud(cloud);

    std::vector<int> pointIdxNKNSearch(1);
    std::vector<float> pointNKNSquaredDistance(1);

    if (kdtree.nearestKSearch(p, 1, pointIdxNKNSearch, pointNKNSquaredDistance) > 0)
    {
        int nearest_idx = pointIdxNKNSearch[0];
        return normals->points[nearest_idx];
    }
    else {
        throw std::runtime_error("未找到最近邻点,可能点云为空或坐标不在范围内");
    }
}


// 将点投影到平面上
std::tuple<pcl::PointXYZ, double> p2Plane(const pcl::PointXYZ& p, std::vector<double> param)
{
    double nx = param[0];
    double ny = param[1];
    double nz = param[2];
    double d = param[3];
    // 计算法向量的长度
    double norm_length = std::sqrt(nx * nx + ny * ny + nz * nz);

    // 计算点到平面的距离
    double distance = std::abs((p.x * nx + p.y * ny + p.z * nz - d) / norm_length);

    // 计算投影点的坐标
    double x_proj = p.x - distance * (nx / norm_length);
    double y_proj = p.y - distance * (ny / norm_length);
    double z_proj = p.z - distance * (nz / norm_length);

    pcl::PointXYZ res1(x_proj, y_proj, z_proj);

    return std::make_tuple(res1, distance);
}



/**
 * @brief           计算给定点云中的点投影在给定平面上的点坐标
 * @param cloud     原点云
 * @param normal    给定平面的法向量
 * @return          投影后的点云
 */
pcl::PointCloud<pcl::PointXYZ>::Ptr projectPointToPlane(const pcl::PointCloud<pcl::PointXYZ>::Ptr& cloud,
    std::vector<double> param, std::vector<double>& depth)
{
    pcl::PointCloud<pcl::PointXYZ>::Ptr cloudRes(new pcl::PointCloud<pcl::PointXYZ>);
    int count = 0;
    for (int i = 0; i < cloud->size(); i++)
    {
        std::tuple<pcl::PointXYZ, double> res = p2Plane(cloud->points[i], param);
        cloudRes->push_back(std::get<0>(res));  // 存储投影点
        depth.push_back(std::get<1>(res));      // 存储局部深度
        count++;
    }

    cloudRes->width = count;
    cloudRes->height = 1;
    return cloudRes;
}



double calculateAngleBetweenNormals(const pcl::Normal& n1, const pcl::Normal& n2) {
    // 计算法向量的点积
    double dotProduct = n1.normal_x * n2.normal_x + n1.normal_y * n2.normal_y + n1.normal_z * n2.normal_z;

    // 确保cosTheta在[-1, 1]范围内
    double cosTheta = std::max(-1.0, std::min(1.0, dotProduct));

    // 计算夹角
    double angle = acos(cosTheta); // 返回值为弧度

    // 可选:将角度转换为度
    angle = angle * (180.0 / M_PI); // 转换为度

    if (angle > 90)
        angle = angle - 180;

    return angle;
}



// 计算两点之间的距离
double distanceP2P(const pcl::PointXYZ& p1, const pcl::PointXYZ& p2)
{
    return sqrt((p1.x - p2.x) * (p1.x - p2.x) + (p1.y - p2.y) * (p1.y - p2.y) + (p1.z - p2.z) * (p1.z - p2.z));
}



// 构造函数
LFSH::LFSH(const pcl::PointXYZ& p, double r, const pcl::PointCloud<pcl::PointXYZ>::Ptr cloud, int N1, int N2, int N3, double normalR) :
    m_P(p), m_R(r), m_SourceCloud(cloud), m_N1(N1), m_N2(N2), m_N3(N3), m_normals(new pcl::PointCloud<pcl::Normal>)
{
    std::set<int> idxs = sphereSearch(m_R, m_P, m_SourceCloud);
    m_cloud = sphereCloud(idxs, m_SourceCloud);         // 球体邻域

    pcl::PointCloud<pcl::Normal>::Ptr sourceCloudNormal = calNormals(m_SourceCloud, normalR);   // 源点云的法线
    for (const auto& point : m_cloud->points)
    {         // 从源点云法向量中找出球体邻域内所有点的法向量
        try {
            pcl::Normal normal = getNormalAtPoint(m_SourceCloud, sourceCloudNormal, point);
            m_normals->push_back(normal);
        }
        catch (const std::runtime_error& e)
        {
            std::cout << e.what() << std::endl;;
        }
    }

    /* ----------计算局部深度---------- */
    pcl::Normal n = getNormalAtPoint(m_cloud, m_normals, m_P);              // 待描述点的法线
    std::cout << "normal: " << n.normal_x << "\t" << n.normal_y << "\t" << n.normal_z << std::endl;
    std::vector<double> planeParam = calculateTangentPlane(m_P, n, m_R);    // 计算垂直于待描述点法线,且与球体邻域相切的平面参数
    pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_proj = projectPointToPlane(m_cloud, planeParam, m_depth); // 计算投影到平面的点坐标,同时计算局部深度

    double min_depth = *std::min_element(m_depth.begin(), m_depth.end());
    double max_depth = *std::max_element(m_depth.begin(), m_depth.end());
    double d_depth = (max_depth - min_depth) / m_N1;
    m_depth_histogram.resize(m_N1, 0);

   

    /* ----------计算法线夹角---------- */
    for (const auto& it : m_normals->points)
    {
        m_degress.push_back(calculateAngleBetweenNormals(n, it));           // 存储法线夹角
    }

    double min_degress = *std::min_element(m_degress.begin(), m_degress.end());
    double max_degress = *std::max_element(m_degress.begin(), m_degress.end());
    double d_degress = (max_degress - min_degress) / m_N2;
    m_degress_histogram.resize(m_N2, 0);

    
  
    for (size_t i = 0; i < m_depth.size(); i++)
    {
        // std::cout << "i: " << i << std::endl;
        int n1 = (m_depth[i] - min_depth) / d_depth;
        if (n1 >= 0 && n1 < m_depth_histogram.size()) 
        {
            m_depth_histogram[n1]++;
        }
        /*std::cout << "n1: " << n1 << std::endl;*/
        int n2 = (m_degress[i] - min_degress) / d_degress;
        /*std::cout << "m_degress[i]: " << m_degress[i] << std::endl;
        std::cout << "n2: " << n2 << std::endl;*/

        if (n2 >= 0 && n2 < m_degress_histogram.size()) 
        {
            m_degress_histogram[n2]++;
        }
    }


    /* ----------计算密度---------- */

    pcl::PointXYZ p_proj = std::get<0>(p2Plane(m_P, planeParam));           // 待描述点的投影

    m_denisty_histogram.resize(m_N3, 0);
    

    double max = 0;
    for (const auto& it : cloud_proj->points)
    {
        double distance = distanceP2P(p_proj, it);
        max = std::max(max, distance);
    }

    // bool flag = false;

    for (const auto& it : cloud_proj->points)
    {
        double distance = distanceP2P(p_proj, it);
       
        int num = distance / (max / m_N3);
        /*if (flag >= m_denisty_histogram.size())
            flag = true;*/
        // std::cout << "num: " << num << std::endl;
        //std::cout << "dis: " << distance << std::endl;
        //std::cout << "num: " << num << std::endl;
        if (num >= 0 && num < m_denisty_histogram.size())
        {
            m_denisty_histogram[num]++;
        }                                                  // 存储密度
    }
    /*std::cout << "flag: " << flag << std::endl;
    std::cout << "max: " << max << std::endl;*/
}

main.cpp

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <pcl/point_types.h>
#include <pcl/io/pcd_io.h>
#include "LFSH.h"


int main()
{
	pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);
	pcl::io::loadPCDFile("C:\\Users\\10704\\Desktop\\test_data\\0_downsample_1cm.pcd", *cloud);
	pcl::PointXYZ p(442429.313, 4130677.500, 14.840);
	LFSH lfsh(p, 2, cloud, 10, 15, 5, 1);

	for (const auto& it : lfsh.getDepthHistogram())
		std::cout << it << "\t";
	std::cout << std::endl;

	for (const auto& it : lfsh.getDegressHistogram())
		std::cout << it << "\t";
	std::cout << std::endl;

	for (const auto& it : lfsh.getDenistyHistogram())
		std::cout << it << "\t";
	std::cout << std::endl;

	std::cout << "count denisty: " << std::accumulate(lfsh.getDenistyHistogram().begin(), lfsh.getDenistyHistogram().end(), 0) << std::endl;
	std::cout << "count degress: " << std::accumulate(lfsh.getDegressHistogram().begin(), lfsh.getDegressHistogram().end(), 0) << std::endl;
	std::cout << "count depth: " << std::accumulate(lfsh.getDepthHistogram().begin(), lfsh.getDepthHistogram().end(), 0) << std::endl;

	system("pause");
	return 0;
}

示例

程序运行结果:(某个点的 LFSH)

柱状图:

下一步实验

① 找到直方图相似度的比较方法(即 LFSH 的比较方法,文献中未给出)

② 针对数据测试 LFSH 的匹配效果。

相关推荐
武子康14 分钟前
大数据-207 数据挖掘 机器学习理论 - 多重共线性 矩阵满秩 线性回归算法
大数据·人工智能·算法·决策树·机器学习·矩阵·数据挖掘
小邓的技术笔记14 分钟前
20241106,LeetCode 每日一题,用 Go 实现整数回文数判断
算法·leetcode·golang
IronmanJay16 分钟前
【LeetCode每日一题】——802.找到最终的安全状态
数据结构·算法·leetcode··拓扑排序·802.找到最终的安全状态·反向图
兔兔爱学习兔兔爱学习38 分钟前
leetcode328. Odd Even Linked List
算法
£suPerpanda1 小时前
牛客周赛 Round65 补题DEF
开发语言·数据结构·c++·算法·深度优先·动态规划·图论
ao_lang1 小时前
剑指offer第五天
python·算法·cpp
付宇轩1 小时前
leetcode 173.二叉搜索树迭代器
算法·leetcode·职场和发展
L_cl1 小时前
数据结构与算法——Java实现 54.力扣1008题——前序遍历构造二叉搜索树
算法·leetcode
KeithTsui1 小时前
ZFC in LEAN 之 前集的等价关系(Equivalence on Pre-set)详解
开发语言·其他·算法·binder·swift