Open3DのPointCloudクラスで遊んでみる(Windows, C++)
前章でOpen3Dの描画機能を使って点群を描画してみました:
少し前知識は要する物の、わかってしまえばシンプルに描画出来ました。これで見える化を確立したので、読み込んだり作成したりした3Dデータを確認できます。
そこで今回は点群データであるPointCloudクラスの機能を色々試して遊んでみましょう。尚、PointCloudクラスが提供するメソッド群は以下のリファレンスにあります:
CodeGenで出力されているので…まぁ何といいましょうか、必要最低限な情報しか無い為、試して予想しないとわからんのですよね、実際は(^-^;
色を変更(PaintUniformColor)
全点群の色を引数の色で塗りつぶします。色情報が飛ぶので注意です:
#include <Open3D.h>
int main() {
auto cloudObj = std::make_shared< open3d::geometry::PointCloud >();
bool res = open3d::io::ReadPointCloudFromPLY(
open3d::data::PLYPointCloud().GetPath(),
*cloudObj,
open3d::io::ReadPointCloudOption() );
// 青っぽい色で塗りつぶし
cloudObj->PaintUniformColor( { 0.3, 0.3, 0.7 } );
if ( res )
open3d::visualization::DrawGeometries( { cloudObj } );
return 0;
}
![](https://assets.st-note.com/img/1692229663191-aTeWQOPttK.png?width=1200)
色はOpen3Dが採用しているVector3dクラスでRGB(0.0~1.0)で指定します。
ランダムで点を抽出(RandomDownSample)
元の点群データからランダムに点を抽出したPointCloudを作成します:
#include <Open3D.h>
int main() {
auto cloudObj = std::make_shared< open3d::geometry::PointCloud >();
bool res = open3d::io::ReadPointCloudFromPLY(
open3d::data::PLYPointCloud().GetPath(),
*cloudObj,
open3d::io::ReadPointCloudOption() );
// 10%に減らした点群で抽出
auto subObj = cloudObj->RandomDownSample( 0.1 );
if ( res )
open3d::visualization::DrawGeometries( { subObj } );
return 0;
}
引数にはサンプリング密度を指定します。0.9なら90%の点群数に、10%なら点の数が1/10に大きく減ります。
![](https://assets.st-note.com/img/1692230079979-tLaSrwcbsx.png?width=1200)
![](https://assets.st-note.com/img/1692230012100-6o5nn5sibx.png?width=1200)
上が元の密度で下が10%に減らした点群です。かっすかすになりました(^-^;。もちろんこれはデータ量を減らすという目的で使えます。また元のPointCloudに対して非破壊なので安心してメソッドを呼べます。
指定半径以内の飛び点を削除(RemoveRadiusOutliers)
点群の中でエラーと思われる飛び点(他の点群から独立しているノイズ点)を削除します:
#include <Open3D.h>
int main() {
auto cloudObj = std::make_shared< open3d::geometry::PointCloud >();
bool res = open3d::io::ReadPointCloudFromPLY(
open3d::data::PLYPointCloud().GetPath(),
*cloudObj,
open3d::io::ReadPointCloudOption() );
// 半径0.02の球の中に25点未満の密度部分を削除
auto subObj = cloudObj->RemoveRadiusOutliers( 25, 0.02, false );
if ( res )
open3d::visualization::DrawGeometries( { std::get<0>(subObj) } );
return 0;
}
第2引数に指定した球の半径に対し第1引数の個数未満の独立した点があった場合、その点が取り除かれます。メソッドはオブジェクトをタプル形式で返します。第1変数が間引き後のPointCloudオブジェクトで、第2引数が選択されたインデックス配列です。
![](https://assets.st-note.com/img/1692232261983-y1fcl35OdN.png?width=1200)
![](https://assets.st-note.com/img/1692232074934-JYjm7BC2ch.png?width=1200)
下は半径0.02で点数を25に指定した場合です。色々な個所の点が取り除かれています。これは敢えてはっきり分かるように調整したもので、本来は少し大きめの半径と数点程度の点数を指定するのかなと想像します。色々触りましたがちょっと調整が難しいメソッドに感じました。
指定インデックスの点群を抽出(SelectByIndex)
元の点群から指定されたインデックスリストの点群を抽出して別のPointCloudオブジェクトとして出力します。元のデータは破壊しません:
#include <Open3D.h>
int main() {
auto cloudObj = std::make_shared< open3d::geometry::PointCloud >();
bool res = open3d::io::ReadPointCloudFromPLY(
open3d::data::PLYPointCloud().GetPath(),
*cloudObj,
open3d::io::ReadPointCloudOption() );
// 元の点群の先頭20%くらいの点群のみ抽出
const size_t num = cloudObj->points_.size() / 5;
std::vector<size_t> subPoints( num );
for ( size_t i = 0; i < num; ++i )
subPoints[ i ] = i;
auto subObj = cloudObj->SelectByIndex( subPoints );
if ( res )
open3d::visualization::DrawGeometries( { subObj } );
return 0;
}
上コードでは元の点の上位20%くらいまでの点群のインデックスリストを作り、SelectByIndexメソッドに渡しています。これにより下図のような点群の一部のみが抽出されます:
![](https://assets.st-note.com/img/1692233774154-TQ9513kbA3.png?width=1200)
このメソッドは第2引数に反転フラグを持っていて、trueにすると第1引数で与えたインデックス以外の点が抽出されるようになります。確かにそういうの必要ですよね。なるほどです。
平面を抽出する(SegmentPlane)
与えられた点群の中に存在する平面的な所を抽出し、その平面方程式の係数と対象となった点のインデックスリストを返します:
#include <Open3D.h>
int main() {
auto cloudObj = std::make_shared< open3d::geometry::PointCloud >();
bool res = open3d::io::ReadPointCloudFromPLY(
open3d::data::PLYPointCloud().GetPath(),
*cloudObj,
open3d::io::ReadPointCloudOption() );
// 平面を抽出する
auto plane = cloudObj->SegmentPlane( 0.005, 3, 100, 0.99999 );
auto subObj = cloudObj->SelectByIndex( std::get<1>( plane ) );
if ( res )
open3d::visualization::DrawGeometries( { subObj } );
return 0;
}
第1引数には推定される平面に含まれる点の許容距離範囲を指定します。小さい程推定平面に近い点のみがピックアップされますが、小さ過ぎるとフィット点が減る為推定自体が暴れてしまいます。
第2引数にはRANSACアルゴリズムの繰り返し数を指定します。RANSACというのはRandom Sample Consensusの略です。統計手法の一つでして、ん~~そうですね、イメージするなら「点をランダムにサンプリングしてフィッティングする事で外れ値の影響を排除する方法」という感じです。3Dスキャナなどで取ったデータには下図のように様々な平面が入っており、全部のデータを使うとどれが平面だか分からなくなります。そこで全体から何点か取ってそれにフィットする平面を求める、という作業を繰り返すとデータ内により広くある平面が検出されやすくなります。これによりロバスト(頑健)な平面検出が出来る事になります。第2引数にはその平面抽出の繰り返し数を指定します。多い方がより頑健ですがその分時間がかかります。
第3引数は1回のRANSACで取るサンプル点数を指定します。ある程度の点数があった方が良いと思いますが、多すぎると収束しなくなるかもしれません。
第4引数は推定平面を確定する確率です。おそらくデフォルト値で構わないです。
![](https://assets.st-note.com/img/1692234428601-f2CNnorkRL.png?width=1200)
![](https://assets.st-note.com/img/1692234515136-CSEYrGgNHw.png?width=1200)
![](https://assets.st-note.com/img/1692234528660-ENR307vbCn.png?width=1200)
上記のコードでは推定平面から0.005の距離以内にある点を抽出しています。元の部屋に対してテストすると床面の点群を抽出してくれました。真横から見ると確かに平面的に点が並んでいます。
戻り値はタプルで第1変数は平面方程式の係数です(Vector4d)。Vector4dの並びは以下を意味します:
![](https://assets.st-note.com/img/1692235764132-Jhkqtgz67m.png)
つまり(a,b,c)がその平面の法線ベクトルになっています。計算してみると正規化もされていますね。よってdは原点から平面までの距離という事になります。実際上の部屋の床の場合は、
![](https://assets.st-note.com/img/1692235914816-V4Pzjg21Du.png)
こういう平面方程式になっているようです。床面はY軸が鉛直軸(法線)であるのもわかりますね。
点群をボクセルにダウンサンプリングする(VoxelDownSample)
元の点群から指定サイズのボクセル(立方体)単位で点群を再サンプリングします:
#include <Open3D.h>
int main() {
auto cloudObj = std::make_shared< open3d::geometry::PointCloud >();
bool res = open3d::io::ReadPointCloudFromPLY(
open3d::data::PLYPointCloud().GetPath(),
*cloudObj,
open3d::io::ReadPointCloudOption() );
// ボクセル単位にリサンプリングする
auto subObj = cloudObj->VoxelDownSample( 0.025 );
if ( res )
open3d::visualization::DrawGeometries( { subObj } );
return 0;
}
引数にボクセル(立方体)の大きさを指定します。結果として一つのボクセル内に含まれる複数の点群が平均化されてボクセルの中心点に再サンプリングされます。下図はボクセルサイズを0.025にして出力したものです。ボクセル間の距離が0.025になり、点群の並びがより離散的になっています:
![](https://assets.st-note.com/img/1692238619602-XQx2j0lNxV.png?width=1200)
他にも気軽なのから難しいものまで色々メソッドがあります。Open3Dを使っていく中で良さそうなメソッドがあれば都度こちらに追加紹介していこうかなと思います。
点群データを扱うと次にやりたくなるのが「点群にメッシュを張る」です。下調べ段階ですがOpen3Dで出来そうな雰囲気がありますので(現時点で未確認)、可能であれば記事化します。
ではまた(^-^)/
この記事が気に入ったらサポートをしてみませんか?