검색어 ROS PCL에 대한 글을 관련성을 기준으로 정렬하여 표시합니다. 날짜순 정렬 모든 글 표시
검색어 ROS PCL에 대한 글을 관련성을 기준으로 정렬하여 표시합니다. 날짜순 정렬 모든 글 표시

2015년 8월 27일 목요일

ROS 기반 다중 객체 비전 인식

이 글은 이전 3차원 비전 기반 평면 객체 인식에 대한 글을 좀 더 개선해 2차로 정리한 글이다. 이 글은 아래를 참조하였다.
1. 개요
이전 글에서 객체 인식을 위해, PCL (point cloud library)를 사용하였다. PCL은 기본적인 3차원 포인트 클라우드 처리 기능이 포함되어 있다.

PCL (point cloud library)

물론, PCL이 모든 솔류션을 제공해주지는 않는다. 하지만, 기본적인 3차원 포인트 클라우드 필터링, 세그먼테이션 등의 기능을 제공해 줌으로, 이를 개발할 시간에, 다른 알고리즘을 구현할 수 있는 시간을 벌 수 있다.

2. ROS PCL
1. 메시지 유형
ROS에서 제공하는 PCL 토픽은 다음과 같은 데이터 구조를 사용한다.
  • sensor_msgs::PointCloud - 첫번째 채택된 ROS의 포인트 클라우드 메시지 구조이다. x, y, z 포인트 (모두 float형)를 포함하고 있다. 멀티 채널을 지원하며, 각 채널은 string 이름으로 구분한다. 이 메시지는 point_cloud_mapping 패키지에서 생성되며, ROS 1.0 이전 포맷 기반으로 데이터 가시화를 지원한다. 
  • sensor_msgs::PointCloud2 - 새로 개선된 ROS 포인트 클라우드 메시지이다. n-D 데이터를 표현할 수 있다. 포인트 값은 어떤 데이터 유형도 가능하다. 밀도, 높이, 폭, 2차원 이미지 등을 포함할 수 있다. 관련 구조는 PCL_March_2010.pdf 혹은 pcl_icra2010.pdf를 참고하라.
  • pcl::PointCloud<T> - PCL라이브러리의 핵심 포인트 클라우드 클래스이다. point_types.h 에 정의되어 있는 템플릿 클래스이다. 이 타입은 PointCloud2 메시지 타입과 유사하며, 헤더를 포함한다. 메시지와 포인트 템플릿 간 변환은 양방향(sensor_msgs::convertPointCloud2ToPointCloud and sensor_msgs::convertPointCloudToPointCloud2.)이며, 포인트 클라우드를 처리할 때는 이 유형이 더 나을 수 있다. 
2. PointCloud2 필드 이름
필드 이름과 설명은 다음과 같다.

  • x - the X Cartesian coordinate of a point (float32)
  • y - the Y Cartesian coordinate of a point (float32)
  • z - the Z Cartesian coordinate of a point (float32)
  • rgb - the RGB (24-bit packed) color at a point (uint32)
  • rgba - the A-RGB (32-bit packed) color at a point (uint32), the field name is unfortunately misleading
  • normal_x - the first component of the normal direction vector at a point (float32)
  • normal_y - the second component of the normal direction vector at a point (float32)
  • normal_z - the third component of the normal direction vector at a point (float32)
  • curvature - the surface curvature change estimate at a point (float32)
  • j1 - the first moment invariant at a point (float32)
  • j2 - the second moment invariant at a point (float32)
  • j3 - the third moment invariant at a point (float32)
  • boundary_point - the boundary property of a point (e.g., set to 1 if the point is lying on a boundary) (bool)
  • principal_curvature_x - the first component of the direction of the principal curvature at a point (float32)
  • principal_curvature_y - the second component of the direction of the principal curvature at a point (float32)
  • principal_curvature_z - the third component of the direction of the principal curvature at a point (float32)
  • pc1 - the magnitude of the first component of the principal curvature at a point (float32)
  • pc2 - the magnitude of the second component of the principal curvature at a point (float32)
  • pfh - the Point Feature Histogram (PFH) feature signature at a point (float32[])
  • fpfh - the Fast Point Feature Histogram (FPFH) feature signature at a point (float32[])
  • ...

3. 다른 유형으로 퍼블리쉬
다음은 다른 유형으로 퍼블리쉬하는 예이다.

ros::NodeHandle nh;
std::string topic = nh.resolveName("point_cloud");
uint32_t queue_size = 1;
// get your point cloud message from somewhere
sensor_msgs::PointCloud2 cloud_msg;

// to advertise you can do it like this (as above):
ros::Publisher pub = nh.advertise<sensor_msgs::PointCloud2>(topic, queue_size);

/// and publish the message
pub.publish(cloud_msg);
// Need to include the pcl ros utilities
#include "pcl_ros/point_cloud.h"

