「詳解3次元点群処理」;金崎朝子・秋月秀一・千葉直也著 読書感想
本日紹介するのは、金崎朝子・秋月秀一・千葉直也著「詳解3次元点群処理」。
著者陣は全員ロボティクス方面の研究者。私にとってはほとんど馴染みのない分野だが、素人なりにSFアニメや科学番組でこの手の3次元データ処理のような場面を見る度にかっこいいという厨二的な憧れがあった。最近、私のような専門外の人間でも読みやすそうな本が出版されたので、ちょっとした冒険のつもりで読んでみた。
本書はコーディングがメインのソフトウェアの技術書であり、特にPythonのOpen3Dライブラリの入門書という側面が強い。センサなどハード面については紹介のみで、サンプルデータは全て公開データを用いているので、実際にセンサを持っている必要はない。また理論面の説明もそれほど多くはないのだが、門外漢の私には目新しい知識が多く、非常に勉強になった。理論面で特に印象に残ったのは以下の2つ。
回転行列、オイラー角、クォータニオンの関係
この3つの回転の表現方法について、私は知識としてそういうものがあるとは認識していた*1。しかしそれはあくまでも点の知識であり、なぜ似たような概念があるのか、何が違うのかを疑問に思ったことすらなかった。本書を読むことで、今まで点在していた知識が整理されてつながり、線や面になった感覚がある。
点群の法線ベクトルの求め方
点群の法線ベクトルを求めるには、ある注目点の近傍点を求め、近傍点群の3次元座標に対して主成分分析を行う。固有値が最も小さい軸(近傍点群の座標の分散が最も小さい、つまり「その方向に点群が平べったく分布している」軸)がその注目点の法線ベクトルになる。機械学習分野の次元削減の観点では、固有値が大きい(分散が大きい)方が情報量が多いと解釈するのだが、目的によっては分散が小さい方に利用価値があるという点が、私にとっては新鮮で、面白い!
Google Colabでの可視化
本書のサンプルコードでは可視化はopen3d.visualizationモジュールを用いて、GUI Windowで行っている。私はGoogle Colabでプログラムを動かすために、Plotlyを用いて可視化を行った。
open3d.visualization.draw_giometries()関数の代わりとしては、こちらのページのdraw_geometries()関数をベースに、以下のような関数を作成した。
import plotly.graph_objects as go import open3d as o3d import numpy as np def draw_geometries(geometries, show=True): def cubes(size, pos_x, pos_y, pos_z, color): # https://community.plotly.com/t/plotly-graph-objects-volume-cube/75455/4 x, y, z = np.meshgrid( np.linspace(pos_x-size/2, pos_x+size/2, 2), np.linspace(pos_y-size/2, pos_y+size/2, 2), np.linspace(pos_z-size/2, pos_z+size/2, 2), ) x, y, z = x.flatten(), y.flatten(), z.flatten() R, G, B = color return go.Mesh3d(x=x, y=y, z=z, alphahull=1, flatshading=True, color=f"rgb({R},{G},{B})") graph_objects = [] for geometry in geometries: geometry_type = geometry.get_geometry_type() if geometry_type == o3d.geometry.Geometry.Type.PointCloud: points = np.asarray(geometry.points) colors = None if geometry.has_colors(): colors = np.asarray(geometry.colors) elif geometry.has_normals(): colors = (0.5, 0.5, 0.5) + np.asarray(geometry.normals) * 0.5 else: geometry.paint_uniform_color((1.0, 0.0, 0.0)) colors = np.asarray(geometry.colors) scatter_3d = go.Scatter3d(x=points[:,0], y=points[:,1], z=points[:,2], mode='markers', marker=dict(size=1, color=colors)) graph_objects.append(scatter_3d) if geometry_type == o3d.geometry.Geometry.Type.TriangleMesh: triangles = np.asarray(geometry.triangles) vertices = np.asarray(geometry.vertices) colors = None if geometry.has_triangle_normals(): colors = (0.5, 0.5, 0.5) + np.asarray(geometry.triangle_normals) * 0.5 colors = tuple(map(tuple, colors)) elif geometry.has_vertex_colors(): # 第3章で追加 colors = np.asarray(geometry.vertex_colors) else: colors = (1.0, 0.0, 0.0) mesh_3d = go.Mesh3d(x=vertices[:,0], y=vertices[:,1], z=vertices[:,2], i=triangles[:,0], j=triangles[:,1], k=triangles[:,2], facecolor=colors, opacity=0.50) graph_objects.append(mesh_3d) # 追加 if geometry_type == o3d.geometry.Geometry.Type.LineSet: lines = np.asarray(geometry.lines) points = np.asarray(geometry.points) colors = None if geometry.has_colors(): colors = (np.asarray(geometry.colors) * 255).astype(int) else: geometry.paint_uniform_color((0.0, 0.0, 0.0)) colors = (np.asarray(geometry.colors) * 255).astype(int) for (i, j), (R, G, B) in zip(lines, colors): line_3d = go.Scatter3d( x=points[[i, j], 0], y=points[[i, j], 1], z=points[[i, j], 2], mode="lines", line=dict(width=1, color=f"rgb({R},{G},{B})") ) graph_objects.append(line_3d) # 追加 if geometry_type == o3d.geometry.Geometry.Type.VoxelGrid: centers = [] colors = [] has_colors = geometry.has_colors() for voxel in geometry.get_voxels(): center = np.asarray(geometry.get_voxel_center_coordinate(voxel.grid_index)) centers.append(center) if has_colors: colors.append(np.asarray(voxel.color)) else: colors.append((0.0, 0.0, 0.0)) centers = np.asarray(centers) colors = (np.asarray(colors) * 255).astype(np.uint8) voxel_size = geometry.voxel_size for center, color in zip(centers, colors): cube = cubes(voxel_size, center[0], center[1], center[2], color=color) graph_objects.append(cube) fig = go.Figure( data=graph_objects, layout=dict( scene=dict( xaxis=dict(visible=False), yaxis=dict(visible=False), zaxis=dict(visible=False) ) ) ) if show: fig.show() else: return fig, graph_objects
また、第4章のためにPlotlyでアニメーションを表示する部分はこちらのページを参考にした。
# フレームの作成 pcds = reg.pcds indices = reg.closest_indices pcd_t = reg.pcd_t line_list = [GetCorrespondenceLines(pcd_s, pcd_t, index) for pcd_s, index in zip(pcds, indices)] frames = [ go.Frame( data=draw_geometries([pcd_t, pcd_s, lines], show=False)[1], layout=dict( scene=dict( xaxis=dict(visible=False), yaxis=dict(visible=False), zaxis=dict(visible=False) ) ) ) for pcd_s, lines in zip(pcds, line_list) ]
# アニメーションの表示 fig, _ = draw_geometries([pcd_t, pcds[0], line_list[0]], show=False) fig.update_layout( title="ICP Animation", title_x=0.5, width=600, height=600, updatemenus=[ dict( buttons=[ dict( args=[ None, { "frame": {"duration": 50, "redraw": True}, "fromcurrent": True, "transition": {"duration": 0} } ], label="Play", method="animate" ) ], type='buttons', showactive=False, y=1, x=1.8, xanchor='right', yanchor='top' ) ] ) fig.update(frames=frames) fig.show()
最後に
自分にとって未知の分野であり、きちんと理解できたかは怪しいものの、久々に「新しいことを学んだ」という感触があった。この手の分野は環境構築が難しいイメージがあったが、本書のサンプルコードはPythonのみで完結し、Open3Dもpipで簡単にインストールできるので、専門外の人でも簡単に遊ぶことができる。3次元データをぐりぐり動かすだけでも面白いので、興味のある方がいたらぜひ読んでみてほしい。
