【NeRF】動画から点群・メッシュ・任意視点動画を生成してみる
NeRFを使えば,点群・メッシュ・任意視点動画が作れるのでやってみた
今回は愛飲するRedBullを被写体にしてみるヨ!
任意視点動画 (GIF版)
https://gyazo.com/a5b73842b81a0e44dd1a6551143567ba
任意視点動画 (動画版)
https://scrapbox.io/files/65a7d778aacff30024486fd6.mp4
点群
https://gyazo.com/026ede4b59d88befb4645e7113b27a0c
https://gyazo.com/e4d3555bd4174bb837e90b98f7ce364f
NeRFとnerfstudioについて簡潔に説明
ボリュームレンダリング
ある点$ xと方向$ dを入力として$ (c,\sigma)を出力する行為.
$ c,\sigmaはそれぞれ色と密度を指す.
画像集合からボリュームレンダリングを行うNNモデル$ F_\theta(x,d)を学習 Lossは元の画像との再構成損失.
自己位置とカメラの{内部, 外部}パラメタを計算するためにCOLMAPを使用 COLMAPのSfMにより3次元点群が得られるが,NeRFではこの点群は使用しない.
「学習」という言葉の注意点
ボリュームレンダリングを行う $ F_\theta(x,d)を学習するため,一つの物体専用のモデルを作るイメージ
したがって,pretrainという概念も,別物体に対する汎化という概念も存在しない
We optimize a separate neural continuous volume representation network for each scene. This requires only a dataset of captured RGB images of the scene, the corresponding camera poses and intrinsic parameters, and scene bounds (we use ground truth camera poses, intrinsics, and bounds for synthetic data, and use the COLMAP structure-from-motion package to estimate these parameters for real data).
nerfstudio
あらゆるNeRF系論文を統一的に実装できるようなフレームワーク
論文もある (SIGGRAPH 2023)
実験環境
テスト環境
GCP V100
nerfstudio == 0.3.4
pycolmap == 0.4.0
hloc == 1.4
前準備: COLMAPをインストール
nerfstudioのDockerfileを使えばこの辺りは不要.
筆者はDockerを使わずに使いたかったので,この手順を踏んだ
code:example.sh
sudo apt-get install \
git \
cmake \
ninja-build \
build-essential \
libboost-program-options-dev \
libboost-filesystem-dev \
libboost-graph-dev \
libboost-system-dev \
libeigen3-dev \
libflann-dev \
libfreeimage-dev \
libmetis-dev \
libgoogle-glog-dev \
libgtest-dev \
libsqlite3-dev \
libglew-dev \
qtbase5-dev \
libqt5opengl5-dev \
libcgal-dev \
libceres-dev
code:example.sh
cd colmap
mkdir build
cd build
cmake .. -GNinja
ninja
sudo ninja install
前準備: nerfstudioをインストール
code:example.sh
cd nerfstudio
pip install -e .
step1: 動画からSfMを実行
ns-process-dataを使えば内部でCOLMAPを呼び出してSfMを実行してくれる code:example.sh
ns-process-data video --data data/redbull/test.mp4 --output-dir data/redbull
https://gyazo.com/95c5a0c8efea7dd12013e76ce2b93aa4
以下のように失敗する場合,COLMAPの性能限界が原因であることが多いので,hlocを使う
ただし,以下のvesionを使うこと
pycolmap == 0.4.0
hloc == 1.4
https://gyazo.com/2b698fe5c36c4b3f4d5918ba2710fd78
https://gyazo.com/e10aa576f5f02757619cc2d112115540
step2: NeRFによりSfMから3Dモデルを構築
SfMからNeRFを学習させる
code:example.sh
ns-train nerfacto --vis viewer --data data/redbull/
https://gyazo.com/37a0ca0cde669980b5dc1c4945f5b57e
VRAM使用量はコレくらい (約17GB / 24GB)
https://gyazo.com/3d975c0217d772c0977950f069f0f660
step3: 結果
SfMにより位置が推定されたカメラ画像と,指定した視点においてボリュームレンダリングされた画像が描画される
https://gyazo.com/445e716a74a92d6ffe0e31b97907063d
https://gyazo.com/e716707a3ed285666892c71a5e868861
https://scrapbox.io/files/65a7d778aacff30024486fd6.mp4
step4: 点群の抽出
code:output_pcd.sh
ns-export pointcloud --load-config outputs/test2/nerfacto/2024-01-17_191700/config.yml --output-dir exports/mesh/ --normal-output-name rgb
https://gyazo.com/026ede4b59d88befb4645e7113b27a0c
https://gyazo.com/e4d3555bd4174bb837e90b98f7ce364f
https://gyazo.com/d566b32a28f4e9836dfa74c03644a3d3
step5: メッシュの作成
code:create_mesh.sh
ns-export poisson --load-config outputs/test2/nerfacto/2024-01-17_191700/config.yml --output-dir exports/mesh/ --normal-output-name rgb
復習
通常,objファイルには表面の特性を記述したmtlファイルと,テクスチャ情報のpngファイルへの参照を保持している
そのため,mtlとテクスチャのpngがファイル直下にあれば,普通にobjファイルを読み込むだけで,テクスチャも読み込んでくれる
描画用コードは後述
https://gyazo.com/1b06d64ca0e71b3b418f3f0bd0341013
https://gyazo.com/fe46bcde1afef27ed8973e2e345965b0
step6: resume
以下のコマンドで学習途中のモデルを再度学習させることができる
code:example.sh
ns-train nerfacto --data data/redbull --load-dir outputs/redbull/nerfacto/2024-01-14_005908/nerfstudio_models
(optional) COLMAP以外の選択肢: Hierarchical-Localization
hloc
COLMAPよりも現代的な技術に基づく.
https://gyazo.com/f144a1367b82548992ed3ec0603a6c25
https://gyazo.com/731426904a5fd038c84904dba810312a
code:install.sh
cd Hierarchical-Localization/
git checkout v1.4
python -m pip install -e .
描画用コード
code:viewer.py
import open3d as o3d
import numpy as np
from sklearn.cluster import KMeans
class GUIEditor:
def __init__(self) -> None:
self.coordinate_frame = o3d.geometry.TriangleMesh.create_coordinate_frame(size=0.1, origin=0, 0, 0) def run(self,obj_path):
# interactive picking
mesh = o3d.io.read_triangle_mesh(obj_path, enable_post_processing=True)
assert mesh.textures
picked_indices = []
while len(picked_indices) < 2:
picked_points = self._pick_points(mesh)
# clastering
vertices = np.asarray(picked_indices)
kmeans = KMeans(n_clusters=2)
kmeans.fit(vertices)
centroids = kmeans.cluster_centers_
# visualize
vis = o3d.visualization.Visualizer()
vis.create_window()
cylinder = self._create_cylinder_between_points(centroids0, centroids1, color=1, 0, 0) vis.add_geometry(cylinder)
vis.add_geometry(mesh)
vis.add_geometry(self.coordinate_frame)
vis.run()
vis.destroy_window()
def _create_cylinder_between_points(self, p1, p2, color, radius=0.001):
distance = np.linalg.norm(p1 - p2)
mid_point = (p1 + p2) / 2
cylinder = o3d.geometry.TriangleMesh.create_cylinder(radius=radius, height=distance)
cylinder.paint_uniform_color(color)
vec = p2 - p1
vec /= np.linalg.norm(vec)
rotation_axis = np.cross(z_axis,vec)
rotation_axis /= np.linalg.norm(rotation_axis)
rotation_angle = np.arccos(np.clip(np.dot(z_axis, vec), -1.0, 1.0))
rotation_matrix = o3d.geometry.get_rotation_matrix_from_axis_angle(rotation_axis * rotation_angle)
cylinder.rotate(rotation_matrix, center=cylinder.get_center())
cylinder.translate(mid_point - cylinder.get_center())
return cylinder
def _pick_points(self,mesh):
print("Showing mesh. Please click on the mesh to select points...")
vis = o3d.visualization.VisualizerWithVertexSelection()
vis.create_window()
vis.add_geometry(mesh)
vis.add_geometry(self.coordinate_frame)
vis.run()
vis.destroy_window()
return vis.get_picked_points()
def main():
editor = GUIEditor()
editor.run("mesh.obj")
if __name__ == "__main__":
main()