// you have an object already, eg with pcl::PointXYZRGB points
pcl::PointCloud<pcl::PointXYZRGB> cloud;

// create a templated publisher
ros::Publisher pub = nh.advertise<pcl::PointCloud<pcl::PointXYZRGB> > (topic, queue_size);

// and just publish the object directly
pub.publish(cloud);


3. 다중 객체 인식
여기서 부터는 ROS에서 관련 예제가 없으므로, 많은 시행 착오를 반복하면서 길을 찾아나가야 한다. 우선 몇몇 레퍼런스를 확인해 보았다. 다음 레퍼런스는 평면 추출과 관련된 기본적인 내용이 포함되어 있다. 다만, ROS에서는 메시지 처리를 위해 보완되어야 하는 부분이 있다.
다음 코드를 이전 글에서 개발했던 my_pcl_tutorial_node 소스로 덮어쓴다. 

#include <ros/ros.h>
// PCL specific includes
#include <sensor_msgs/PointCloud2.h>
#include <pcl_conversions/pcl_conversions.h>
#include <pcl/ros/conversions.h>
#include <pcl/point_cloud.h>
#include <pcl/point_types.h>

#include <pcl/sample_consensus/model_types.h>
#include <pcl/sample_consensus/method_types.h>
#include <pcl/segmentation/sac_segmentation.h>
#include <pcl/filters/extract_indices.h>

ros::Publisher pub;

void
cloud_cb (const sensor_msgs::PointCloud2ConstPtr& input)
{
  // Convert the sensor_msgs/PointCloud2 data to pcl/PointCloud
  pcl::PointCloud<pcl::PointXYZ> cloud;
  pcl::fromROSMsg (*input, cloud);

  pcl::ModelCoefficients coefficients;
  pcl::PointIndices inliers;
  // Create the segmentation object
  pcl::SACSegmentation<pcl::PointXYZ> seg;
  // Optional
  seg.setOptimizeCoefficients (true);
  // Mandatory
  seg.setModelType (pcl::SACMODEL_PLANE);
  seg.setMethodType (pcl::SAC_RANSAC);
  seg.setDistanceThreshold (0.01);

  seg.setInputCloud (cloud.makeShared ());
  seg.segment (inliers, coefficients);
 
  // Create the filtering object
  pcl::ExtractIndices<pcl::PointXYZ> extract;
   
  // Extract the inliers
  ROS_INFO("extract the inliers"); 
  pcl::PointCloud<pcl::PointXYZ> in_cloud;
  pcl::IndicesPtr indices(&inliers.indices);
  extract.setInputCloud (cloud.makeShared ());
  extract.setIndices (indices);
  extract.setNegative (false);
  extract.filter (in_cloud);
 
  // Convert To ROS data type
  ROS_INFO("convert to ros data type"); 
  pcl::PCLPointCloud2 cloud_p;
  pcl::toPCLPointCloud2(in_cloud, cloud_p);
  sensor_msgs::PointCloud2 output;
  pcl_conversions::fromPCL(cloud_p, output);
  pub.publish(output);
 
  ROS_INFO("published it.");
}

int
main (int argc, char** argv)
{
  // Initialize ROS
  ros::init (argc, argv, "my_pcl_tutorial");
  ros::NodeHandle nh;

  // Create a ROS subscriber for the input point cloud
  ros::Subscriber sub = nh.subscribe ("camera/depth/points", 1, cloud_cb);

  // Create a ROS publisher for the output model coefficients
  pub = nh.advertise<sensor_msgs::PointCloud2> ("pclplaneoutput", 1);
 
  // Spin
  ros::spin ();
}

실행해 보면, 생각했던 결과가 나오지 않는다. 메모리 free() 시 오류가 난다. 메시지 유형과 PCL의 유형 간 변환 시 고려해야 하는 분이 있어 보인다. 구글링을 통해, 스마트포인트로 인한 메모리 릭(pcl::IndicesPtr indices(&inliers.indices) 부분)을 삭제/수정하고 다시 실행해 보았다.

다음과 같이 잘 추출된다.


이제 다중 평면을 추출하기 위해, 다음과 같이 코딩해 보았다.

#include <ros/ros.h>
// PCL specific includes
#include <sensor_msgs/PointCloud2.h>
#include <pcl_conversions/pcl_conversions.h>
#include <pcl/ros/conversions.h>
#include <pcl/point_cloud.h>
#include <pcl/point_types.h>

#include <pcl/sample_consensus/model_types.h>
#include <pcl/sample_consensus/method_types.h>
#include <pcl/segmentation/sac_segmentation.h>
#include <pcl/filters/extract_indices.h>

ros::Publisher pub;

