このドキュメントは、PCLの3D機能評価手法の概要を紹介し、pcl :: Featureクラスの内部に関心のあるユーザーや開発者のためのガイドとして機能する

基本理論

3Dマッピングシステムの概念で定義される ポイント(点) は、ネイティブ表現では、与えられた起点に対するデカルト座標x、y、zを用いて表される。座標系の原点が時間的に変化しないと仮定すると、時刻$t_1$と$t_2$で得られた2つの点$p_1$と$p_2$があり、それらは同じ座標を有する可能性がある。しかし、ある距離尺度(例えば、ユークリッド距離)に関してそのような点が互いに等しくても、全く異なる表面上でサンプリングすることができ、周辺の点集合とあわせて考えると全く違う情報を持ちうるため、これらの点を比較することは質の悪い問題である。 なぜかといえば、世界が$t_1$と$t_2$の間で変化していないという保証はないからである。強度や表面輝度値や色など、サンプリング点についての追加情報を提供できるデバイス(センサー)はあるが、この問題を完全に解決するものではなく、比較することには曖昧さが残る。

いろいろな理由で点集合を比較する必要があるアプリケーションでは、幾何学的な表面を区別できるよう、より優れた特性とメトリックを必要とする。したがって、デカルト座標を持つ単一の実体としての3D点という概念は消えさり、新しい概念である 局所記述子 という概念が取り入れられた。文献には、 形状記述子幾何学特徴 など、同じ概念化を記述する異なる命名体系が豊富に存在するが、この文書では、 点特徴表現(point feature representation) と呼ぶ。

周囲の近傍点を含めることによって、特徴定式化において基礎となるサンプリングされた表面の幾何形状を推論し、キャプチャすることができる。これはあいまい性のある比較問題の解決に寄与する。理想的には、結果として得られる特徴量は、同一もしくは類似の表面に存在する点に対し(なんらかの尺度において)極めて似た値となる(下図を参照)。良い 点特徴表現とは、以下の条件の下で、同一の局所表面特性を捉えることができ、悪い 点特徴表現と区別される。:

  • 剛体変換 : すなわち、データ内の3D回転や3D移動は、結果の特徴ベクトルF推定に影響を与えてはならない。
  • サンプリング密度の変動 : 原理的には、局所表面パッチは、そのサンプリング密度の多少の違いによらず、同じ特徴ベクトル・シグネチャを有するべきである。
  • ノイズ : 点特徴量表現は、データ内に微小のノイズの存在下であっても、その特徴ベクトルは同一、もしくは非常に類似した値を保持しなければならない。

http://pointclouds.org/documentation/tutorials/_images/good_features.jpg

一般に、PCL特徴量は、高速kd-tree検索を使用し、近似手法により検索対象点の最近傍点の計算を行う。検索には以下の2種類がある:

  • 検索対象点の k(ユーザ指定のパラメータ)近傍点を求める(k-最近某探索とも呼ばれる)。
  • 検索対象点から半径 r の球内の すべての近傍 を求める(半径探索とも呼ばれる)。

法線推定の例

