前回のチュートリアルで紹介したテクニックのほとんどは、前処理に焦点を当てていた。つまり、クラウド上である種の操作を行い、それ以降の分析や作業の準備を整えていた。 ダウンサンプリング、外れ値の除去、サーフェススムージング、法線の推定...
このチュートリアルでは準備段階の後にポイントクラウドを使用した面白いトピックを扱う:登録、セグメント化、モデルマッチング…
登録とは、パズルのピースのような2点のクラウドを整列させる技術である。正確に言うと、アルゴリズムはこれらの間の対応関係のセットを見つけるものである。それができるということは、両方のクラウドに共通するシーンの領域があることを意味する。線形変換を計算し、回転と平行移動を含む行列を出力する。共通する領域が重なるようにこれらの操作を一方のクラウドに適用することにより、もう一方のクラウドとの整合をとることができる。
最良の結果は非常に似ているクラウドにより得られるので、それらの間の変換を最小限に抑えるようにする。これが意味することは、Kinectを手に持って走りながらフレームを取得し、そのクラウドの照合をPCLに期待する、ということはできないということである。センサーを一定間隔で静に動かす方が良いのである。正確に角度を刻めるロボットアームや回転テーブルを使用できる場合はさらに良い。
シーンやオブジェクトの完全で完璧な連続モデルを取得できるため、登録は非常に便利なテクニックである。 CPUの負荷も高い。そこで KinFuのように、GPUを用いてクラウドの照合をリアルタイムで可能にする最適化が開発されている。
登録アルゴリズムの処理を示すフローチャート: 繰り返しにおける一つの処理において、「データ取得→キーポイント推定→特徴量記述推定」(2つの処理が合流)→対応関係推定(照合)→「対応関係否定手法1 ...対応関係否定手法N」→変換行列推定
上の図は、手続きの実行方法を示している。主に2つの方法から選択できる。
クラウドが大きく違うとICPは失敗する。通常、クラウドの最初の大まかな整列を得るために最初に特徴量を使用し、その後ICPを使用して精密に整合する。特徴量ベースの登録は、3D物体の認識問題と基本的に同じなので、ここでは検討しない。特徴量、記述子、認識については、次のチュートリアルで扱う。
PCLは、ICPアルゴリズムに対し複数の実装を提供している。 ここでは最適化されていない一般的なAPIを示すが、APIセクションにあげられている最適化バージョンを試してみようと思うことだろう。 2つのポイントクラウドを整列(登録)するコードは次のとおりである:
#include <pcl/io/pcd_io.h>
#include <pcl/registration/icp.h>
#include <iostream>
int
main(int argc, char** argv)
{
// Objects for storing the point clouds. ポイントクラウドの格納用オブジェクト
pcl::PointCloud<pcl::PointXYZ>::Ptr sourceCloud(new pcl::PointCloud<pcl::PointXYZ>);
pcl::PointCloud<pcl::PointXYZ>::Ptr targetCloud(new pcl::PointCloud<pcl::PointXYZ>);
pcl::PointCloud<pcl::PointXYZ>::Ptr finalCloud(new pcl::PointCloud<pcl::PointXYZ>);
// Read two PCD files from disk. ハードディスクからPCDファイルを2個読み込む
if (pcl::io::loadPCDFile<pcl::PointXYZ>(argv[1], *sourceCloud) != 0)
{
return -1;
}
if (pcl::io::loadPCDFile<pcl::PointXYZ>(argv[2], *targetCloud) != 0)
{
return -1;
}
// ICP object. ICPオブジェクト
pcl::IterativeClosestPoint<pcl::PointXYZ, pcl::PointXYZ> registration;
registration.setInputSource(sourceCloud);
registration.setInputTarget(targetCloud);
registration.align(*finalCloud);
if (registration.hasConverged())
{
std::cout << "ICP converged." << std::endl
<< "The score is " << registration.getFitnessScore() << std::endl;
std::cout << "Transformation matrix:" << std::endl;
std::cout << registration.getFinalTransformation() << std::endl;
}
else std::cout << "ICP did not converge." << std::endl;
}
"finalCloud"に保存されたクラウドは "sourceCloud"と同じである(同じポイントを持つ)が、ICPで見つかった変換が適用されているため、 "targetCloud"の隣で可視化すると、正しく適合する(エラーは避けられず、 センサの品質、クラウド間の距離、使用されるアルゴリズム...などに依存する)
(左)ソース・クラウド (右)ターゲット・クラウド
(左)登録前に2つのクラウドを一緒に表示
(右)登録後に2つのクラウドを一緒に表示
あるデータセットが与えられた場合、RANSAC(Random Sample Consensus)と呼ばれる反復メソッドを使用して、データの一部が特定の数学モデルに適合するかどうか判断できる。 RANSACは、データが適合値(多少のノイズがあってもモデルに調整できるデータ)と外れ値(モデルにはまったく適合しないデータ)を含んでいると仮定して動作する。 このアルゴリズムは非決定論的である。反復ごとに、結果の精度が改善される。 正確にいえば、結果の正しさの可能性は高くなるものの、100%になることはない。
(左)適合値と外れ値を含む、線形モデルに適合するデータポイント
(右)RANSACを適用して線形モデルのパラメータを見つけ出し、外れ値(赤丸)を破棄した後の同じデータ
PCLは、ポイントクラウドで動作するRANSACの実装を提供している。 たとえば、クラウドのどの点が球面モデルに適合しているかを調べることができる。この手順では、そのモデルのパラメータが返される。 以下にその方法を示す:
#include <pcl/io/pcd_io.h>
#include <pcl/sample_consensus/ransac.h>
#include <pcl/sample_consensus/sac_model_sphere.h>
int
main(int argc, char** argv)
{
// Objects for storing the point clouds. ポイントクラウドの格納用オブジェクト
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);
pcl::PointCloud<pcl::PointXYZ>::Ptr inlierPoints(new pcl::PointCloud<pcl::PointXYZ>);
// Read a PCD file from disk. ハードディスクからPCDファイルを読み込む
if (pcl::io::loadPCDFile<pcl::PointXYZ>(argv[1], *cloud) != 0)
{
return -1;
}
// RANSAC objects: model and algorithm. RANSACのオブジェクト: モデルとアルゴリズム
pcl::SampleConsensusModelSphere<pcl::PointXYZ>::Ptr model(new pcl::SampleConsensusModelSphere<pcl::PointXYZ>(cloud));
pcl::RandomSampleConsensus<pcl::PointXYZ> ransac(model);
// Set the maximum allowed distance to the model. モデルへの距離として許される最大値
ransac.setDistanceThreshold(0.01);
ransac.computeModel();
std::vector<int> inlierIndices;
ransac.getInliers(inlierIndices);
// Copy all inliers of the model to another cloud. モデルの適合値すべてを別なクラウドにコピーする
pcl::copyPointCloud<pcl::PointXYZ>(*cloud, inlierIndices, *inlierPoints);
}
選べるモデルはたくさんある。最も重要なものを以下に上げる:
またRANSACクラスは、その動作を決める関数を複数提供している。 たとえば、 "setMaxIterations()"を使用すると、距離閾値の代わりに、アルゴリズムを実行する最大反復回数を指定できる(停止条件を設定しない場合、RANSACは永遠に実行され、毎回少しずつ良い結果を生み出す)。
モデル係数を復元後、クラウドのポイントをそのモデルに投影することができる。 例えば、モデルが平面の場合、投射後は、クラウドの点は全て平面上にのるであろう。
#include <pcl/io/pcd_io.h>
#include <pcl/sample_consensus/method_types.h>
#include <pcl/sample_consensus/model_types.h>
#include <pcl/segmentation/sac_segmentation.h>
#include <pcl/filters/extract_indices.h>
#include <pcl/filters/project_inliers.h>
#include <pcl/visualization/cloud_viewer.h>
#include <iostream>
int
main(int argc, char** argv)
{
// Objects for storing the point clouds. ポイントクラウドの格納用オブジェクト
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);
pcl::PointCloud<pcl::PointXYZ>::Ptr cloudNoPlane(new pcl::PointCloud<pcl::PointXYZ>);
pcl::PointCloud<pcl::PointXYZ>::Ptr planePoints(new pcl::PointCloud<pcl::PointXYZ>);
pcl::PointCloud<pcl::PointXYZ>::Ptr projectedPoints(new pcl::PointCloud<pcl::PointXYZ>);
// Read a PCD file from disk. ハードディスクからPCDファイルを読み込む
if (pcl::io::loadPCDFile<pcl::PointXYZ>(argv[1], *cloud) != 0)
{
return -1;
}
// Get the plane model, if present. もしもあれば平面モデルをとってくる
pcl::ModelCoefficients::Ptr coefficients(new pcl::ModelCoefficients);
pcl::SACSegmentation<pcl::PointXYZ> segmentation;
segmentation.setInputCloud(cloud);
segmentation.setModelType(pcl::SACMODEL_PLANE);
segmentation.setMethodType(pcl::SAC_RANSAC);
segmentation.setDistanceThreshold(0.01);
segmentation.setOptimizeCoefficients(true);
pcl::PointIndices::Ptr inlierIndices(new pcl::PointIndices);
segmentation.segment(*inlierIndices, *coefficients);
if (inlierIndices->indices.size() == 0)
std::cout << "Could not find a plane in the scene." << std::endl;
else
{
std::cerr << "Plane coefficients: " << coefficients->values[0] << " "
<< coefficients->values[1] << " "
<< coefficients->values[2] << " "
<< coefficients->values[3] << std::endl;
// Create a second point cloud that does not have the plane points.
// Also, extract the plane points to visualize them later.
// 平面のポイントを『含まない」二番目のポイントクラウドを作成。また後で視覚化するためその平面ポイントを抽出
pcl::ExtractIndices<pcl::PointXYZ> extract;
extract.setInputCloud(cloud);
extract.setIndices(inlierIndices);
extract.filter(*planePoints);
extract.setNegative(true);
extract.filter(*cloudNoPlane);
// Object for projecting points onto a model. モデルにポイントを投影するためのオブジェクト
pcl::ProjectInliers<pcl::PointXYZ> projection;
// We have to specify what model we used. どのようなモデルを使用するか記述しなければならない
projection.setModelType(pcl::SACMODEL_PLANE);
projection.setInputCloud(cloudNoPlane);
// And we have to give the coefficients we got. 得られた係数を与えなければならない
projection.setModelCoefficients(coefficients);
projection.filter(*projectedPoints);
// Visualize everything. すべてを視覚化する
pcl::visualization::CloudViewer viewerPlane("Plane");
viewerPlane.showCloud(planePoints);
while (!viewerPlane.wasStopped())
{
// Do nothing but wait. ただ待つだけ
}
pcl::visualization::CloudViewer viewerProjection("Projected points");
viewerProjection.showCloud(projectedPoints);
while (!viewerProjection.wasStopped())
{
// Do nothing but wait. ただ待つだけ
}
}
}
例を実行すると、すべての点が平面上に「平坦化」されていることがわかる(シーン内に点があれば)。 プログラムはまず、平面に適合するポイントを表示する。 Alt + F4、またはQで終了すると、投影の結果を表示する2番目のウィンドウが開く。
セグメンテーションとは、クラウドをいろいろな部分に分割することであり、分割されたポイントの集まりのことをクラスタと呼ぶ(このため、クラスタリングとも呼ばれる)。元となる考えは、クラウドをいくつかの部分に分割し、独立して処理することである。理想的には、すべてのクラスタは論理的に「オブジェクト」である。たとえば、テーブルの上に3個の箱があるようなクラウドの場合は、テーブルと3個の箱の合計4つのクラスタが作成される。
セグメンテーション手法はたくさんあり、それぞれにポイントをグループ化するための独自の基準がある。ポイント間の距離を考慮したり、法線やテクスチャを考慮したりするものもある。ここでは、それぞれの概要を説明する。
セグメンテーションはこれまでに見られた他のメソッドと同様に、シーンから個々のオブジェクトのモデルを取得することができる。また、平面や円柱のような特定の形状を分割することもできる。セグメンテーション技術を実践するためにポイントクラウドが必要な場合は、OSD(オブジェクト・セグメンテーション・データベース)を参照すること。
ユークリッド・セグメンテーションは最も単純である。 2点間の距離を調べ、それが閾値よりも小さいければ、2点とも同じクラスタに属するとみなす。塗りつぶし(flood fill)アルゴリズムのように動作する。クラウド内のポイントは、クラスタに対して「選択済み」としてマークされる。 その後、ウイルスのように、十分近い距離にある他の点に広がっていく。新なた点が追加されなくなるまで、この処理を続ける。 次に、新しいクラスタを初期化し、マークされていない残りの点からこの手順を繰り返す。 PCLでは、ユークリッド・セグメンテーションは次のように行われる。
#include <pcl/io/pcd_io.h>
#include <pcl/segmentation/extract_clusters.h>
#include <iostream>
int
main(int argc, char** argv)
{
// Object for storing the point cloud. ポイントクラウドの格納用オブジェクト
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);
// Read a PCD file from disk. ハードディスクからPCDファイルを読み込む
if (pcl::io::loadPCDFile<pcl::PointXYZ>(argv[1], *cloud) != 0)
{
return -1;
}
// kd-tree object for searches. 探索のためのkd木のオブジェクト
pcl::search::KdTree<pcl::PointXYZ>::Ptr kdtree(new pcl::search::KdTree<pcl::PointXYZ>);
kdtree->setInputCloud(cloud);
// Euclidean clustering object. ユークリッドクラスタリングのオブジェクト
pcl::EuclideanClusterExtraction<pcl::PointXYZ> clustering;
// Set cluster tolerance to 2cm (small values may cause objects to be divided
// in several clusters, whereas big values may join objects in a same cluster).
// クラスタ許容値を2cmに設定(値が小さいと複数のクラスタにオブジェクトが分割される、
// 大きいと同じクラスタにすべてのポイントが入ってしまう)
clustering.setClusterTolerance(0.02);
// Set the minimum and maximum number of points that a cluster can have.
// 一つのクラスタがモテるポイントの最小個数と最大個数を設定
clustering.setMinClusterSize(100);
clustering.setMaxClusterSize(25000);
clustering.setSearchMethod(kdtree);
clustering.setInputCloud(cloud);
std::vector<pcl::PointIndices> clusters;
clustering.extract(clusters);
// For every cluster... どのクラスタに対しても
int currentClusterNum = 1;
for (std::vector<pcl::PointIndices>::const_iterator i = clusters.begin(); i != clusters.end(); ++i)
{
// ...add all its points to a new cloud... そしてそのポイントを新たなクラウドに
pcl::PointCloud<pcl::PointXYZ>::Ptr cluster(new pcl::PointCloud<pcl::PointXYZ>);
for (std::vector<int>::const_iterator point = i->indices.begin(); point != i->indices.end(); point++)
cluster->points.push_back(cloud->points[*point]);
cluster->width = cluster->points.size();
cluster->height = 1;
cluster->is_dense = true;
// ...and save it to disk. それをハードディスクに格納
if (cluster->points.size() <= 0)
break;
std::cout << "Cluster " << currentClusterNum << " has " << cluster->points.size() << " points." << std::endl;
std::string fileName = "cluster" + boost::to_string(currentClusterNum) + ".pcd";
pcl::io::savePCDFileASCII(fileName, *cluster);
currentClusterNum++;
}
}
良い結果が得られるまで、クラスター許容値パラメータを用いて処理を続ける必要がある。 また、シーンから床やテーブルを取り除くために平面分割(後述する)を行うことを勧める。そうしないと、結果が正しくない可能性がああるからである。
プログラムが終了すれば、以下のコマンドで一遍にすべてのクラスタを視覚化することができる。
pcl_viewer cluster*
(左)元のクラウド (右)ユークリッド・セグメンテーション後のクラスタ
すべてのセグメンテーション・オブジェクトは "setIndices()"関数を提供している。 これによりクラウド全体ではなく、クラスタリングを適用するポイントのインデックスを指定できる。
条件付きユークリッド・セグメンテーションは、1つの例外を除いて、上記の標準的なセグメンテーションと同じように動作する。距離チェックとは別に、ポイントはクラスターに追加されたカスタム要件を満たす必要がある。 この条件はユーザー指定である。 すべての2点の組(最初の点をシードといいこれが処理対象、2番目の点を候補といい、シードの近傍の点)それぞれに対し、カスタム関数が呼び出される。 この関数には特定のシグネチャがあり、以下を受け取り:
1. 2つのポイントのコピー。これによりテストが実行できる
2. 2点間の二乗距離
そしてブール値を返す。 値がtrueの場合に候補をクラスターに追加でき、falseの場合は距離チェックに合格してもクラスターに追加しない。
#include <pcl/io/pcd_io.h>
#include <pcl/segmentation/conditional_euclidean_clustering.h>
#include <iostream>
// If this function returns true, the candidate point will be added to the cluster of the seed point.
// この関数がtrueを返せば、候補のポイントがシード・ポイントのクラスタに追加される
bool
customCondition(const pcl::PointXYZ& seedPoint, const pcl::PointXYZ& candidatePoint, float squaredDistance)
{
// Do whatever you want here.
if (candidatePoint.y < seedPoint.y)
return false;
return true;
}
int
main(int argc, char** argv)
{
// Object for storing the point cloud. ポイントクラウドの格納用オブジェク
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);
// Read a PCD file from disk. ハードディスクからPCDファイルを読み込む
if (pcl::io::loadPCDFile<pcl::PointXYZ>(argv[1], *cloud) != 0)
{
return -1;
}
// Conditional Euclidean clustering object. 条件付きユークリッドクラスタリングのオブジェクト
pcl::ConditionalEuclideanClustering<pcl::PointXYZ> clustering;
clustering.setClusterTolerance(0.02);
clustering.setMinClusterSize(100);
clustering.setMaxClusterSize(25000);
clustering.setInputCloud(cloud);
// Set the function that will be called for every pair of points to check. ポイントのペアに対するチェック関数を設定
clustering.setConditionFunction(&customCondition);
std::vector<pcl::PointIndices> clusters;
clustering.segment(clusters);
// For every cluster... どのクラスタに対しても…
int currentClusterNum = 1;
for (std::vector<pcl::PointIndices>::const_iterator i = clusters.begin(); i != clusters.end(); ++i)
{
// ...add all its points to a new cloud... ...すべてのポイントを新たなクラウドに追加する...
pcl::PointCloud<pcl::PointXYZ>::Ptr cluster(new pcl::PointCloud<pcl::PointXYZ>);
for (std::vector<int>::const_iterator point = i->indices.begin(); point != i->indices.end(); point++)
cluster->points.push_back(cloud->points[*point]);
cluster->width = cluster->points.size();
cluster->height = 1;
cluster->is_dense = true;
// ...and save it to disk. そしてハードディスクに格納
if (cluster->points.size() <= 0)
break;
std::cout << "Cluster " << currentClusterNum << " has " << cluster->points.size() << " points." << std::endl;
std::string fileName = "cluster" + boost::to_string(currentClusterNum) + ".pcd";
pcl::io::savePCDFileASCII(fileName, *cluster);
currentClusterNum++;
}
}
上で実装された条件(候補のY座標がシードのY座標よりも小さいことのチェック)はあまり意味がなく、これはメソッドの仕組みを理解してほしいためだけのものである。
"pcl::ConditionalEuclideanClustering"のコンストラクタに "true"を渡すと、オブジェクトは小さすぎたり大きすぎたりするために破棄されたクラスタを記憶する。 そして後でgetRemovedClusters()関数を使用してそれらを取り出すことができる。
このタイプのセグメンテーション(ユークリッドのようなグリーディな塗りつぶし手法でもある)は、滑らかさ制約に従いポイントをグループ化する。つまり、法線の角度や曲率の差が、ポイントが同じ滑らかな表面に属すかどうかの検査のために使用される。ここでテーブルの上に置かれた箱を考えてみる:ユークリッド・セグメンテーションでは、これらは『接触」しているので、両方のポイントが同じクラスター内にあるとみなされる。しかし、領域成長セグメンテーションでは、テーブル内の点の法線と箱の側面の法線の角度の差は90°(もちろん理想的な法線推定で)であるため、同じクラスターにはならない。
#include <pcl/io/pcd_io.h>
#include <pcl/search/kdtree.h>
#include <pcl/features/normal_3d.h>
#include <pcl/segmentation/region_growing.h>
#include <iostream>
int
main(int argc, char** argv)
{
// Object for storing the point cloud. ポイントクラウドの格納用オブジェクト
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);
// Object for storing the normals. 法線の格納用オブジェクト
pcl::PointCloud<pcl::Normal>::Ptr normals(new pcl::PointCloud<pcl::Normal>);
// Read a PCD file from disk. ハードディスクからPCDファイルを読み込む
if (pcl::io::loadPCDFile<pcl::PointXYZ>(argv[1], *cloud) != 0)
{
return -1;
}
// kd-tree object for searches. 探索のためのkd木オブジェクト
pcl::search::KdTree<pcl::PointXYZ>::Ptr kdtree(new pcl::search::KdTree<pcl::PointXYZ>);
kdtree->setInputCloud(cloud);
// Estimate the normals. 法線を推定
pcl::NormalEstimation<pcl::PointXYZ, pcl::Normal> normalEstimation;
normalEstimation.setInputCloud(cloud);
normalEstimation.setRadiusSearch(0.03);
normalEstimation.setSearchMethod(kdtree);
normalEstimation.compute(*normals);
// Region growing clustering object. 領域成長クラスタリングのオブジェクト
pcl::RegionGrowing<pcl::PointXYZ, pcl::Normal> clustering;
clustering.setMinClusterSize(100);
clustering.setMaxClusterSize(10000);
clustering.setSearchMethod(kdtree);
clustering.setNumberOfNeighbours(30);
clustering.setInputCloud(cloud);
clustering.setInputNormals(normals);
// Set the angle in radians that will be the smoothness threshold 滑らかさの閾値の角度をラディアンで設定
// (the maximum allowable deviation of the normals).
clustering.setSmoothnessThreshold(7.0 / 180.0 * M_PI); // 7 degrees. 7度をラディアンに
// Set the curvature threshold. The disparity between curvatures will be
// tested after the normal deviation check has passed.
// 曲率の閾値の設定。標準偏差の検査が通ったあとで曲率差が調べられる
clustering.setCurvatureThreshold(1.0);
std::vector <pcl::PointIndices> clusters;
clustering.extract(clusters);
// For every cluster... どのクラスタに対しても...
int currentClusterNum = 1;
for (std::vector<pcl::PointIndices>::const_iterator i = clusters.begin(); i != clusters.end(); ++i)
{
// ...add all its points to a new cloud... すべてのポイントを新しいクラウドに追加...
pcl::PointCloud<pcl::PointXYZ>::Ptr cluster(new pcl::PointCloud<pcl::PointXYZ>);
for (std::vector<int>::const_iterator point = i->indices.begin(); point != i->indices.end(); point++)
cluster->points.push_back(cloud->points[*point]);
cluster->width = cluster->points.size();
cluster->height = 1;
cluster->is_dense = true;
// ...and save it to disk. ハードディスクに格納
if (cluster->points.size() <= 0)
break;
std::cout << "Cluster " << currentClusterNum << " has " << cluster->points.size() << " points." << std::endl;
std::string fileName = "cluster" + boost::to_string(currentClusterNum) + ".pcd";
pcl::io::savePCDFileASCII(fileName, *cluster);
currentClusterNum++;
}
}
この方法は前のものに基づいていますが、法線と点の曲率を比較する代わりに、RGBカラーを比較する。 いつものように、差が閾値よりも小さい場合、両方が同じクラスタに入れられる。 明らかにこれが機能するためには、RGB情報(タイプ "PointXYZRGB")を持つクラウドが必要である。 OpenNI視覚化プログラムを使用して、KinectやXtionでこれを取得することができる。
カラーチェックとは別に、このアルゴリズムでは2つの後処理ステップも使用されている。 最初の処理では、類似の平均色を有する隣接するクラスタがマージされる(これは閾値でも制御される)。 第2の処理では、最小サイズよりも小さいクラスタは、その隣接するクラスタと併合される。
#include <pcl/io/pcd_io.h>
#include <pcl/search/kdtree.h>
#include <pcl/segmentation/region_growing_rgb.h>
#include <iostream>
int
main(int argc, char** argv)
{
// Object for storing the point cloud, with color information. 色情報付きでポイントクラウドに格納するためのオブジェクト
pcl::PointCloud<pcl::PointXYZRGB>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZRGB>);
// Read a PCD file from disk. ハードディスクからPCDファイルを読み込む
if (pcl::io::loadPCDFile<pcl::PointXYZRGB>(argv[1], *cloud) != 0)
{
return -1;
}
// kd-tree object for searches. 探索のためのkd木オブジェクト
pcl::search::KdTree<pcl::PointXYZRGB>::Ptr kdtree(new pcl::search::KdTree<pcl::PointXYZRGB>);
kdtree->setInputCloud(cloud);
// Color-based region growing clustering object. 色ベースの領域成長クラスタリングのオブジェクト
pcl::RegionGrowingRGB<pcl::PointXYZRGB> clustering;
clustering.setInputCloud(cloud);
clustering.setSearchMethod(kdtree);
// Here, the minimum cluster size affects also the postprocessing step:
// clusters smaller than this will be merged with their neighbors.
// ここで最小クラスタのサイズは後処理に影響する:これより小さなクラスタは近傍のクラスタにマージ
clustering.setMinClusterSize(100);
// Set the distance threshold, to know which points will be considered neighbors.
// 距離の閾値の設定。近傍としてみなせるポイントを決めるためのもの
clustering.setDistanceThreshold(10);
// Color threshold for comparing the RGB color of two points. 2点のRGB色を比較するための色閾値
clustering.setPointColorThreshold(6);
// Region color threshold for the postprocessing step: clusters with colors
// within the threshold will be merged in one.
// 後処理のための領域色閾値: この閾値以下の色を持つクラスタは一つにマージ
clustering.setRegionColorThreshold(5);
std::vector <pcl::PointIndices> clusters;
clustering.extract(clusters);
// For every cluster... すべてのクラスタに対し...
int currentClusterNum = 1;
for (std::vector<pcl::PointIndices>::const_iterator i = clusters.begin(); i != clusters.end(); ++i)
{
// ...add all its points to a new cloud... そのポイントをすべて新たなクラウドに追加...
pcl::PointCloud<pcl::PointXYZRGB>::Ptr cluster(new pcl::PointCloud<pcl::PointXYZRGB>);
for (std::vector<int>::const_iterator point = i->indices.begin(); point != i->indices.end(); point++)
cluster->points.push_back(cloud->points[*point]);
cluster->width = cluster->points.size();
cluster->height = 1;
cluster->is_dense = true;
// ...and save it to disk. ハードディスクに格納
if (cluster->points.size() <= 0)
break;
std::cout << "Cluster " << currentClusterNum << " has " << cluster->points.size() << " points." << std::endl;
std::string fileName = "cluster" + boost::to_string(currentClusterNum) + ".pcd";
pcl::io::savePCDFileASCII(fileName, *cluster);
currentClusterNum++;
}
}
Min-Cut(最小カット)アルゴリズムは、これまでのものとは異なり、2分割セグメンテーションを実行する。クラウドは、関心のあるオブジェクトに属していないポイント(背景ポイント)と、オブジェクトの一部と見なされるポイント(前景ポイント)の2つのクラスターに分割される。
これを実行するために、アルゴリズムは頂点グラフを使用する。頂点(グラフのノード)はポイントを表す。さらに、ソースとシンクと呼ばれる2つの追加の頂点があり、異なるペナルティ(重み)のエッジで他の頂点に接続される。次に、隣接点間のエッジも確立される。各エッジは、それらを区切る距離に応じた重み値を持つ。距離が大きいほど、エッジがカットされる可能性が高くなる。またこのアルゴリズムは入力として、オブジェクトの中心点と半径(サイズ)を必要とする。すべてが設定されると、最小カットが見つけられ、前景ポイントのリストが表示される。
#include <pcl/io/pcd_io.h>
#include <pcl/segmentation/min_cut_segmentation.h>
#include <iostream>
int
main(int argc, char** argv)
{
// Object for storing the point cloud. ポイントクラウドの格納用オブジェクト
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);
// Read a PCD file from disk. ハードディスクからPCDファイルを読み込む
if (pcl::io::loadPCDFile<pcl::PointXYZ>(argv[1], *cloud) != 0)
{
return -1;
}
// Min-cut clustering object. 最小カットクラスタリングのオブジェクト
pcl::MinCutSegmentation<pcl::PointXYZ> clustering;
clustering.setInputCloud(cloud);
// Create a cloud that lists all the points that we know belong to the object
// (foreground points). We should set here the object's center.
// そのオブジェクトに属すことがわかっているポイント(前景ポイント)をすべてリストしたクラウドを生成。
// そのオブジェクトの中心をここで設定すべき
pcl::PointCloud<pcl::PointXYZ>::Ptr foregroundPoints(new pcl::PointCloud<pcl::PointXYZ>());
pcl::PointXYZ point;
point.x = 100.0;
point.y = 100.0;
point.z = 100.0;
foregroundPoints->points.push_back(point);
clustering.setForegroundPoints(foregroundPoints);
// Set sigma, which affects the smooth cost calculation. It should be
// set depending on the spacing between points in the cloud (resolution).
// 滑らかさコスト計算に影響するsigma値を設定。クラウド内のポイントの配置(解像度)によって決めるべき
clustering.setSigma(0.05);
// Set the radius of the object we are looking for. 探索するオブジェクトの半径の設定
clustering.setRadius(0.20);
// Set the number of neighbors to look for. Increasing this also increases
// the number of edges the graph will have.
// 探索する近傍点の個数の設定。これを増やすとグラフが持つエッジの個数も増える
clustering.setNumberOfNeighbours(20);
// Set the foreground penalty. It is the weight of the edges
// that connect clouds points with the source vertex.
// 前景のペナルティの設定。ペナルティとはソースの点とクラウドの点を接続するエッジの重みのこと
clustering.setSourceWeight(0.6);
std::vector <pcl::PointIndices> clusters;
clustering.extract(clusters);
std::cout << "Maximum flow is " << clustering.getMaxFlow() << "." << std::endl;
// For every cluster... すべてのクラスタに対し...
int currentClusterNum = 1;
for (std::vector<pcl::PointIndices>::const_iterator i = clusters.begin(); i != clusters.end(); ++i)
{
// ...add all its points to a new cloud... そのポイントをすべて新たなクラウドに追加する..
pcl::PointCloud<pcl::PointXYZ>::Ptr cluster(new pcl::PointCloud<pcl::PointXYZ>);
for (std::vector<int>::const_iterator point = i->indices.begin(); point != i->indices.end(); point++)
cluster->points.push_back(cloud->points[*point]);
cluster->width = cluster->points.size();
cluster->height = 1;
cluster->is_dense = true;
// ...and save it to disk. そしてハードディスクに格納する
if (cluster->points.size() <= 0)
break;
std::cout << "Cluster " << currentClusterNum << " has " << cluster->points.size() << " points." << std::endl;
std::string fileName = "cluster" + boost::to_string(currentClusterNum) + ".pcd";
pcl::io::savePCDFileASCII(fileName, *cluster);
currentClusterNum++;
}
}
最小カットアルゴリズムについての詳細は元論文(PDF)を参照のこと。
前節で見たように、PCLは、一連の点をモデルに合わせるRANSACアルゴリズムの実装を提供している。 クラウドを検索し、選択したモデル(平面、球...)をサポートする点のリストを見つけ、モデルの係数を計算する。 そのようなオブジェクトが存在すれば、容易にセグメンテーションが実行できる。
ここで、既知の係数を持つモデルの点を抽出したければ、次のチュートリアルを参照のこと:
以下のコードでは、すべての面的な表面をポイントクラウドからセグメント化できる。 これは非常に重要である、というのは、シーン内の床、天井、壁、テーブルなどが検出できるからである。 探しているオブジェクトがテーブルの上にあることがわかっている場合は、テーブルの支持関係にないポイントをすべて破棄でき、その結果として検索性能を向上させられる。 このために設計されたpcl::ExtractPolygonalPrismDataクラスを見てみよう。 平面の係数を与えて凸包を計算したら、与えられた高さだけ閉包領域(hull)を押し出して3Dプリズムを作成し、その中にあるすべての点を抽出することができる。
#include <pcl/io/pcd_io.h>
#include <pcl/sample_consensus/method_types.h>
#include <pcl/sample_consensus/model_types.h>
#include <pcl/segmentation/sac_segmentation.h>
#include <iostream>
int
main(int argc, char** argv)
{
// Objects for storing the point clouds. ポイントクラウドの格納用オブジェクト
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);
pcl::PointCloud<pcl::PointXYZ>::Ptr inlierPoints(new pcl::PointCloud<pcl::PointXYZ>);
// Read a PCD file from disk. ハードディスクからPCDファイルを読み込む
if (pcl::io::loadPCDFile<pcl::PointXYZ>(argv[1], *cloud) != 0)
{
return -1;
}
// Object for storing the plane model coefficients. 平面モデル係数を格納するためのオブジェクト
pcl::ModelCoefficients::Ptr coefficients(new pcl::ModelCoefficients);
// Create the segmentation object. 分割オブジェクトの生成
pcl::SACSegmentation<pcl::PointXYZ> segmentation;
segmentation.setInputCloud(cloud);
// Configure the object to look for a plane. 平面を探索するオブジェクトを構成
segmentation.setModelType(pcl::SACMODEL_PLANE);
// Use RANSAC method. RANSAC手法を使用
segmentation.setMethodType(pcl::SAC_RANSAC);
// Set the maximum allowed distance to the model. そのモデルに対する最大許容距離を設定
segmentation.setDistanceThreshold(0.01);
// Enable model coefficient refinement (optional). (選択可能)モデル係数の精緻化を可能にする
segmentation.setOptimizeCoefficients(true);
pcl::PointIndices inlierIndices;
segmentation.segment(inlierIndices, *coefficients);
if (inlierIndices.indices.size() == 0)
std::cout << "Could not find any points that fitted the plane model." << std::endl;
else
{
std::cerr << "Model coefficients: " << coefficients->values[0] << " "
<< coefficients->values[1] << " "
<< coefficients->values[2] << " "
<< coefficients->values[3] << std::endl;
// Copy all inliers of the model to another cloud.
pcl::copyPointCloud<pcl::PointXYZ>(*cloud, inlierIndices, *inlierPoints);
}
}
クラウドから円筒形をセグメンテーションする、もしも似ている形状があるのなら。半径を指定するための追加のパラメータがある:
#include <pcl/io/pcd_io.h>
#include <pcl/sample_consensus/method_types.h>
#include <pcl/sample_consensus/model_types.h>
#include <pcl/segmentation/sac_segmentation.h>
#include <iostream>
int
main(int argc, char** argv)
{
// Objects for storing the point clouds. ポイントクラウドの格納用オブジェクト
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);
pcl::PointCloud<pcl::PointXYZ>::Ptr inlierPoints(new pcl::PointCloud<pcl::PointXYZ>);
// Read a PCD file from disk. ハードディスクからPCDファイルを読み込む
if (pcl::io::loadPCDFile<pcl::PointXYZ>(argv[1], *cloud) != 0)
{
return -1;
}
// Object for storing the plane model coefficients. 平面モデル係数の格納のためのオブジェクト
pcl::ModelCoefficients::Ptr coefficients(new pcl::ModelCoefficients);
// Create the segmentation object. 分割オブジェクトの生成
pcl::SACSegmentation<pcl::PointXYZ> segmentation;
segmentation.setInputCloud(cloud);
// Configure the object to look for a plane. 平面を探索するためのオブジェクトの構成
segmentation.setModelType(pcl::SACMODEL_CYLINDER);
// Use RANSAC method. RANSAC手法の使用
segmentation.setMethodType(pcl::SAC_RANSAC);
// Set the maximum allowed distance to the model. そのモデルに対する最大許容距離を設定
segmentation.setDistanceThreshold(0.01);
// Enable model coefficient refinement (optional). (選択可能)モデル係数の精緻化を可能にする
segmentation.setOptimizeCoefficients(true);
// Set minimum and maximum radii of the cylinder. 円筒の最小半径と最大半径を設定
segmentation.setRadiusLimits(0, 0.1);
pcl::PointIndices inlierIndices;
segmentation.segment(inlierIndices, *coefficients);
if (inlierIndices.indices.size() == 0)
std::cout << "Could not find any points that fitted the cylinder model." << std::endl;
// Copy all inliers of the model to another cloud. そのモデルのすべての適合値を別なクラウドにコピー
else pcl::copyPointCloud<pcl::PointXYZ>(*cloud, inlierIndices, *inlierPoints);
}
与えられたポイントのセットに対し、PCLはQHullライブラリを使用して形状の閉包領域(hull)を計算できる。 閉包領域は、体積を示すシェル(殻)のように、点集合の最も外側の境界に一致する点として定義できる。 計算可能な閉包領域には、凸型(convex)と凹型(concave)の2種類がある。 凸包は「穴」を持つことはできない。つまり、任意の点のペアを結ぶ線分は決して「空」の空間を横切ることはない。 一方、凹状の閉包領域は、通常、次の絵で見ることができるように、より少ない面積で済む:
(左)凹型(concave)閉包領域 (右) 凸型(convex)閉包領域
次のコードは、シーンで見つかった最初の面の凹面を計算するためにPCLを使用する方法を示している。
#include <pcl/io/pcd_io.h>
#include <pcl/sample_consensus/method_types.h>
#include <pcl/sample_consensus/model_types.h>
#include <pcl/segmentation/sac_segmentation.h>
#include <pcl/filters/extract_indices.h>
#include <pcl/surface/concave_hull.h>
#include <pcl/visualization/cloud_viewer.h>
#include <iostream>
int
main(int argc, char** argv)
{
// Objects for storing the point clouds. ポイントクラウドの格納用オブジェクト
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);
pcl::PointCloud<pcl::PointXYZ>::Ptr plane(new pcl::PointCloud<pcl::PointXYZ>);
pcl::PointCloud<pcl::PointXYZ>::Ptr concaveHull(new pcl::PointCloud<pcl::PointXYZ>);
// Read a PCD file from disk. ハードディスクからPCDファイルを読み込む
if (pcl::io::loadPCDFile<pcl::PointXYZ>(argv[1], *cloud) != 0)
{
return -1;
}
// Get the plane model, if present. もしあれば、平面モデルを取得
pcl::ModelCoefficients::Ptr coefficients(new pcl::ModelCoefficients);
pcl::SACSegmentation<pcl::PointXYZ> segmentation;
segmentation.setInputCloud(cloud);
segmentation.setModelType(pcl::SACMODEL_PLANE);
segmentation.setMethodType(pcl::SAC_RANSAC);
segmentation.setDistanceThreshold(0.01);
segmentation.setOptimizeCoefficients(true);
pcl::PointIndices::Ptr inlierIndices(new pcl::PointIndices);
segmentation.segment(*inlierIndices, *coefficients);
if (inlierIndices->indices.size() == 0)
std::cout << "Could not find a plane in the scene." << std::endl;
else
{
// Copy the points of the plane to a new cloud. その平面のポイントを新たなクラウドにコピー
pcl::ExtractIndices<pcl::PointXYZ> extract;
extract.setInputCloud(cloud);
extract.setIndices(inlierIndices);
extract.filter(*plane);
// Object for retrieving the concave hull. 凹面領域を探索するためのオブジェクト
pcl::ConcaveHull<pcl::PointXYZ> hull;
hull.setInputCloud(plane);
// Set alpha, which is the maximum length from a vertex to the center of the voronoi cell
// (the smaller, the greater the resolution of the hull).
// アルファ値の設定、これは点からボロノイ領域への最大距離のこと(小さければ、領域の解像度が大きくなる)
hull.setAlpha(0.1);
hull.reconstruct(*concaveHull);
// Visualize the hull. 凹面領域の視覚化
pcl::visualization::CloudViewer viewerPlane("Concave hull");
viewerPlane.showCloud(concaveHull);
while (!viewerPlane.wasStopped())
{
// Do nothing but wait. ただ待つだけ
}
}
}
小さなアルファ値を選択することで、閉包領域の詳細を改善できる。
(左) PCLデータセットからとったシーン (右下)クラウドの中のテーブルの凹型閉包領域 (フルスクリーンの方が見やすい)
pcl::CropHullクラスは、閉包領域の内側や外側にある点を見つけることを可能にする。 その使い方の一つは、テーブル上に物体がある場面では、次のようになるだろう:
凸包の計算は上と同様に行われ、それに応じて名前が変更される。 また、ここでアルファ・パラメータを設定する必要はない:
#include <pcl/io/pcd_io.h>
#include <pcl/sample_consensus/method_types.h>
#include <pcl/sample_consensus/model_types.h>
#include <pcl/segmentation/sac_segmentation.h>
#include <pcl/filters/extract_indices.h>
#include <pcl/surface/convex_hull.h>
#include <pcl/visualization/cloud_viewer.h>
#include <iostream>
int
main(int argc, char** argv)
{
// Objects for storing the point clouds. ポイントクラウドの格納用オブジェクト
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);
pcl::PointCloud<pcl::PointXYZ>::Ptr plane(new pcl::PointCloud<pcl::PointXYZ>);
pcl::PointCloud<pcl::PointXYZ>::Ptr convexHull(new pcl::PointCloud<pcl::PointXYZ>);
// Read a PCD file from disk. ハードディスクからPCDファイルを読み込む
if (pcl::io::loadPCDFile<pcl::PointXYZ>(argv[1], *cloud) != 0)
{
return -1;
}
// Get the plane model, if present. もしあれば、平面モデルを取得
pcl::ModelCoefficients::Ptr coefficients(new pcl::ModelCoefficients);
pcl::SACSegmentation<pcl::PointXYZ> segmentation;
segmentation.setInputCloud(cloud);
segmentation.setModelType(pcl::SACMODEL_PLANE);
segmentation.setMethodType(pcl::SAC_RANSAC);
segmentation.setDistanceThreshold(0.01);
segmentation.setOptimizeCoefficients(true);
pcl::PointIndices::Ptr inlierIndices(new pcl::PointIndices);
segmentation.segment(*inlierIndices, *coefficients);
if (inlierIndices->indices.size() == 0)
std::cout << "Could not find a plane in the scene." << std::endl;
else
{
// Copy the points of the plane to a new cloud. その平面のポイントを新たなクラウドにコピー
pcl::ExtractIndices<pcl::PointXYZ> extract;
extract.setInputCloud(cloud);
extract.setIndices(inlierIndices);
extract.filter(*plane);
// Object for retrieving the convex hull. 凸面領域探索のためのオブジェクト
pcl::ConvexHull<pcl::PointXYZ> hull;
hull.setInputCloud(plane);
hull.reconstruct(*convexHull);
// Visualize the hull. 領域の視覚化
pcl::visualization::CloudViewer viewerPlane("Convex hull");
viewerPlane.showCloud(convexHull);
while (!viewerPlane.wasStopped())
{
// Do nothing but wait. ただ待つだけ
}
}
}
PCLを使用してポイントクラウドから滑らかなパラメトリック表面表現を作成する方法については、次を参照のこと:
三角形分割とは、ポイントを相互に接続し、連続したポリゴンメッシュ(三角形ポリゴン、つまり三角形)で終わるポイントクラウドによって取得された表面を推定する方法である。 この表面メッシュを取得した後、たとえば、Blenderや3ds Maxで開くことができるVTK(Visualization Toolkit)のような、ほとんどの3Dモデリングツールで理解できる形式にエクスポートできる。 PCLはVTKからPLYまたはOBJに変換することもできる。
次のコードでは、局所的2D投影で動作する欲張り三角形分割アルゴリズムのPCL実装を使用している。 どの点に対しても、法線に沿って「下を」見て、隣接する点を接続していく。 詳細について、特にパラメータに関して、APIまたはオリジナルのPCLチュートリアルを参照のこと:
#include <pcl/io/pcd_io.h>
#include <pcl/features/normal_3d.h>
#include <pcl/surface/gp3.h>
#include <pcl/io/vtk_io.h>
int
main(int argc, char** argv)
{
// Object for storing the point cloud. ポイントクラウドの格納用オブジェクト
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);
// Object for storing the normals. 法線の格納用オブジェクト
pcl::PointCloud<pcl::Normal>::Ptr normals(new pcl::PointCloud<pcl::Normal>);
// Object for storing both the points and the normals. ポイントと法線の両方の格納用オブジェクト
pcl::PointCloud<pcl::PointNormal>::Ptr cloudNormals(new pcl::PointCloud<pcl::PointNormal>);
// Read a PCD file from disk. ハードディスクからPCDファイルを読み込む
if (pcl::io::loadPCDFile<pcl::PointXYZ>(argv[1], *cloud) != 0)
{
return -1;
}
// Normal estimation. 法線の推定
pcl::NormalEstimation<pcl::PointXYZ, pcl::Normal> normalEstimation;
normalEstimation.setInputCloud(cloud);
normalEstimation.setRadiusSearch(0.03);
pcl::search::KdTree<pcl::PointXYZ>::Ptr kdtree(new pcl::search::KdTree<pcl::PointXYZ>);
normalEstimation.setSearchMethod(kdtree);
normalEstimation.compute(*normals);
// The triangulation object requires the points and normals to be stored in the same structure.
// 三角形分割オブジェクトは、同じ構造にポイントと法線とを格納することを要求
pcl::concatenateFields(*cloud, *normals, *cloudNormals);
// Tree object for searches in this new object. この新たなオブジェクトで探索するためのkd木オブジェクト
pcl::search::KdTree<pcl::PointNormal>::Ptr kdtree2(new pcl::search::KdTree<pcl::PointNormal>);
kdtree2->setInputCloud(cloudNormals);
// Triangulation object. 三角形分割オブジェクト
pcl::GreedyProjectionTriangulation<pcl::PointNormal> triangulation;
// Output object, containing the mesh. 出力オブジェクト、メッシュを含む
pcl::PolygonMesh triangles;
// Maximum distance between connected points (maximum edge length). 結合点の最大距離(エッジの最大長さ)
triangulation.setSearchRadius(0.025);
// Maximum acceptable distance for a point to be considered,
// relative to the distance of the nearest point.
// 対象とするポイントの最大許容距離、最近接ポイントの距離との相対値
triangulation.setMu(2.5);
// How many neighbors are searched for. 探索する近接点の個数
triangulation.setMaximumNearestNeighbors(100);
// Points will not be connected to the current point
// if their normals deviate more than the specified angle.
// ポイントは、法線の差が指定された角度より大きければ、現在のポイントに対し結合されない
triangulation.setMaximumSurfaceAngle(M_PI / 4); // 45 degrees.
// If false, the direction of normals will not be taken into account
// when computing the angle between them.
// もしもfalseなら、法線の方向は角度計算のときに考慮されない
triangulation.setNormalConsistency(false);
// Minimum and maximum angle there can be in a triangle.
// The first is not guaranteed, the second is.
// 三角形があったときの、その最大角度と最小角度。最小角度は保証されず、最大角度は保証される
triangulation.setMinimumAngle(M_PI / 18); // 10 degrees. 10度をラディアンで
triangulation.setMaximumAngle(2 * M_PI / 3); // 120 degrees. 120度をラディアンで
// Triangulate the cloud. クラウドを三角形分割
triangulation.setInputCloud(cloudNormals);
triangulation.setSearchMethod(kdtree2);
triangulation.reconstruct(triangles);
// Save to disk. ハードディスクに格納、vtkファイルとして
pcl::io::saveVTKFile("mesh.vtk", triangles);
}
ポイントクラウドの三角形分割によって生成されたVTKファイル
Go to root: PhD-3D-Object-Tracking
Links to articles:
PCL/OpenNI tutorial 0: The very basics
PCL/OpenNI tutorial 1: Installing and testing
PCL/OpenNI tutorial 2: Cloud processing (basic)
PCL/OpenNI tutorial 3: Cloud processing (advanced)
PCL/OpenNI tutorial 4: 3D object recognition (descriptors)
PCL/OpenNI tutorial 5: 3D object recognition (pipeline)
PCL/OpenNI troubleshooting