void
cloud_cb (const sensor_msgs::PointCloud2ConstPtr& input)
{
  // Convert the sensor_msgs/PointCloud2 data to pcl/PointCloud
  pcl::PointCloud<pcl::PointXYZ> cloud;
  pcl::fromROSMsg (*input, cloud);

  // Create the segmentation object
  pcl::SACSegmentation<pcl::PointXYZ> seg;
  // Optional
  seg.setOptimizeCoefficients (true);
  // Mandatory
  seg.setModelType (pcl::SACMODEL_PLANE);
  seg.setMethodType (pcl::SAC_RANSAC);
  seg.setDistanceThreshold (0.01);

  pcl::PointCloud<pcl::PointXYZI> TotalCloud;
  sensor_msgs::PointCloud2 output;
  for(int i = 0; i < 8; i++)
  {
      if(cloud.size() < 10000)
             break;
      seg.setInputCloud (cloud.makeShared ());
     
      pcl::ModelCoefficients coefficients;
      // pcl::PointIndices inliers;
      pcl::PointIndices::Ptr inliers(new pcl::PointIndices); 
      seg.segment (*inliers, coefficients);
     
      // Create the filtering object
      pcl::ExtractIndices<pcl::PointXYZ> extract;
   
      // Extract the inliers
      ROS_INFO("extract the inliers"); 
      pcl::PointCloud<pcl::PointXYZ> in_cloud;
      extract.setInputCloud (cloud.makeShared ());
      extract.setIndices (inliers);
      extract.setNegative (false);
      extract.filter (in_cloud);
      extract.setNegative (true);
      extract.filter (cloud);
     
      pcl::PointCloud<pcl::PointXYZ>::iterator index = in_cloud.begin();
      for(; index != in_cloud.end(); index++)
      {
         pcl::PointXYZ pt = *index;
         pcl::PointXYZI pt2;
         pt2.x = pt.x, pt2.y = pt.y, pt2.z = pt.z;
         pt2.intensity = (float)(i + 1);

         TotalCloud.push_back(pt2);
      }
     
      ROS_INFO("%d. remained point cloud = %d", i, (int)cloud.size()); 
  }
  
    // Convert To ROS data type 
  pcl::PCLPointCloud2 cloud_p;
  pcl::toPCLPointCloud2(TotalCloud, cloud_p); 
  pcl_conversions::fromPCL(cloud_p, output);
  output.header.frame_id = "camera_depth_optical_frame";   
  pub.publish(output); 
 
  ROS_INFO("published it."); 
}

int
main (int argc, char** argv)
{
  // Initialize ROS
  ros::init (argc, argv, "my_pcl_tutorial");
  ros::NodeHandle nh;

  // Create a ROS subscriber for the input point cloud
  ros::Subscriber sub = nh.subscribe ("camera/depth/points", 1, cloud_cb);

  // Create a ROS publisher for the output model coefficients
  // pub = nh.advertise<pcl_msgs::ModelCoefficients> ("pclplaneoutput", 1);
  pub = nh.advertise<sensor_msgs::PointCloud2> ("pclplaneoutput", 1);
 
  // Spin
  ros::spin ();
}

그리고, 빌드/실행해 보면, 다음과 같이 다중으로 평면 객체가 잘 추출되는 것을 확인할 수 있다.




4. 결론
PCL을 사용할 때, ROS 유형간의 변환에 주의할 필요가 있다. 이 부분의 솔류션을 알아내기 위해 약간 삽질이 있었는 데, PCL에서 extract하는 부분의 객체 유형을 달리해서 해결했다. 또한, PCL처리로 얻은 포인트 클라우드를 ROS유형으로 변환할 때, frame_id가 제대로 설정안되는 경우가 있다. 이 경우에는 수동으로 설정해 주어야 한다.

또 다른 주의점은 PCL은 포인터 레퍼런스를 관리하기 위해, 스마트 포인트 인자를 받도록 되어 있다. 이 부분을 잘 못 사용하면 메모리릭이 발생하거나, 시스템 폴트가 생긴다. 이런 경우, 찾아내기가 어려운데, ROS_INFO() 매크로와 소스 리마크를 잘 활용해 코드 하나 하나씩 확인해 보면 찾을 수 있다.

이 모든 조합을 확인하고, 솔류션 획득까지 걸린 시행착오와 코딩에 걸린 시간은 대략 5시간이다. 하지만, 무한삽질도 아닌 이 정도손실로 ROS에 불평하면 메이커라 말할 자격이 없을 것이다(스스로를 위로해 본다. 헐~).

ROS와 PCL 기반으로 다중 객체를 추출해 보고 나니 더욱 더 욕심이 생긴다. 다음 단계는 느린 추출 속도를 보완하기 위해 GPU를 고려해 보고, 더 다양한 형상을 추출해 본다. 고지를 향해서~

2015년 8월 29일 토요일