検索対象点が決定されれば、その近傍点集合を用いて、検索対象点の周りのサンプリングされた表面の幾何形状を捉えるような局所特徴表現を推定できる。 表面の幾何形状を記述する際の重要な問題は、座標系の方向を最初に推測すること、すなわち、その法線を推定することである。 表面法線は曲面の重要な特徴であり、シェーディングやその他の視覚効果を生成する正しい光源を適用するためにCGアプリケーションなどの多くの領域で頻繁に使用される(詳細は Rusu (2009) PhD Thesis http://mediatum.ub.tum.de/download/800632/800632.pdf  を参照)。

次のコードは、入力データセット内のすべての点の表面法線の集合を推定し、draw_geometries関数により表示する(nを押すと法線が表示される)。なおvoxel_down_sampleを実行しているのは、このままだと点の個数が多いため、法線表示したときに見難いからである。

In [5]:
from py3d import *

print("Testing IO for point cloud ...")
pcd = read_point_cloud("./TestData/fragment.pcd")

print("Downsample the point cloud with a voxel of 0.05")
downpcd = voxel_down_sample(pcd, voxel_size = 0.05)

print("Compute the normal of the downsampled point cloud")
estimate_normals(downpcd, search_param = KDTreeSearchParamHybrid(
            radius = 0.1, max_nn = 30))

draw_geometries([downpcd])
Testing IO for point cloud ...
Compute the normal of the downsampled point cloud
In [6]:
help(estimate_normals)
Help on built-in function estimate_normals in module py3d:

estimate_normals(...) method of builtins.PyCapsule instance
    estimate_normals(cloud: py3d.PointCloud, search_param: py3d.KDTreeSearchParam=KDTreeSearchParamKNN with knn = 30) -> bool
    
    Function to compute the normals of a point cloud

estimate_normalsはすべての点の法線を計算する。 この関数は共分散分析を用いて隣接点を見つけ、隣接点の主軸を計算する。

この関数は、KDTreeSearchParamHybridクラスのインスタンスを引数として取る。 radius = 0.1max_nn = 30という2つの重要な引数は、検索半径と最大最近傍を指定するもの。 これにより10cmの探索半径を持ち、計算時間を節約するため最大30個の近傍しか考慮しない。

In [7]:
help(downpcd)
Help on PointCloud in module py3d object:

class PointCloud(Geometry3D)
 |  Method resolution order:
 |      PointCloud
 |      Geometry3D
 |      Geometry
 |      pybind11_builtins.pybind11_object
 |      builtins.object
 |  
 |  Methods defined here:
 |  
 |  __add__(...)
 |      __add__(self: py3d.PointCloud, arg0: py3d.PointCloud) -> py3d.PointCloud
 |  
 |  __copy__(...)
 |      __copy__(self: py3d.PointCloud) -> py3d.PointCloud
 |  
 |  __deepcopy__(...)
 |      __deepcopy__(self: py3d.PointCloud, arg0: dict) -> py3d.PointCloud
 |  
 |  __iadd__(...)
 |      __iadd__(self: py3d.PointCloud, arg0: py3d.PointCloud) -> py3d.PointCloud
 |  
 |  __init__(...)
 |      __init__(*args, **kwargs)
 |      Overloaded function.
 |      
 |      1. __init__(self: py3d.PointCloud) -> None
 |      
 |      Default constructor
 |      
 |      2. __init__(self: py3d.PointCloud, arg0: py3d.PointCloud) -> None
 |      
 |      Copy constructor
 |  
 |  __repr__(...)
 |      __repr__(self: py3d.PointCloud) -> str
 |  
 |  has_colors(...)
 |      has_colors(self: py3d.PointCloud) -> bool
 |  
 |  has_normals(...)
 |      has_normals(self: py3d.PointCloud) -> bool
 |  
 |  has_points(...)
 |      has_points(self: py3d.PointCloud) -> bool
 |  
 |  normalize_normals(...)
 |      normalize_normals(self: py3d.PointCloud) -> None
 |  
 |  paint_uniform_color(...)
 |      paint_uniform_color(self: py3d.PointCloud, arg0: numpy.ndarray[float64[3, 1]]) -> None
 |  
 |  ----------------------------------------------------------------------
 |  Data descriptors defined here:
 |  
 |  colors
 |  
 |  normals
 |  
 |  points
 |  
 |  ----------------------------------------------------------------------
 |  Methods inherited from Geometry3D:
 |  
 |  get_max_bound(...)
 |      get_max_bound(self: py3d.Geometry3D) -> numpy.ndarray[float64[3, 1]]
 |  
 |  get_min_bound(...)
 |      get_min_bound(self: py3d.Geometry3D) -> numpy.ndarray[float64[3, 1]]
 |  
 |  transform(...)
 |      transform(self: py3d.Geometry3D, arg0: numpy.ndarray[float64[4, 4]]) -> None
 |  
 |  ----------------------------------------------------------------------
 |  Methods inherited from Geometry:
 |  
 |  clear(...)
 |      clear(self: py3d.Geometry) -> None
 |  
 |  dimension(...)
 |      dimension(self: py3d.Geometry) -> int
 |  
 |  get_geometry_type(...)
 |      get_geometry_type(self: py3d.Geometry) -> three::Geometry::GeometryType
 |  
 |  is_empty(...)
 |      is_empty(self: py3d.Geometry) -> bool
 |  
 |  ----------------------------------------------------------------------
 |  Data and other attributes inherited from Geometry:
 |  
 |  Image = Type.Image
 |  
 |  LineSet = Type.LineSet
 |  
 |  PointCloud = Type.PointCloud
 |  
 |  TriangleMesh = Type.TriangleMesh
 |  
 |  Type = <class 'py3d.Geometry.Type'>
 |  
 |  
 |  Unspecified = Type.Unspecified
 |  
 |  ----------------------------------------------------------------------
 |  Methods inherited from pybind11_builtins.pybind11_object:
 |  
 |  __new__(*args, **kwargs) from pybind11_builtins.pybind11_type
 |      Create and return a new object.  See help(type) for accurate signature.

In [8]:
downpcd.has_normals()
Out[8]:
True
In [9]:
downpcd.normals
Out[9]:
std::vector<Eigen::Vector3d> with 113662 elements.
Use numpy.asarray() to access data.
In [13]:
import numpy as np
pcd_normals=np.asarray(downpcd.normals)
print(pcd_normals.shape)
print(pcd_normals[:5])
(113662, 3)
[[ 0.99498315 -0.08507544 -0.05263738]
 [ 0.99441002 -0.09168508 -0.05236943]
 [ 0.99441002 -0.09168508 -0.05236943]
 [ 0.99441002 -0.09168508 -0.05236943]
 [ 0.99829398 -0.05599475 -0.01654424]]

法線が計算されれれば、pcdデータにその情報がつけ加わる。法線データがあるかどうかは has_normals()メソッドで確かめられる。また、データそのものは、上記で見たようにnormals属性値(numpy.asarray変換を通して)によりアクセスできる。