【PCL自学:Registration 2】如何使用ICP及如何实现增量配准

一、ICP(迭代最邻近点)的使用

1.1 什么是ICP

  本章演示了在代码中使用迭代最近点算法(ICP),ICP即通过最小化两个点云的点之间的距离并对它们进行严格的转换,来确定一个点云是否只是另一个点云的严格转换的方法,最终输出为源点云(动态点云)到目标点云(静态点云)的RT转换关系。这种方法被广泛应用到点云配准问题上。
  需要注意的是ICP往往用于点云配准的最后一步,即精配准过程,这要求源和目标两片点云的姿态相差不能太大,不然容易引起匹配错误。典型的ICP算法包括点-点,点-面,面-面的三种类型,各有优缺点。其共同的缺点是收敛速度慢,对离群值、缺失数据和部分重叠的敏感性差。
  在此篇文章中以最简单的点到点ICP在PCL中的使用做出介绍,在文章最后给出了ICP领域最近的学术成果论文,由清华大学著作,以供学习探讨。

1.2 点到点ICP示例代码分析

#include <iostream>
#include <pcl/io/pcd_io.h>
#include <pcl/point_types.h>
#include <pcl/registration/icp.h>

int main ()
{
  pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_in (new pcl::PointCloud<pcl::PointXYZ>(5,1));
  pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_out (new pcl::PointCloud<pcl::PointXYZ>);

  // 随机生成源点云
  for (auto& point : *cloud_in)
  {
    point.x = 1024 * rand() / (RAND_MAX + 1.0f);
    point.y = 1024 * rand() / (RAND_MAX + 1.0f);
    point.z = 1024 * rand() / (RAND_MAX + 1.0f);
  }
  
  std::cout << "Saved " << cloud_in->size () << " data points to input:" << std::endl;
   // 输出随机生成的源点云位置
  for (auto& point : *cloud_in)
    std::cout << point << std::endl;
  // 创建目标点云,每个点在源点云x+0.7的位置
  *cloud_out = *cloud_in;
  std::cout << "size:" << cloud_out->size() << std::endl;
  for (auto& point : *cloud_out)
    point.x += 0.7f;

  std::cout << "Transformed " << cloud_in->size () << " data points:" << std::endl;
   // 输出目标点云位置
  for (auto& point : *cloud_out)
    std::cout << point << std::endl;
	// 声明PCL的ICP类,设置源点云和目标点云
  pcl::IterativeClosestPoint<pcl::PointXYZ, pcl::PointXYZ> icp;
  icp.setInputSource(cloud_in);
  icp.setInputTarget(cloud_out);
  // 声明输出迭代结束后的点云
  pcl::PointCloud<pcl::PointXYZ> Final;
  // 开始迭代对齐计算
  icp.align(Final);
	//如果两个pointcloud正确对齐(意味着它们都是相同的云,只是对其中一个应用了某种严格的转换)
	//那么hasconverged () = 1 (true)。然后输出最终转换的拟合度得分和一些相关信息。
  std::cout << "has converged:" << icp.hasConverged() << " score: " <<
  icp.getFitnessScore() << std::endl;
  std::cout << icp.getFinalTransformation() << std::endl;

 return (0);
}

二、增量ICP的方法

2.1 什么是增量ICP

  本节介绍如何使用迭代最近点算法来增量地配准一系列的点云,即在第一个点云的坐标系中转换所有的点云。这是通过在每个连续的云之间找到最好的转换,并在整个云集上积累这些转换来实现的。
  首先数据集由点云组成,这些点云已经大致预先对齐在一个共同的坐标系中(例如在一个机器人的里程计或地图坐标系中),并且彼此有重叠区域。示例点云可以在PCL官方github上下载,点击【示例点云】可以下载。以下五个点云全都需要下载下来,示例代码会将5片点云配准成同一整体。
在这里插入图片描述

2.2 示例代码

注意: 代码较多,可参考注释理解。


 */
/* \author Radu Bogdan Rusu
 * adaptation Raphael Favier*/

 #include <pcl/memory.h>  // for pcl::make_shared
 #include <pcl/point_types.h>
 #include <pcl/point_cloud.h>
 #include <pcl/point_representation.h>
 
 #include <pcl/io/pcd_io.h>
 
 #include <pcl/filters/voxel_grid.h>
 #include <pcl/filters/filter.h>
 
 #include <pcl/features/normal_3d.h>
 
 #include <pcl/registration/icp.h>
 #include <pcl/registration/icp_nl.h>
 #include <pcl/registration/transforms.h>
 
