NeRF近年火遍大江南北,但是训练时间长(五六小时起步)的问题一直是美中不足的地方。于是,各种魔改版本你方唱罢我登场,层出不穷。但是,instant-ngp一出,把大家给整破防了,居然特喵的能这么快?训练一个场景几分钟甚至几秒就能出结果,还能实时渲染新视角?还有可视化工具?逆天了呀!哪怕没了解过NeRF的人拿着GUI都能玩一玩!这篇文章给大家梳理一下环境搭建和一些README当中没有说明的隐藏使用,以及一些小demo。
代码地址:https://github.com/NVlabs/instant-ngp
其实NVlabs的README已经很详尽了,一般情况下跟着递归克隆仓库、创建conda虚拟环境、安装requirements和build一下就好了,我至今在多台不同配置(显卡、操作系统、python版本)的电脑上安装过,都是全流程无bug。如果很不幸遇到了的话,他们也已经总结得很详细了,应该不会有太大问题。样例数据集也很轻松就可以跑起来,相信大家一定没有问题!接下来直接上干货了。
这一块其实他们也有文档,点这里。和其他NeRF基本一个套路,拿到图片,跑colmap先做稀疏重建,拿到相机信息和一些图片的位姿信息,得到一个transform.json文件,这就是对这个场景的训练集了。值得注意的是,colmap本身是不够完善的,只凭借视觉,会有一些找不到共视关系的图被丢掉,或者分成不同的component,有可能100张图片最后json文件里只有30张。要解决这个问题有两个办法,一个就是重新拍,尽可能拍得密集切角度不要骤然转换得太厉害,一个办法就是引入GPS信息之类的,不通过colmap,咱直接有pose,那自己再写个脚本存成对应格式就可以了。
重建的话,有以下3种情况:
GUI用起来虽然爽,但是也有局限,输命令能做到的事会更多一点,接下来讲一下命令行使用instant-ngp。
文件结构如下:
|
——instant-ngp
|
——data
|
———场景scene,比如fox
|
| ——— colmap
| ——— images
进入到scene文件夹,调用colmap
python <path-to-instant-ngp>/scripts/colmap2nerf.py --colmap_matcher exhaustive --run_colmap --aabb_scale 16
然后,到instant-ngp目录下:
./build/testbed --mode nerf --scene data/nerf/fox/
情况2:
进入到scene所在的文件夹:
python <path-to-instant-ngp>/scripts/sfm2nerf.py --aabb_scale 16
然后,到instant-ngp目录下:
./build/testbed --mode nerf --scene data/xgrids/yoda/
如果有训练好的模型想直接打开而非从头学习的话:
./build/testbed --mode nerf --scene data/nerf/fox/ --snapshot=data/nerf/fox/base.msgpack
这一步其实是让人头疼的地方,GUI点点点很爽,看起来也很方便,可是这么美好的场景想要保存、要导出却犯了难,怎么办呢?
使用的是marching cube,效果比较差:
python scripts/run.py --scene data/nerf/fox/ --mode nerf --load_snapshot data/nerf/fox/base.msgpack --save_mesh data/nerf/fox/mesh.obj
–scene 场景的路径
–mode 模式,选nerf即可
–load_snapshot 保存的训练好的模型
–screenshot_transforms 需要渲染的角度,数据结构和nerf的json格式一样
–screenshot_frames 渲染哪一帧,如果想全部渲染就不要这个参数,会默认渲染全部
–screenshot_dir 渲染好的图片存储的位置
–width 图片宽度
–height 图片高度
样例数据Nerf fox:
python scripts/run.py --scene data/nerf/fox/ --mode nerf --load_snapshot data/nerf/fox/base.msgpack --screenshot_transforms data/nerf/fox/transforms.json --screenshot_frames 1 --screenshot_dir data/nerf/fox/screenshot --width 2048 --height 2048 --n_steps 0
自己的数据:
python scripts/run.py --scene <path-to-scene> --mode nerf --load_snapshot <path-to-snapshot> --screenshot_transforms <path-to-scene>/transforms.json --screenshot_frames 1 --screenshot_dir <path-to-screenshot-folder> --width 2048 --height 2048 --n_steps 0
最简单的办法自然就是把前面训练集的transform.json对应的那些视角渲染的图片串起来了,但是这样有两个问题。第一,受限于拍摄的不可靠性,无法确认所有训练集图片渲染出来的也很好,人为再挑选太麻烦。第二,我们可能会想要看到沿着某个路径行走或者向四周旋转查看的效果,我们应该提供人为选择一些新视角图片然后再渲染的功能。基于此,我写了一个通过在GUI里自己选择的渲染视角来合成一个视频的脚本。
在GUI当中选择新的视角,然后在camera path窗口add,这样这个视角下的相机参数会被添加到base_cam.json当中。
运行rendervideo.py
python scripts/rendervideo.py --scene <scene_path> --n_seconds <seconds> --fps <fps> --render_name <name> --width <resolution_width> --height <resolution_height>
比如:
python scripts/rendervideo.py --scene data/nerf/fox --n_seconds 10 --fps 60 --render_name foxvideo --width 1920 --height 1080
然后mp4视频都会被保存在instant-ngp下(而非各自数据集下),所以注意命名的时候就要做好区分。
rendervideo.py的代码如下:
#!/usr/bin/env python3
# Copyright (c) 2020-2022, NVIDIA CORPORATION. All rights reserved.
#
# NVIDIA CORPORATION and its licensors retain all intellectual property
# and proprietary rights in and to this software, related documentation
# and any modifications thereto. Any use, reproduction, disclosure or
# distribution of this software and related documentation without an express
# license agreement from NVIDIA CORPORATION is strictly prohibited.
import os, sys, shutil
import argparse
from tqdm import tqdm
import common
import pyngp as ngp # noqa
import numpy as np
"""Render the video based on specific camera poses.
Render a series of images from specific views and then generate a smoothing video.
Args:
scene:
The scene to load. Can be the scene's name or a full path to the training data.
width:
Resolution width of video
height:
Resolution width of video
n_seconds:
Number of seconds
fps:
FPS of the video
render_name:
Name of video.
Returns:
None. A video will be saved at scene root path
"""
def render_video(resolution, numframes, scene, name, spp, fps, exposure=0):
testbed = ngp.Testbed(ngp.TestbedMode.Nerf)
testbed.load_snapshot(os.path.join(scene, "base.msgpack"))
testbed.load_camera_path(os.path.join(scene, "base_cam.json"))
if 'temp' in os.listdir():
shutil.rmtree('temp')
os.makedirs('temp')
for i in tqdm(list(range(min(numframes,numframes+1))), unit="frames", desc=f"Rendering"):
testbed.camera_smoothing = i > 0
frame = testbed.render(resolution[0], resolution[1], spp, True, float(i)/numframes, float(i + 1)/numframes, fps, shutter_fraction=0.5)
common.write_image(f"temp/{i:04d}.jpg", np.clip(frame * 2**exposure, 0.0, 1.0), quality=100)
os.system(f"ffmpeg -i temp/%04d.jpg -vf \"fps={fps}\" -c:v libx264 -pix_fmt yuv420p {name}_test.mp4")
shutil.rmtree('temp')
def parse_args():
parser = argparse.ArgumentParser(description="render neural graphics primitives testbed")
parser.add_argument("--scene", "--training_data", default="", help="The scene to load. Can be the scene's name or a full path to the training data.")
parser.add_argument("--width", "--screenshot_w", type=int, default=1920, help="Resolution width of the render video")
parser.add_argument("--height", "--screenshot_h", type=int, default=1080, help="Resolution height of the render video")
parser.add_argument("--n_seconds", type=int, default=1, help="Number of seconds")
parser.add_argument("--fps", type=int, default=60, help="number of fps")
parser.add_argument("--render_name", type=str, default="", help="name of the result video")
args = parser.parse_args()
return args
if __name__ == "__main__":
args = parse_args()
render_video([args.width, args.height], args.n_seconds*args.fps, args.scene, args.render_name, spp=8, fps=args.fps)