유클리드 거리 기반 3차원 포인트 클라우드 세그먼테이션

이 글은 ROS PCL의 처리 속도를 고려해, 유클리드 거리 기반 3차원 포인트 클라우드 세그먼테이션을 처리해 보도록 한다.

1. 개요
유클리스 거리 기반 클러스터링 방법은 주어진 포인트 클라우드를 거리에 따라 그룹핑하는 효과적인 방법이다. 점 간 거리만 계산하고, 탐색하면 되므로, 처리속도가 다른 클러스터링 알고리즘에 비해 매우 빠르다. 다만, 세밀한 클러스터링을 할 수 없는 단점이 있다.



2. 코딩 
다음 프로그램은 계산 성능을 좀 더 높이기 위해, 복셀(voxel)을 이용해, LOD(level of detail)을 조정하여, 포인트 클라우드의 계산양을 줄인 후, 클러스터링한다. ROS에서 다음과 같이 코딩하고 빌드한다.

#include <ros/ros.h>
// PCL specific includes
#include <sensor_msgs/PointCloud2.h>
#include <pcl_conversions/pcl_conversions.h>
#include <pcl/ros/conversions.h>
#include <pcl/point_cloud.h>
#include <pcl/point_types.h>

#include <pcl/ModelCoefficients.h>
#include <pcl/io/pcd_io.h>
#include <pcl/filters/extract_indices.h>
#include <pcl/filters/voxel_grid.h>
#include <pcl/features/normal_3d.h>
#include <pcl/kdtree/kdtree.h>
#include <pcl/sample_consensus/method_types.h>
#include <pcl/sample_consensus/model_types.h>
#include <pcl/segmentation/sac_segmentation.h>
#include <pcl/segmentation/extract_clusters.h>

ros::Publisher pub;

typedef pcl::PointXYZ PointT;

void
cloud_cb (const sensor_msgs::PointCloud2ConstPtr& input)
{
  // Convert the sensor_msgs/PointCloud2 data to pcl/PointCloud
  pcl::PointCloud<pcl::PointXYZ> cloud;
  pcl::fromROSMsg (*input, cloud);
 
  // Data containers used
  // Create the filtering object: downsample the dataset using a leaf size of 1cm
  pcl::VoxelGrid<pcl::PointXYZ> vg;
  pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_filtered (new pcl::PointCloud<pcl::PointXYZ>);
  vg.setInputCloud (cloud.makeShared());
  vg.setLeafSize (0.01f, 0.01f, 0.01f);
  vg.filter (*cloud_filtered);
  std::cout << "PointCloud after filtering has: " << cloud_filtered->points.size ()  << " data points." << std::endl;

  // Creating the KdTree object for the search method of the extraction
  pcl::search::KdTree<pcl::PointXYZ>::Ptr tree (new pcl::search::KdTree<pcl::PointXYZ>);
  tree->setInputCloud (cloud_filtered);

  std::vector<pcl::PointIndices> cluster_indices;
  pcl::EuclideanClusterExtraction<pcl::PointXYZ> ec;
  ec.setClusterTolerance (0.02); // 2cm
  ec.setMinClusterSize (100);
  ec.setMaxClusterSize (25000);
  ec.setSearchMethod (tree);
  ec.setInputCloud (cloud_filtered);
  ec.extract (cluster_indices);
 

  std::cout << "Number of clusters is equal to " << cluster_indices.size () << std::endl;
 
  pcl::PointCloud<pcl::PointXYZI> TotalCloud; 
  int j = 0;
  for (std::vector<pcl::PointIndices>::const_iterator it = cluster_indices.begin (); it != cluster_indices.end (); ++it)
  {
    for (std::vector<int>::const_iterator pit = it->indices.begin (); pit != it->indices.end (); ++pit)
    {
        pcl::PointXYZ pt = cloud_filtered->points[*pit];
            pcl::PointXYZI pt2;
            pt2.x = pt.x, pt2.y = pt.y, pt2.z = pt.z;
            pt2.intensity = (float)(j + 1);

            TotalCloud.push_back(pt2);
    }
    j++;
  }
  
    // Convert To ROS data type 
  pcl::PCLPointCloud2 cloud_p;
  pcl::toPCLPointCloud2(TotalCloud, cloud_p);
  
  sensor_msgs::PointCloud2 output; 
  pcl_conversions::fromPCL(cloud_p, output);
  output.header.frame_id = "camera_depth_optical_frame";   
  pub.publish(output); 
 
  ROS_INFO("published it."); 
}

