4
0
mirror of https://github.com/nerfstudio-project/nerfstudio.git synced 2026-11-11 02:24:38 +08:00
nerfstudio/scripts/exporter.py
Brent Yi 6a7c8820b8
Viewer backend refactor (#1702)
* Copy in `viser` communication infrastructure

* Update background image

* Add build directory to fix viser HTTP server
May want to remove these later

* Copy in `viser` communication infrastructure

* Update background image

* Pass dataset vis using viser

* initial impl of ViewerParameter

* idk what i did but brent and matt know

* Suppress eslint typescript warnings

* implement ViewerElement base class, then ViewerButton, ViewerSlider, etc

* client -> server message setup

* multi folder stuff

* Stream controls over viser

* import

* add types to viewer_param.py

* Move another message to viser

* Update folder color

* Use non throttled messages

* multiple folders works

* Cleanup, more porting

* starting impl of control panel in viser

* More porting

* scenebox to viser

* Cleanup

* Fix render updates

* More cleanup, docs, typing

* start control_panel.py

* set_hidden logic but not merged in

* control_panel replicates behavior of existing control panel (visually), except for setting the list of output options. not yet integrated with viewer_utils.py for logic flow. also implemented set_disabled and set_hidden in javscript/ViewerElement side

* Remove some original leva components

* add some train util logic, attempt set_value, doesn't work on javascript side fully yet (value updates but not slider visually

* bye zmq

* missing call to decoder

* 'gui_set' works properly, control panel now replicates behavior of original

* remove print

* Fix status viewer message

* More cleanup

* support set_options in dropdown

* hiding buttons works correctly

* fix set_options, switch from cur_value to get_value()

* Fix slowdown

* output options

* colormap logic converted

* add handle to cb_hook

* fix some lambda bugs

* cleanup

* Save checkpoint logic readded

* Cleanup and minor fixes

* snappier ui

* Refactoring

* Fix state update bugs

* Bug fixes

* Write less to redux

* added comments to reducer code

* Render state machine (#1727)

* state machine works, but train step throttling not implemented

* more step logic, cleanup

* remove random try catcH

* mutex logic for render

* Update lock logic

* types and docs

* added some state machine logic to help rerenders

* merge

---------

Co-authored-by: Justin <justin.g.kerr@gmail.com>

* Refactor and cleanup

* More cleanup and docs

* docstring to gui_utils

* docstrings, rename viewer_param.py to viewer_elements.py

* Fix viewer, remove bridge stuff

* Render on connect

* Fix lint errors

* More eslint fixes

* render_freq logic

* better rerender logic for crop

* More cleanup

* Purge

* Cleanup

* Lint fixes

* Fix cache

* better parsing logic

* Fix infinite test

* More cleanup

* typo

* Update webdocs

* Update docs

* rerender on custom hooks

* add more docs

* Use shared utilities in `viser.infra`

* rework callback to use elements instead of handles

* Fix ESlint errors

* change naming of handle to element, add setter for value

* cleanup

---------

Co-authored-by: Matt Tancik <mattt4001@gmail.com>
Co-authored-by: Justin <justin.g.kerr@gmail.com>
Co-authored-by: Ethan Weber <eweb0124@gmail.com>
Co-authored-by: Matthew Tancik <tancik@berkeley.edu>
2023-04-14 14:53:10 -07:00

433 lines
17 KiB
Python

"""
Script for exporting NeRF into other formats.
"""
# pylint: disable=no-member
from __future__ import annotations
import json
import os
import sys
from dataclasses import dataclass, field
from pathlib import Path
from typing import List, Optional, Tuple, Union
import numpy as np
import open3d as o3d
import torch
import tyro
from rich.console import Console
from typing_extensions import Annotated, Literal
from nerfstudio.cameras.rays import RayBundle
from nerfstudio.exporter import texture_utils, tsdf_utils
from nerfstudio.exporter.exporter_utils import (
collect_camera_poses,
generate_point_cloud,
get_mesh_from_filename,
)
from nerfstudio.exporter.marching_cubes import (
generate_mesh_with_multires_marching_cubes,
)
from nerfstudio.pipelines.base_pipeline import Pipeline, VanillaPipeline
from nerfstudio.utils.eval_utils import eval_setup
CONSOLE = Console(width=120)
@dataclass
class Exporter:
"""Export the mesh from a YML config to a folder."""
load_config: Path
"""Path to the config YAML file."""
output_dir: Path
"""Path to the output directory."""
@dataclass
class ExportPointCloud(Exporter):
"""Export NeRF as a point cloud."""
num_points: int = 1000000
"""Number of points to generate. May result in less if outlier removal is used."""
remove_outliers: bool = True
"""Remove outliers from the point cloud."""
estimate_normals: bool = False
"""Estimate normals for the point cloud."""
depth_output_name: str = "depth"
"""Name of the depth output."""
rgb_output_name: str = "rgb"
"""Name of the RGB output."""
use_bounding_box: bool = True
"""Only query points within the bounding box"""
bounding_box_min: Tuple[float, float, float] = (-1, -1, -1)
"""Minimum of the bounding box, used if use_bounding_box is True."""
bounding_box_max: Tuple[float, float, float] = (1, 1, 1)
"""Maximum of the bounding box, used if use_bounding_box is True."""
num_rays_per_batch: int = 32768
"""Number of rays to evaluate per batch. Decrease if you run out of memory."""
std_ratio: float = 10.0
"""Threshold based on STD of the average distances across the point cloud to remove outliers."""
def main(self) -> None:
"""Export point cloud."""
if not self.output_dir.exists():
self.output_dir.mkdir(parents=True)
_, pipeline, _, _ = eval_setup(self.load_config)
# Increase the batchsize to speed up the evaluation.
pipeline.datamanager.train_pixel_sampler.num_rays_per_batch = self.num_rays_per_batch
pcd = generate_point_cloud(
pipeline=pipeline,
num_points=self.num_points,
remove_outliers=self.remove_outliers,
estimate_normals=self.estimate_normals,
rgb_output_name=self.rgb_output_name,
depth_output_name=self.depth_output_name,
normal_output_name=None,
use_bounding_box=self.use_bounding_box,
bounding_box_min=self.bounding_box_min,
bounding_box_max=self.bounding_box_max,
std_ratio=self.std_ratio,
)
torch.cuda.empty_cache()
CONSOLE.print(f"[bold green]:white_check_mark: Generated {pcd}")
CONSOLE.print("Saving Point Cloud...")
tpcd = o3d.t.geometry.PointCloud.from_legacy(pcd)
# The legacy PLY writer converts colors to UInt8,
# let us do the same to save space.
tpcd.point.colors = (tpcd.point.colors * 255).to(o3d.core.Dtype.UInt8)
o3d.t.io.write_point_cloud(str(self.output_dir / "point_cloud.ply"), tpcd)
print("\033[A\033[A")
CONSOLE.print("[bold green]:white_check_mark: Saving Point Cloud")
@dataclass
class ExportTSDFMesh(Exporter):
"""
Export a mesh using TSDF processing.
"""
downscale_factor: int = 2
"""Downscale the images starting from the resolution used for training."""
depth_output_name: str = "depth"
"""Name of the depth output."""
rgb_output_name: str = "rgb"
"""Name of the RGB output."""
resolution: Union[int, List[int]] = field(default_factory=lambda: [128, 128, 128])
"""Resolution of the TSDF volume or [x, y, z] resolutions individually."""
batch_size: int = 10
"""How many depth images to integrate per batch."""
use_bounding_box: bool = True
"""Whether to use a bounding box for the TSDF volume."""
bounding_box_min: Tuple[float, float, float] = (-1, -1, -1)
"""Minimum of the bounding box, used if use_bounding_box is True."""
bounding_box_max: Tuple[float, float, float] = (1, 1, 1)
"""Minimum of the bounding box, used if use_bounding_box is True."""
texture_method: Literal["tsdf", "nerf"] = "nerf"
"""Method to texture the mesh with. Either 'tsdf' or 'nerf'."""
px_per_uv_triangle: int = 4
"""Number of pixels per UV triangle."""
unwrap_method: Literal["xatlas", "custom"] = "xatlas"
"""The method to use for unwrapping the mesh."""
num_pixels_per_side: int = 2048
"""If using xatlas for unwrapping, the pixels per side of the texture image."""
target_num_faces: Optional[int] = 50000
"""Target number of faces for the mesh to texture."""
def main(self) -> None:
"""Export mesh"""
if not self.output_dir.exists():
self.output_dir.mkdir(parents=True)
_, pipeline, _, _ = eval_setup(self.load_config)
tsdf_utils.export_tsdf_mesh(
pipeline,
self.output_dir,
self.downscale_factor,
self.depth_output_name,
self.rgb_output_name,
self.resolution,
self.batch_size,
use_bounding_box=self.use_bounding_box,
bounding_box_min=self.bounding_box_min,
bounding_box_max=self.bounding_box_max,
)
# possibly
# texture the mesh with NeRF and export to a mesh.obj file
# and a material and texture file
if self.texture_method == "nerf":
# load the mesh from the tsdf export
mesh = get_mesh_from_filename(
str(self.output_dir / "tsdf_mesh.ply"), target_num_faces=self.target_num_faces
)
CONSOLE.print("Texturing mesh with NeRF")
texture_utils.export_textured_mesh(
mesh,
pipeline,
self.output_dir,
px_per_uv_triangle=self.px_per_uv_triangle if self.unwrap_method == "custom" else None,
unwrap_method=self.unwrap_method,
num_pixels_per_side=self.num_pixels_per_side,
)
@dataclass
class ExportPoissonMesh(Exporter):
"""
Export a mesh using poisson surface reconstruction.
"""
num_points: int = 1000000
"""Number of points to generate. May result in less if outlier removal is used."""
remove_outliers: bool = True
"""Remove outliers from the point cloud."""
depth_output_name: str = "depth"
"""Name of the depth output."""
rgb_output_name: str = "rgb"
"""Name of the RGB output."""
normal_method: Literal["open3d", "model_output"] = "model_output"
"""Method to estimate normals with."""
normal_output_name: str = "normals"
"""Name of the normal output."""
save_point_cloud: bool = False
"""Whether to save the point cloud."""
use_bounding_box: bool = True
"""Only query points within the bounding box"""
bounding_box_min: Tuple[float, float, float] = (-1, -1, -1)
"""Minimum of the bounding box, used if use_bounding_box is True."""
bounding_box_max: Tuple[float, float, float] = (1, 1, 1)
"""Minimum of the bounding box, used if use_bounding_box is True."""
num_rays_per_batch: int = 32768
"""Number of rays to evaluate per batch. Decrease if you run out of memory."""
texture_method: Literal["point_cloud", "nerf"] = "nerf"
"""Method to texture the mesh with. Either 'point_cloud' or 'nerf'."""
px_per_uv_triangle: int = 4
"""Number of pixels per UV triangle."""
unwrap_method: Literal["xatlas", "custom"] = "xatlas"
"""The method to use for unwrapping the mesh."""
num_pixels_per_side: int = 2048
"""If using xatlas for unwrapping, the pixels per side of the texture image."""
target_num_faces: Optional[int] = 50000
"""Target number of faces for the mesh to texture."""
std_ratio: float = 10.0
"""Threshold based on STD of the average distances across the point cloud to remove outliers."""
def validate_pipeline(self, pipeline: Pipeline) -> None:
"""Check that the pipeline is valid for this exporter."""
if self.normal_method == "model_output":
CONSOLE.print("Checking that the pipeline has a normal output.")
origins = torch.zeros((1, 3), device=pipeline.device)
directions = torch.ones_like(origins)
pixel_area = torch.ones_like(origins[..., :1])
camera_indices = torch.zeros_like(origins[..., :1])
ray_bundle = RayBundle(
origins=origins, directions=directions, pixel_area=pixel_area, camera_indices=camera_indices
)
outputs = pipeline.model(ray_bundle)
if self.normal_output_name not in outputs:
CONSOLE.print(
f"[bold yellow]Warning: Normal output '{self.normal_output_name}' not found in pipeline outputs."
)
CONSOLE.print(f"Available outputs: {list(outputs.keys())}")
CONSOLE.print(
"[bold yellow]Warning: Please train a model with normals "
"(e.g., nerfacto with predicted normals turned on)."
)
CONSOLE.print("[bold yellow]Warning: Or change --normal-method")
CONSOLE.print("[bold yellow]Exiting early.")
sys.exit(1)
def main(self) -> None:
"""Export mesh"""
if not self.output_dir.exists():
self.output_dir.mkdir(parents=True)
_, pipeline, _, _ = eval_setup(self.load_config)
self.validate_pipeline(pipeline)
# Increase the batchsize to speed up the evaluation.
pipeline.datamanager.train_pixel_sampler.num_rays_per_batch = self.num_rays_per_batch
# Whether the normals should be estimated based on the point cloud.
estimate_normals = self.normal_method == "open3d"
pcd = generate_point_cloud(
pipeline=pipeline,
num_points=self.num_points,
remove_outliers=self.remove_outliers,
estimate_normals=estimate_normals,
rgb_output_name=self.rgb_output_name,
depth_output_name=self.depth_output_name,
normal_output_name=self.normal_output_name if self.normal_method == "model_output" else None,
use_bounding_box=self.use_bounding_box,
bounding_box_min=self.bounding_box_min,
bounding_box_max=self.bounding_box_max,
std_ratio=self.std_ratio,
)
torch.cuda.empty_cache()
CONSOLE.print(f"[bold green]:white_check_mark: Generated {pcd}")
if self.save_point_cloud:
CONSOLE.print("Saving Point Cloud...")
o3d.io.write_point_cloud(str(self.output_dir / "point_cloud.ply"), pcd)
print("\033[A\033[A")
CONSOLE.print("[bold green]:white_check_mark: Saving Point Cloud")
CONSOLE.print("Computing Mesh... this may take a while.")
mesh, densities = o3d.geometry.TriangleMesh.create_from_point_cloud_poisson(pcd, depth=9)
vertices_to_remove = densities < np.quantile(densities, 0.1)
mesh.remove_vertices_by_mask(vertices_to_remove)
print("\033[A\033[A")
CONSOLE.print("[bold green]:white_check_mark: Computing Mesh")
CONSOLE.print("Saving Mesh...")
o3d.io.write_triangle_mesh(str(self.output_dir / "poisson_mesh.ply"), mesh)
print("\033[A\033[A")
CONSOLE.print("[bold green]:white_check_mark: Saving Mesh")
# This will texture the mesh with NeRF and export to a mesh.obj file
# and a material and texture file
if self.texture_method == "nerf":
# load the mesh from the poisson reconstruction
mesh = get_mesh_from_filename(
str(self.output_dir / "poisson_mesh.ply"), target_num_faces=self.target_num_faces
)
CONSOLE.print("Texturing mesh with NeRF")
texture_utils.export_textured_mesh(
mesh,
pipeline,
self.output_dir,
px_per_uv_triangle=self.px_per_uv_triangle if self.unwrap_method == "custom" else None,
unwrap_method=self.unwrap_method,
num_pixels_per_side=self.num_pixels_per_side,
)
@dataclass
class ExportMarchingCubesMesh(Exporter):
"""Export a mesh using marching cubes."""
isosurface_threshold: float = 0.0
"""The isosurface threshold for extraction. For SDF based methods the surface is the zero level set."""
resolution: int = 1024
"""Marching cube resolution."""
simplify_mesh: bool = False
"""Whether to simplify the mesh."""
bounding_box_min: Tuple[float, float, float] = (-1.0, -1.0, -1.0)
"""Minimum of the bounding box."""
bounding_box_max: Tuple[float, float, float] = (1.0, 1.0, 1.0)
"""Maximum of the bounding box."""
px_per_uv_triangle: int = 4
"""Number of pixels per UV triangle."""
unwrap_method: Literal["xatlas", "custom"] = "xatlas"
"""The method to use for unwrapping the mesh."""
num_pixels_per_side: int = 2048
"""If using xatlas for unwrapping, the pixels per side of the texture image."""
target_num_faces: Optional[int] = 50000
"""Target number of faces for the mesh to texture."""
def main(self) -> None:
"""Main function."""
if not self.output_dir.exists():
self.output_dir.mkdir(parents=True)
_, pipeline, _, _ = eval_setup(self.load_config)
# TODO: Make this work with Density Field
assert hasattr(pipeline.model.config, "sdf_field"), "Model must have an SDF field."
CONSOLE.print("Extracting mesh with marching cubes... which may take a while")
assert (
self.resolution % 512 == 0
), f"""resolution must be divisible by 512, got {self.resolution}.
This is important because the algorithm uses a multi-resolution approach
to evaluate the SDF where the mimimum resolution is 512."""
# Extract mesh using marching cubes for sdf at a multi-scale resolution.
multi_res_mesh = generate_mesh_with_multires_marching_cubes(
geometry_callable_field=lambda x: pipeline.model.field.forward_geonetwork(x)[:, 0].contiguous(),
resolution=self.resolution,
bounding_box_min=self.bounding_box_min,
bounding_box_max=self.bounding_box_max,
isosurface_threshold=self.isosurface_threshold,
coarse_mask=None,
)
filename = self.output_dir / "sdf_marching_cubes_mesh.ply"
multi_res_mesh.export(filename)
# load the mesh from the marching cubes export
mesh = get_mesh_from_filename(str(filename), target_num_faces=self.target_num_faces)
CONSOLE.print("Texturing mesh with NeRF...")
texture_utils.export_textured_mesh(
mesh,
pipeline,
self.output_dir,
px_per_uv_triangle=self.px_per_uv_triangle if self.unwrap_method == "custom" else None,
unwrap_method=self.unwrap_method,
num_pixels_per_side=self.num_pixels_per_side,
)
@dataclass
class ExportCameraPoses(Exporter):
"""
Export camera poses to a .json file.
"""
def main(self) -> None:
"""Export camera poses"""
if not self.output_dir.exists():
self.output_dir.mkdir(parents=True)
_, pipeline, _, _ = eval_setup(self.load_config)
assert isinstance(pipeline, VanillaPipeline)
train_frames, eval_frames = collect_camera_poses(pipeline)
for file_name, frames in [("transforms_train.json", train_frames), ("transforms_eval.json", eval_frames)]:
if len(frames) == 0:
CONSOLE.print(f"[bold yellow]No frames found for {file_name}. Skipping.")
continue
output_file_path = os.path.join(self.output_dir, file_name)
with open(output_file_path, "w", encoding="UTF-8") as f:
json.dump(frames, f, indent=4)
CONSOLE.print(f"[bold green]:white_check_mark: Saved poses to {output_file_path}")
Commands = tyro.conf.FlagConversionOff[
Union[
Annotated[ExportPointCloud, tyro.conf.subcommand(name="pointcloud")],
Annotated[ExportTSDFMesh, tyro.conf.subcommand(name="tsdf")],
Annotated[ExportPoissonMesh, tyro.conf.subcommand(name="poisson")],
Annotated[ExportMarchingCubesMesh, tyro.conf.subcommand(name="marching-cubes")],
Annotated[ExportCameraPoses, tyro.conf.subcommand(name="cameras")],
]
]
def entrypoint():
"""Entrypoint for use with pyproject scripts."""
tyro.extras.set_accent_color("bright_yellow")
tyro.cli(Commands).main()
if __name__ == "__main__":
entrypoint()
# For sphinx docs
get_parser_fn = lambda: tyro.extras.get_parser(Commands) # noqa