#include <pcl/visualization/pcl_visualizer.h>
 
 using pcl::visualization::PointCloudColorHandlerGenericField;
 using pcl::visualization::PointCloudColorHandlerCustom;
 
 // 定义
 typedef pcl::PointXYZ PointT;
 typedef pcl::PointCloud<PointT> PointCloud;
 typedef pcl::PointNormal PointNormalT;
 typedef pcl::PointCloud<PointNormalT> PointCloudWithNormals;
 
 // 使用全局变量
 // 显示窗口的全局变量指针
 	pcl::visualization::PCLVisualizer *p;
 	// 左右视点
 	int vp_1, vp_2;
 
 // 点云结构体:点云指针、文件名称
 struct PCD
 {
   PointCloud::Ptr cloud;
   std::string f_name;

   PCD() : cloud (new PointCloud) {};
 };
 
 // PCD点云名称比较器
 struct PCDComparator
 {
   bool operator () (const PCD& p1, const PCD& p2)
   {
     return (p1.f_name < p2.f_name);
   }
 };
 
 
 // 定义一个新的点云表示方法-坐标曲率表示法 < x, y, z, curvature >
 class MyPointRepresentation : public pcl::PointRepresentation <PointNormalT>
 {
   using pcl::PointRepresentation<PointNormalT>::nr_dimensions_;
 public:
   MyPointRepresentation ()
   {
     // 定义点云表示的维度
    nr_dimensions_ = 4;
  }

  // Override the copyToFloatArray method to define our feature vector
  // 重写copyToFloatArray方法定义特征向量
  virtual void copyToFloatArray (const PointNormalT &p, float * out) const
  {
    // < x, y, z, curvature >
    out[0] = p.x;
    out[1] = p.y;
    out[2] = p.z;
    out[3] = p.curvature;
  }
};


/** \brief Display source and target on the first viewport of the visualizer
显示窗口的第一个视点显示源点云和目标点云
**/
void showCloudsLeft(const PointCloud::Ptr cloud_target, const PointCloud::Ptr cloud_source)
{
  p->removePointCloud ("vp1_target");
  p->removePointCloud ("vp1_source");
// 设置目标点云绿色,源点云红色个
 PointCloudColorHandlerCustom<PointT> tgt_h (cloud_target, 0, 255, 0);
 PointCloudColorHandlerCustom<PointT> src_h (cloud_source, 255, 0, 0);
  p->addPointCloud (cloud_target, tgt_h, "vp1_target", vp_1);
  p->addPointCloud (cloud_source, src_h, "vp1_source", vp_1);

  PCL_INFO ("Press q to begin the registration.\n");
  p-> spin();
}



/** \brief Display source and target on the second viewport of the visualizer
 *显示窗口的第二个视点显示源点云和目标点云
 */
void showCloudsRight(const PointCloudWithNormals::Ptr cloud_target, const PointCloudWithNormals::Ptr cloud_source)
{
  p->removePointCloud ("source");
  p->removePointCloud ("target");


 PointCloudColorHandlerGenericField<PointNormalT> tgt_color_handler (cloud_target, "curvature");
  if (!tgt_color_handler.isCapable ())
      PCL_WARN ("Cannot create curvature color handler!\n");

  PointCloudColorHandlerGenericField<PointNormalT> src_color_handler (cloud_source, "curvature");
 if (!src_color_handler.isCapable ())
     PCL_WARN ("Cannot create curvature color handler!\n");


  p->addPointCloud (cloud_target, tgt_color_handler, "target", vp_2);
  p->addPointCloud (cloud_source, src_color_handler, "source", vp_2);

  p->spinOnce();
}


/** \简介: 加载一组PCD文件
  * \参数 argc 是pcd文件的数量 (pass from main ())
  * \参数 argv 是pcd文件的名字容器 (pass from main ())
  * \参数 models 是加载后的点集结果
  */