int
main (int argc, char** argv)
{
  // Initialize ROS
  ros::init (argc, argv, "my_pcl_tutorial");
  ros::NodeHandle nh;

  // Create a ROS subscriber for the input point cloud
  ros::Subscriber sub = nh.subscribe ("camera/depth/points", 1, cloud_cb);

  // Create a ROS publisher for the output model coefficients
  // pub = nh.advertise<pcl_msgs::ModelCoefficients> ("pclplaneoutput", 1);
  pub = nh.advertise<sensor_msgs::PointCloud2> ("pclplaneoutput", 1);
 
  // Spin
  ros::spin ();
}


3. 실험
빌드 후 실행하면 다음과 같은 결과가 출력된다.


rviz에서 살펴보면, 다음과 같이 점 간 거리에 따라 적절히 그룹핑되었음을 확인할 수 있다.




실제 구분된 세그먼트까지 거리를 측정해 보자. XTion에서 파이프까지 거리는 대략 60cm이다 (줄자가 없어, 일단 자로 테스트한다).





rviz에서 axis를 추가한다. 축 길이는 비교하기 좋게 0.6 (60cm)로 한다.


그리고, measure명령으로 거리를 재어 보다. 아래와 같이 거의 비슷하게 거리값이 나오는 것을 확인할 수 있다.


각도도 대략 비교해 보았다. 큰 차이가 없어 보인다.



파이프 지름과 rviz상에 측정된 지름을 비교해 보았다. 파이프는 약 10cm지름을 가지므로, rviz측정 상에서 1.4 cm정도 오차가 있으며, 이는 파이프 반쪽면의 일부만 스캔된 점, LOD 처리로 포인트 밀도가 줄어든 점으로 인한 것이다. 




사다리를 놓고, 스캔을 해 보았다. 스캔된 가시영역과 그림자문제로 인해, 여러 개로 사다리가 세그먼테이션되는 것을 확인할 수 있다.


4. 결과 
이번에는 계산 성능을 고려해, 점 간 거리에 따른 클러스터링을 처리해 보았다. 성능은 다른 클러스터링 및 형상 추출 기법에 비해, 최소 20배 이상 빠르게 처리된다. 다만, 세밀한 단위로 클러스터링되지 않은 한계가 있다. 이는 앞의 글에서 설명한 다른 기법과 적절히 혼용하면 해결할 수 있다.

다중 실린더(Cylinder) 형상 추출

앞의 글에서는 다중 평면을 추출해 보았다. 이 글에서는 다중 실린더를 추출해 보도록 한다.

1. 개요
다중 실린더는 평면보다 처리할 것들이 더 많다. 실린더를 추출하기 전에 평면을 추출해, 제거해야 하며, 적절한 실린더 파라메터를 설정해 주어야 한다.

2. 코딩
다음과 같이 코딩해 본다. 추출할 반경은 0.05 미터에서 0.2 미터 반경 사이의 실린더만 추출해 본다 .빌드 및 실행 방법은 이전 글과 동일하다.

#include <ros/ros.h>
// PCL specific includes
#include <sensor_msgs/PointCloud2.h>
#include <pcl_conversions/pcl_conversions.h>
#include <pcl/ros/conversions.h>
#include <pcl/point_cloud.h>
#include <pcl/point_types.h>

#include <pcl/sample_consensus/model_types.h>
#include <pcl/sample_consensus/method_types.h>
#include <pcl/segmentation/sac_segmentation.h>
#include <pcl/filters/extract_indices.h>

#include <pcl/ModelCoefficients.h>
#include <pcl/filters/passthrough.h>
#include <pcl/features/normal_3d.h>

ros::Publisher pub;

typedef pcl::PointXYZ PointT;

