点群データをボクセルに変換する処理を書いたよ
こんにちは、SAKAMOTOです。
この記事はタイトルの通り、ほとんど報告みたいなものです!
一番伝えたいメッセージは「ぬいぐるみモデリング可愛いよね〜。ていうかLumaAIって凄くね?」であって、実装について解説する記事ではありません(一応公開はしておきます)。
実行結果
可愛い!好きな粒度で点群データをローポリにして遊べます!
ちなみにこの点群データはLumaAIというアプリを使って出力したモデル(OBJ形式)を点群データ(PCD形式)に変換して作成しました。OBJ→PCDの変換にはCloudCompareを使っています。
LumaAIってすごい
このアプリでは、複数の画像データから3Dシーンを生成する技術(NeRF)を応用して、スマホで撮影した動画からオブジェクトの3Dモデル(OBJ形式)を作成することを可能にしています!
↓作成したデータはこんな感じで公開もできる!↓
画像データから3Dモデルを作成できるということは、カメラさえ付いていればモデルが作成できてしまう訳です。
今までLiDAR(レーザースキャナー)を搭載していなければ作成できなかったはずの3Dモデルを誰でも作成できるようにした画期的な技術です。
この技術の応用先
実世界のデータから簡単にローポリデータを作成することができるようになったので、3Dゲーム用素材の作成が爆速になるかも?
地形データに対して実行すれば、実世界をマイクラで再現することも可能です。
実装
こちらに公開しています。
処理の流れ
点群(PCD形式)を読み込む(->o3d.geometry.PointCloud)。
点群をボクセルデータ(o3d.geometry.VoxelGrid)に置き換える。
見やすさのため、ボクセルを囲むLineSet(o3d.geometry.LineSet)を作成する。
作成したVoxelGridとLineSetを描画する。
ボクセルデータをメッシュ(o3d.geometry.TriangleMesh)に変換する。
できたメッシュデータをPLY形式で書き出す。
実行例
poetry run python voxelize.py -i <点群データのパス(str)> -v <ボクセルの一辺の長さ(float)>
import argparse
from typing import List
import numpy as np
import open3d as o3d # type: ignore
def create_mesh_edges(
cube_size: float, cube_center: np.ndarray
) -> o3d.geometry.LineSet:
"""
与えられたサイズと中心点を持つ立方体のメッシュを作成します。(メッシュの枠線)
Parameters:
cube_size (float): 立方体の一辺の長さ
cube_center (np.ndarray): 立方体の中心点
Returns:
o3d.geometry.LineSet: 立方体のメッシュ
"""
vertices = (
np.array(
[
[-1, -1, -1],
[-1, -1, 1],
[-1, 1, -1],
[-1, 1, 1],
[1, -1, -1],
[1, -1, 1],
[1, 1, -1],
[1, 1, 1],
]
)
* cube_size
/ 2
+ cube_center
)
edges = np.array(
[
[0, 1],
[0, 2],
[0, 4],
[1, 3],
[1, 5],
[2, 3],
[2, 6],
[3, 7],
[4, 5],
[4, 6],
[5, 7],
[6, 7],
]
)
lines = o3d.geometry.LineSet()
lines.points = o3d.utility.Vector3dVector(vertices)
lines.lines = o3d.utility.Vector2iVector(edges)
return lines
def create_mesh_cube(
cube_size: float, cube_center: np.ndarray, cube_color: np.ndarray
) -> o3d.geometry.TriangleMesh:
"""
与えられたサイズと中心点を持つ立方体のメッシュを作成します。(立方体の実体)
Parameters:
cube_size (float): 立方体の一辺の長さ
cube_center (np.ndarray): 立方体の中心点
Returns:
o3d.geometry.TriangleMesh: 立方体のメッシュ
"""
cube = o3d.geometry.TriangleMesh.create_box(
width=cube_size, height=cube_size, depth=cube_size
)
cube.translate(
cube_center - np.array([cube_size / 2, cube_size / 2, cube_size / 2])
)
cube.vertex_colors = o3d.utility.Vector3dVector(np.tile(cube_color, (8, 1)))
return cube
def voxel_grid_to_mesh(
voxel_grid: o3d.geometry.VoxelGrid, voxel_size: float
) -> o3d.geometry.TriangleMesh:
"""
VoxelGridをTriangleMeshに変換します。
Parameters:
voxel_grid (o3d.geometry.VoxelGrid): 入力VoxelGrid
voxel_size (float): ボクセルの一辺の長さ
Returns:
o3d.geometry.TriangleMesh: 結合されたメッシュ
"""
combined_mesh = o3d.geometry.TriangleMesh()
# 各ボクセルを立方体メッシュに変換して結合
for voxel in voxel_grid.get_voxels():
cube_center = (
voxel.grid_index * voxel_size + voxel_grid.get_min_bound() + voxel_size / 2
)
cube_color = voxel.color
cube_mesh = create_mesh_cube(voxel_size, cube_center, cube_color)
combined_mesh += cube_mesh
return combined_mesh
def voxelize_pcd(
pcd: o3d.geometry.PointCloud,
voxel_size: float,
) -> tuple[o3d.geometry.VoxelGrid, List[o3d.geometry.LineSet]]:
"""
点群をボクセル化し、ボクセルグリッドとラインセットのリストを返します。
Parameters:
pcd (o3d.geometry.PointCloud): 入力点群
voxel_size (float): ボクセルの一辺の長さ
Returns:
tuple[o3d.geometry.VoxelGrid, List[o3d.geometry.LineSet]]: ボクセルグリッドとラインセットのリスト
"""
voxel_grid = o3d.geometry.VoxelGrid.create_from_point_cloud(
pcd, voxel_size=voxel_size
)
min_bound = voxel_grid.get_min_bound()
points = np.array(
[
(voxel.grid_index * voxel_size + min_bound + voxel_size / 2)
for voxel in voxel_grid.get_voxels()
]
)
colors = np.array([voxel.color for voxel in voxel_grid.get_voxels()])
linesets = []
for voxel in voxel_grid.get_voxels():
cube_center = voxel.grid_index * voxel_size + min_bound + voxel_size / 2
cube = create_mesh_edges(voxel_size, cube_center)
linesets.append(cube)
o3d.visualization.draw_geometries([pcd] + linesets + [voxel_grid])
mesh = voxel_grid_to_mesh(voxel_grid, voxel_size)
o3d.io.write_triangle_mesh("data/voxel.ply", mesh)
return voxel_grid, linesets
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Load and display a point cloud")
parser.add_argument(
"--pcd_path",
"-i",
type=str,
help="Path to the point cloud file (.pcd)",
)
parser.add_argument(
"--voxel_size", "-v", type=float, default=0.2, help="voxel size"
)
args = parser.parse_args()
pcd = o3d.io.read_point_cloud(args.pcd_path)
o3d.visualization.draw_geometries([pcd])
voxel_grid, linesets = voxelize_pcd(pcd, args.voxel_size)