void loadData (int argc, char **argv, std::vector<PCD, Eigen::aligned_allocator<PCD> > &models)
{
  std::string extension (".pcd");
  // 假设第一个参数是实际的测试模型
  for (int i = 1; i < argc; i++)
  {
    std::string fname = std::string (argv[i]);
    // Needs to be at least 5: .plot
    if (fname.size () <= extension.size ())
      continue;

    std::transform (fname.begin (), fname.end (), fname.begin (), (int(*)(int))tolower);

    //检查是不是 pcd 文件
    if (fname.compare (fname.size () - extension.size (), extension.size (), extension) == 0)
    {
      // 读点云并保存在model中
      PCD m;
      m.f_name = argv[i];
      pcl::io::loadPCDFile (argv[i], *m.cloud);
      //remove NAN points from the cloud
      std::vector<int> indices;
      pcl::removeNaNFromPointCloud(*m.cloud,*m.cloud, indices);

      models.push_back (m);
    }
  }
}



/** \简介: 对齐一对PointCloud数据集并返回结果
  * \参数 cloud_src 是源点云
  * \参数 cloud_tgt 是目标点云
  * \参数 output 对齐后的源点云(动态点云)
  * \参数 final_transform 源点云与目标点云的转换关系
  */
void pairAlign (const PointCloud::Ptr cloud_src, const PointCloud::Ptr cloud_tgt, PointCloud::Ptr output, Eigen::Matrix4f &final_transform, bool downsample = false)
{
  //
  // 对于大型数据应该启用下采样提高速度
  PointCloud::Ptr src (new PointCloud);
  PointCloud::Ptr tgt (new PointCloud);
  pcl::VoxelGrid<PointT> grid; // 体素网格
  if (downsample) // 是否启用下采样
  {
    grid.setLeafSize (0.05, 0.05, 0.05); // 设置下采样尺寸
    grid.setInputCloud (cloud_src);
    grid.filter (*src);

    grid.setInputCloud (cloud_tgt);
    grid.filter (*tgt);
  }
  else
  {
  // 不进行下采样
    src = cloud_src;
    tgt = cloud_tgt;
  }


  // 计算表面法向和曲率
  PointCloudWithNormals::Ptr points_with_normals_src (new PointCloudWithNormals);
  PointCloudWithNormals::Ptr points_with_normals_tgt (new PointCloudWithNormals);

  // 法向估计
  pcl::NormalEstimation<PointT, PointNormalT> norm_est;
  pcl::search::KdTree<pcl::PointXYZ>::Ptr tree (new pcl::search::KdTree<pcl::PointXYZ> ());
  norm_est.setSearchMethod (tree); // 设置搜索方法为kdtree
  norm_est.setKSearch (30);		// 设置k参数为30
  
  norm_est.setInputCloud (src); // 输入源点云
  norm_est.compute (*points_with_normals_src); // 开始估计法向
  pcl::copyPointCloud (*src, *points_with_normals_src); // 拷贝点云

  norm_est.setInputCloud (tgt);
  norm_est.compute (*points_with_normals_tgt);
  pcl::copyPointCloud (*tgt, *points_with_normals_tgt);
  //
  // 实例化自定义点表示(上面定义的)…
  MyPointRepresentation point_representation;
  //并对“曲率”维度进行加权,使其与x、y和z保持平衡
  float alpha[4] = {1.0, 1.0, 1.0, 1.0};
  point_representation.setRescaleValues (alpha);

  //
  // 对齐  pcl::IterativeClosestPointNonLinear<PointNormalT, PointNormalT> reg;
  reg.setTransformationEpsilon (1e-6);
  // 设置最大距离为 (src<->tgt) 10cm
  // 注意:考虑到速度,可根据数据集的大小进行调整该参数
  reg.setMaxCorrespondenceDistance (0.1);  
  // 设置点表示方式
  reg.setPointRepresentation (pcl::make_shared<const MyPointRepresentation> (point_representation));

  reg.setInputSource (points_with_normals_src);
  reg.setInputTarget (points_with_normals_tgt);



  //
  // 在循环中运行相同的优化并可视化结果
  Eigen::Matrix4f Ti = Eigen::Matrix4f::Identity (), prev, targetToSource;
  PointCloudWithNormals::Ptr reg_result = points_with_normals_src;
  reg.setMaximumIterations (2); 
  for (int i = 0; i < 30; ++i)
  {
    PCL_INFO ("Iteration Nr. %d.\n", i);

    // 保存云用于可视化
    points_with_normals_src = reg_result;

    // 估计对齐
    reg.setInputSource (points_with_normals_src);
    reg.align (*reg_result);

		// 累积每个迭代之间的转换
    Ti = reg.getFinalTransformation () * Ti;

		//如果这个变换和前一个变换的差值小于阈值,通过减少最大的距离来优化这个过程
    if (std::abs ((reg.getLastIncrementalTransformation () - prev).sum ()) < reg.getTransformationEpsilon ())
      reg.setMaxCorrespondenceDistance (reg.getMaxCorrespondenceDistance () - 0.001);
    
    prev = reg.getLastIncrementalTransformation ();

    // 显示当前状态
    showCloudsRight(points_with_normals_tgt, points_with_normals_src);
  }

	//
  // 获得从目标点云到源点云的转换
  targetToSource = Ti.inverse();

  //
  // 将目标点云转换回源点云
  pcl::transformPointCloud (*cloud_tgt, *output, targetToSource);

  p->removePointCloud ("source");
  p->removePointCloud ("target");

  PointCloudColorHandlerCustom<PointT> cloud_tgt_h (output, 0, 255, 0);
  PointCloudColorHandlerCustom<PointT> cloud_src_h (cloud_src, 255, 0, 0);
  p->addPointCloud (output, cloud_tgt_h, "target", vp_2);
  p->addPointCloud (cloud_src, cloud_src_h, "source", vp_2);

	PCL_INFO ("Press q to continue the registration.\n");
  p->spin ();

  p->removePointCloud ("source"); 
  p->removePointCloud ("target");

  //将源点云添加到转换后的目标点云上
  *output += *cloud_src;
  
  final_transform = targetToSource;
 }