void
cloud_cb (const sensor_msgs::PointCloud2ConstPtr& input)
{
  // Convert the sensor_msgs/PointCloud2 data to pcl/PointCloud
  pcl::PointCloud<pcl::PointXYZ> cloud;
  pcl::fromROSMsg (*input, cloud);

  // Create the segmentation object
  pcl::SACSegmentation<pcl::PointXYZ> seg;
  // Optional
  seg.setOptimizeCoefficients (true);
  // Mandatory
  seg.setModelType (pcl::SACMODEL_PLANE);
  seg.setMethodType (pcl::SAC_RANSAC);
  seg.setDistanceThreshold (0.01);

    // remove plane
  for(int i = 0; i < 4; i++)
  {
      if(cloud.size() < 10000)
             break;
      seg.setInputCloud (cloud.makeShared ());
     
      pcl::ModelCoefficients coefficients;
      // pcl::PointIndices inliers;
      pcl::PointIndices::Ptr inliers(new pcl::PointIndices); 
      seg.segment (*inliers, coefficients);
     
      // Create the filtering object
      pcl::ExtractIndices<pcl::PointXYZ> extract;
   
      // Extract the inliers
      ROS_INFO("extract the inliers"); 
      pcl::PointCloud<pcl::PointXYZ> in_cloud;
      extract.setInputCloud (cloud.makeShared ());
      extract.setIndices (inliers);
      extract.setNegative (false);
      extract.filter (in_cloud);
      extract.setNegative (true);
      extract.filter (cloud);
  }


  // Algorithm
  pcl::NormalEstimation<PointT, pcl::Normal> ne;
  pcl::SACSegmentationFromNormals<PointT, pcl::Normal> seg2;
  pcl::search::KdTree<PointT>::Ptr tree (new pcl::search::KdTree<PointT> ());

  // Datasets
  pcl::PointCloud<PointT>::Ptr cloud_filtered (new pcl::PointCloud<PointT>);
  pcl::PointCloud<pcl::Normal>::Ptr cloud_normals (new pcl::PointCloud<pcl::Normal>);
  pcl::ModelCoefficients::Ptr coefficients_cylinder (new pcl::ModelCoefficients);
  pcl::PointIndices::Ptr inliers_cylinder (new pcl::PointIndices);

  pcl::PointCloud<pcl::PointXYZI> TotalCloud;
  sensor_msgs::PointCloud2 output;
  for(int i = 0; i < 1; i++)
  {
        // Estimate point normals
        ne.setSearchMethod (tree);
        ne.setInputCloud (cloud.makeShared ());
        ne.setKSearch (50);
        ne.compute (*cloud_normals);

        // Create the segmentation object for cylinder segmentation and set all the parameters
        seg2.setOptimizeCoefficients (true);
        seg2.setModelType (pcl::SACMODEL_CYLINDER);
        seg2.setMethodType (pcl::SAC_RANSAC);
        seg2.setNormalDistanceWeight (0.1);
        seg2.setMaxIterations (1000);
        seg2.setDistanceThreshold (0.05);
        seg2.setRadiusLimits (0.05, 0.2);
        seg2.setInputCloud (cloud.makeShared ());
        seg2.setInputNormals (cloud_normals);

        // Obtain the cylinder inliers and coefficients
        seg2.segment (*inliers_cylinder, *coefficients_cylinder);
        std::cerr << "Cylinder coefficients: " << *coefficients_cylinder << std::endl;

        // Write the cylinder inliers to disk
        pcl::PointCloud<PointT>::Ptr cloud_cylinder (new pcl::PointCloud<PointT> ());
        pcl::ExtractIndices<PointT> extract;       
        extract.setInputCloud (cloud.makeShared ());
        extract.setIndices (inliers_cylinder);
        extract.setNegative (false);
        extract.filter (*cloud_cylinder);
       
      pcl::PointCloud<pcl::PointXYZ>::iterator index = cloud_cylinder->begin();
      for(; index != cloud_cylinder->end(); index++)
      {
         pcl::PointXYZ pt = *index;
         pcl::PointXYZI pt2;
         pt2.x = pt.x, pt2.y = pt.y, pt2.z = pt.z;
         pt2.intensity = (float)(i + 1);

         TotalCloud.push_back(pt2);
      }
     
      ROS_INFO("%d. cylinder point cloud = %d", i, (int)TotalCloud.size()); 
  }
  
    // Convert To ROS data type 
  pcl::PCLPointCloud2 cloud_p;
  pcl::toPCLPointCloud2(TotalCloud, cloud_p); 
  pcl_conversions::fromPCL(cloud_p, output);
  output.header.frame_id = "camera_depth_optical_frame";   
  pub.publish(output); 
 
  ROS_INFO("published it."); 
}

int
main (int argc, char** argv)
{
  // Initialize ROS
  ros::init (argc, argv, "my_pcl_tutorial");
  ros::NodeHandle nh;

  // Create a ROS subscriber for the input point cloud
  ros::Subscriber sub = nh.subscribe ("camera/depth/points", 1, cloud_cb);

  // Create a ROS publisher for the output model coefficients
  // pub = nh.advertise<pcl_msgs::ModelCoefficients> ("pclplaneoutput", 1);
  pub = nh.advertise<sensor_msgs::PointCloud2> ("pclplaneoutput", 1);
 
  // Spin
  ros::spin ();
}

3. 실행 결과
실행 결과는 다음과 같다. PCL에서 7개의 파라메터가 추출되는 데, 앞의 3개 값은 실린더 축의 첫번째 좌표 P1, 다음 3개 값은 두번째 좌표 P2, 마지막 값은 반경이다. 0.18 반경을 가진 실린더가 추출되었음을 알 수 있다.


ROS rviz로 확인해 보면, 실린더와 유사한 형상이 추출되었음을 확인할 수 있다. 



4. 실험
이제 몇가지 실험을 해 본다. 실험을 위해 반경 0.05 m 인 파이프를 준비한다.




실행해 테스트해 보면, 다음과 같이 0.04 m 반경을 가진 파이프를 인식한 것을 확인할 수 있다. 인식 오차는 1 cm 정도이다.


rviz에서 확인해 보자. 우선 파이프를 추출하기 전 포인트 클라우드이다.





파이프를 인식해 보면, 앞의 포인트 클라우드에서 파이프 부분을 제대로 추출하였음을 확인할 수 있다. 다만, 일부 평면이 추출되었는 데, 이는 실린더 추출 시 설정 파라메터에서, 속도 문제로 반복횟수를 1000으로 낮추는 바람에, 허용오차가 제대로 고려되지 않은 문제로 보인다. 반경의 오차도 같은 이유로 높아진 원인이 있다. 물론, 불완전한 포인트 클라우드도 반경 오차에 영향을 미치고 있다. 





이제   for(int i = 0; i < 1; i++)  부분을 두번 실행되도록 2로 수정한다. 그리고 빌드해 실행해 본다. 그럼 포인트 클라우드에서 설정된 파라메터 조건에 맞는 추출된 2개의 실런더 형상을 확인할 수 있을 것이다 .



5. 결론
앞의 시험 결과, 처리 성능만 개선한다면, 파이프 파라메터와 피팅(fitting)율을 높일 수 있기 때문에, 파이프 추출 시 정밀도 등을 높일 수 있다. 그럼에도 불구하고, 평면에 비해, 실린더는 형상 인식에 더 많은 계산 시간이 필요하여, 실시간성이 요구되는 응용에는 적합하지 않을 수 있다.


3차원 포인트 클라우드 스캔 기반 객체 추적

이 글은 앞의 내용을 종합해, 3차원 포인트 클라우드를 입력받아, 스캔 기반으로 객체를 추적하는 방법을 개발해 본다.

1. 개요
객체를 추적하기 위해서는 포인트 클라우드를 적절히 세그먼테이션해야 하며, 각 세그먼트의 물성(중심점, AABB 정보 등)을 추출해야 한다. 각 세그먼트는 RANSAC등의 방법을 통해, 평면, 실린더 등 객체로 구분할 수 있다.

여기서는 단순히, 유클리디언 거리로 포인트 클라우드를 세그먼테이션하고, 세그먼트의 중심점 및 최대 영역을 획득해 본다.

2. 코딩
앞의 글과 같이 PCL 패키지를 생성해 보고, node의 소스를 다음과 같이 코딩한다.

#include <ros/ros.h>
// PCL specific includes
#include <sensor_msgs/PointCloud2.h>
#include <pcl_conversions/pcl_conversions.h>
#include <pcl/ros/conversions.h>
#include <pcl/point_cloud.h>
#include <pcl/point_types.h>

#include <pcl/ModelCoefficients.h>
#include <pcl/io/pcd_io.h>
#include <pcl/filters/extract_indices.h>
#include <pcl/filters/voxel_grid.h>
#include <pcl/features/normal_3d.h>
#include <pcl/kdtree/kdtree.h>
#include <pcl/sample_consensus/method_types.h>
#include <pcl/sample_consensus/model_types.h>
#include <pcl/segmentation/sac_segmentation.h>
#include <pcl/segmentation/extract_clusters.h>
// #include <pcl/features/moment_of_inertia_estimation.h>

ros::Publisher pub, pub_obj;

typedef pcl::PointXYZ PointT;