/* ---[ */
int main (int argc, char** argv)
{
  // 加载点云数据
  std::vector<PCD, Eigen::aligned_allocator<PCD> > data;
 loadData (argc, argv, data);

  // 检查用户输入是否正确
  if (data.empty ())
  {
    PCL_ERROR ("Syntax is: %s <source.pcd> <target.pcd> [*]\n", argv[0]);
    PCL_ERROR ("[*] - multiple files can be added. The registration results of (i, i+1) will be registered against (i+2), etc\n");
    return (-1);
  }
  PCL_INFO ("Loaded %d datasets.\n", (int)data.size ());
  
  // 创建可视化对象
  p = new pcl::visualization::PCLVisualizer (argc, argv, "Pairwise Incremental Registration example");
  p->createViewPort (0.0, 0, 0.5, 1.0, vp_1);
  p->createViewPort (0.5, 0, 1.0, 1.0, vp_2);

	PointCloud::Ptr result (new PointCloud), source, target;
  Eigen::Matrix4f GlobalTransform = Eigen::Matrix4f::Identity (), pairTransform;
  
  for (std::size_t i = 1; i < data.size (); ++i)
  {
    source = data[i-1].cloud;
    target = data[i].cloud;

   // 添加可视化数据
    showCloudsLeft(source, target);

    PointCloud::Ptr temp (new PointCloud);
    PCL_INFO ("Aligning %s (%zu) with %s (%zu).\n", data[i-1].f_name.c_str (), static_cast<std::size_t>(source->size ()), data[i].f_name.c_str (), static_cast<std::size_t>(target->size ()));
    pairAlign (source, target, temp, pairTransform, true);

    //将当前点云对转换为全局转换
    pcl::transformPointCloud (*temp, *result, GlobalTransform);

    // 更新全局转换
    GlobalTransform *= pairTransform;

    // 保存对齐的点对,转换到第一个点云的坐标下
    std::stringstream ss;
    ss << i << ".pcd";
    pcl::io::savePCDFile (ss.str (), *result, true);

  }
}

运行以上程序可以看到如下变化。
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  除了PCL中自带的ICP方法,当前较为前沿的快速鲁棒ICP方法可以参考以下这篇论文,其主要贡献在于使用了安德森加速将收敛速度加快。该文章在github上有开源代码以供学习参考。
[1] Zhang J , Y Yao, Deng B . Fast and Robust Iterative Closet Point[J]. 2020.


【博主简介】
  斯坦福的兔子,男,天津大学机械工程工学硕士。毕业至今从事光学三维成像及点云处理相关工作。因工作中使用的三维处理库为公司内部库,不具有普遍适用性,遂自学开源PCL库及其相关数学知识以备使用。谨此将自学过程与君共享。
博主才疏学浅,尚不具有指导能力,如有问题还请各位在评论处留言供大家共同讨论。
若前辈们有工作机会介绍欢迎私信,最好是北京。

评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值