void
cloud_cb (const sensor_msgs::PointCloud2ConstPtr& input)
{
  // Convert the sensor_msgs/PointCloud2 data to pcl/PointCloud
  pcl::PointCloud<pcl::PointXYZ> cloud;
  pcl::fromROSMsg (*input, cloud);
 
  // Data containers used
  // Create the filtering object: downsample the dataset using a leaf size of 1cm
  pcl::VoxelGrid<pcl::PointXYZ> vg;
  pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_filtered (new pcl::PointCloud<pcl::PointXYZ>);
  vg.setInputCloud (cloud.makeShared());
  vg.setLeafSize (0.01f, 0.01f, 0.01f);
  vg.filter (*cloud_filtered);
  std::cout << "PointCloud after filtering has: " << cloud_filtered->points.size ()  << " data points." << std::endl;

  // Creating the KdTree object for the search method of the extraction
  pcl::search::KdTree<pcl::PointXYZ>::Ptr tree (new pcl::search::KdTree<pcl::PointXYZ>);
  tree->setInputCloud (cloud_filtered);

  std::vector<pcl::PointIndices> cluster_indices;
  pcl::EuclideanClusterExtraction<pcl::PointXYZ> ec;
  ec.setClusterTolerance (0.02); // 2cm
  ec.setMinClusterSize (100);
  ec.setMaxClusterSize (25000);
  ec.setSearchMethod (tree);
  ec.setInputCloud (cloud_filtered);
  ec.extract (cluster_indices);
 

  std::cout << "Number of clusters is equal to " << cluster_indices.size () << std::endl;
 
  // pcl::MomentOfInertiaEstimation <pcl::PointXYZ> feature_extractor;
 
  pcl::PointCloud<pcl::PointXYZI> TotalCloud, ObjectCloud; 
  int j = 0;
  for (std::vector<pcl::PointIndices>::const_iterator it = cluster_indices.begin (); it != cluster_indices.end (); ++it)
  {
      pcl::PointCloud<pcl::PointXYZI>::Ptr segment(new pcl::PointCloud<pcl::PointXYZI> ());

        pcl::PointXYZI min_point_AABB, max_point_AABB, mass_center;
        // Eigen::Vector3f mass_center;
        min_point_AABB.x = min_point_AABB.y = min_point_AABB.z = 10000000000.0;
      max_point_AABB.x = max_point_AABB.y = max_point_AABB.z = -10000000000.0;

    for (std::vector<int>::const_iterator pit = it->indices.begin (); pit != it->indices.end (); ++pit)
    {
        pcl::PointXYZ pt = cloud_filtered->points[*pit];
            pcl::PointXYZI pt2;
            pt2.x = pt.x, pt2.y = pt.y, pt2.z = pt.z;
            pt2.intensity = (float)(j + 1);

            TotalCloud.push_back(pt2);
            segment->push_back(pt2);
           
            min_point_AABB.x = std::min(min_point_AABB.x, pt.x);
            min_point_AABB.y = std::min(min_point_AABB.y, pt.y);
            min_point_AABB.z = std::min(min_point_AABB.z, pt.z);
            min_point_AABB.intensity = (float)(j + 1);

            max_point_AABB.x = std::max(max_point_AABB.x, pt.x);
            max_point_AABB.y = std::max(max_point_AABB.y, pt.y);
            max_point_AABB.z = std::max(max_point_AABB.z, pt.z);
            max_point_AABB.intensity = (float)(j + 1);           
    }
         
        /* feature_extractor.setInputCloud (segment);
        feature_extractor.compute ();


        feature_extractor.getAABB (min_point_AABB, max_point_AABB); 
        feature_extractor.getMassCenter (mass_center);   */
       
        mass_center.x = (max_point_AABB.x - min_point_AABB.x) / 2.0 + min_point_AABB.x;
        mass_center.y = (max_point_AABB.y - min_point_AABB.y) / 2.0 + min_point_AABB.y;
        mass_center.z = (max_point_AABB.z - min_point_AABB.z) / 2.0 + min_point_AABB.z;
        mass_center.intensity = (float)(j + 1);               
                       
        ObjectCloud.push_back(min_point_AABB);
        ObjectCloud.push_back(max_point_AABB);
        ObjectCloud.push_back(mass_center);
   
    j++;
  }

    // Convert To ROS data type 
  pcl::PCLPointCloud2 cloud_p;
  pcl::toPCLPointCloud2(TotalCloud, cloud_p);
  
  sensor_msgs::PointCloud2 output; 
  pcl_conversions::fromPCL(cloud_p, output);
  output.header.frame_id = "camera_depth_optical_frame";   
  pub.publish(output);

    // Convert To ROS data type 
  pcl::PCLPointCloud2 cloud_p2;
  pcl::toPCLPointCloud2(ObjectCloud, cloud_p2);
  
  sensor_msgs::PointCloud2 output2; 
  pcl_conversions::fromPCL(cloud_p2, output2);
  output2.header.frame_id = "camera_depth_optical_frame";   
  pub_obj.publish(output2);   

 
  ROS_INFO("published it."); 
}

int
main (int argc, char** argv)
{
  // Initialize ROS
  ros::init (argc, argv, "my_pcl_tutorial");
  ros::NodeHandle nh;

  // Create a ROS subscriber for the input point cloud
  ros::Subscriber sub = nh.subscribe ("camera/depth/points", 1, cloud_cb);

  // Create a ROS publisher for the segments
  pub = nh.advertise<sensor_msgs::PointCloud2> ("pclsegments", 1);

  // Create a ROS publisher for the objects
  pub_obj = nh.advertise<sensor_msgs::PointCloud2> ("pclobjects", 1);
 
  // Spin
  ros::spin ();
}

3. 실험
실행하면, 다음과 같이 세그먼테이션되고, 각 세그먼트의 중심점과 최대 영역점이 8 pixel의 점으로 표시된다.


4. 결론
특정 객체를 찾은 후에, 객체의 물성을 획득하면, 그 객체를 추적하거나, 그립하는 것이 가능하다. 이와 엑추에이터를 연계하기 위해서는 PID제어(앞의 글 참고)가 고려되어야 한다.
또한, moment_of_inertia_estimation.h와 같은 최신 버전의 API가 Indigo PCL버전에서는 제공되지 않는 것은 아쉬움이 있다.