mirror of
https://github.com/nerfstudio-project/nerfstudio.git
synced 2026-11-11 18:44:21 +08:00
* first commit for clickable web viewer * pylint ignore * functional clickable viewer with updated docs * fixed logging
509 lines
21 KiB
Python
509 lines
21 KiB
Python
# Copyright 2022 The Plenoptix Team. All rights reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
"""Code to interface with the `vis/` (the JS visualizer).
|
|
"""
|
|
|
|
import enum
|
|
import logging
|
|
import sys
|
|
import threading
|
|
import time
|
|
from typing import Any, Dict
|
|
|
|
import numpy as np
|
|
import torch
|
|
|
|
from nerfactory.cameras.cameras import Cameras
|
|
from nerfactory.cameras.rays import RayBundle
|
|
from nerfactory.configs import base as cfg
|
|
from nerfactory.dataloaders.image_dataset import ImageDataset
|
|
from nerfactory.dataloaders.structs import DatasetInputs
|
|
from nerfactory.models.base import Model
|
|
from nerfactory.utils import profiler, visualization
|
|
from nerfactory.utils.decorators import check_visualizer_enabled, decorate_all
|
|
from nerfactory.utils.writer import GLOBAL_BUFFER, EventName
|
|
from nerfactory.viewer.server.subprocess import run_viewer_bridge_server_as_subprocess
|
|
from nerfactory.viewer.server.utils import get_intrinsics_matrix_and_camera_to_world_h
|
|
from nerfactory.viewer.server.visualizer import Viewer
|
|
|
|
|
|
class OutputTypes(str, enum.Enum):
|
|
"""Noncomprehsnive list of output render types"""
|
|
|
|
INIT = "init"
|
|
RGB = "rgb"
|
|
RGB_FINE = "rgb_fine"
|
|
|
|
|
|
class ColormapTypes(str, enum.Enum):
|
|
"""Noncomprehsnive list of colormap render types"""
|
|
|
|
INIT = "init"
|
|
DEFAULT = "default"
|
|
TURBO = "turbo"
|
|
DEPTH = "depth"
|
|
SEMANTIC = "semantic"
|
|
BOOLEAN = "boolean"
|
|
|
|
|
|
class IOChangeException(Exception):
|
|
"""Basic camera exception to interrupt visualizer"""
|
|
|
|
|
|
class SetTrace:
|
|
"""Basic trace function"""
|
|
|
|
def __init__(self, func):
|
|
self.func = func
|
|
|
|
def __enter__(self):
|
|
sys.settrace(self.func)
|
|
return self
|
|
|
|
def __exit__(self, ext_type, exc_value, traceback):
|
|
sys.settrace(None)
|
|
|
|
|
|
class RenderThread(threading.Thread):
|
|
"""Thread that does all the rendering calls while listening for interrupts
|
|
|
|
Args:
|
|
state: current visualizer state object
|
|
graph: current checkpoint of model
|
|
camera_ray_bundle: input rays to pass through the graph to render out
|
|
"""
|
|
|
|
def __init__(self, state: "VisualizerState", graph: Model, camera_ray_bundle: RayBundle):
|
|
threading.Thread.__init__(self)
|
|
self.state = state
|
|
self.graph = graph
|
|
self.camera_ray_bundle = camera_ray_bundle
|
|
self.exc = None
|
|
self.vis_outputs = None
|
|
|
|
def run(self):
|
|
"""run function that renders out images given the current graph and ray bundles.
|
|
Interlaced with a trace function that checks to see if any I/O changes were registered.
|
|
Exits and continues program if IOChangeException thrown.
|
|
"""
|
|
outputs = None
|
|
try:
|
|
with SetTrace(self.state.check_interrupt):
|
|
outputs = self.graph.get_outputs_for_camera_ray_bundle(self.camera_ray_bundle)
|
|
except IOChangeException as e:
|
|
self.exc = e
|
|
|
|
if outputs:
|
|
self.vis_outputs = outputs
|
|
|
|
self.state.check_done_render = True
|
|
self.state.check_interrupt_vis = False
|
|
|
|
def join(self, timeout=None):
|
|
threading.Thread.join(self)
|
|
if self.exc:
|
|
raise self.exc
|
|
|
|
|
|
class CheckThread(threading.Thread):
|
|
"""Thread the constantly checks for io changes and sets a flag indicating interrupt
|
|
|
|
Args:
|
|
state: current visualizer state object
|
|
"""
|
|
|
|
def __init__(self, state):
|
|
threading.Thread.__init__(self)
|
|
self.state = state
|
|
|
|
def run(self):
|
|
"""Run function that checks to see if any of the existing state has changed
|
|
(e.g. camera pose/output type/resolutions).
|
|
Sets the visualizer state flag to true to signal
|
|
to render thread that an interrupt was registered.
|
|
"""
|
|
self.state.check_done_render = False
|
|
while not self.state.check_done_render:
|
|
# check camera
|
|
data = self.state.vis["renderingState/camera"].read()
|
|
if data is not None:
|
|
camera_object = data["object"]
|
|
if self.state.prev_camera_matrix is None or not np.allclose(
|
|
camera_object["matrix"], self.state.prev_camera_matrix
|
|
):
|
|
self.state.check_interrupt_vis = True
|
|
return
|
|
|
|
# check output type
|
|
output_type = self.state.vis["renderingState/output_choice"].read()
|
|
if output_type is None:
|
|
output_type = OutputTypes.INIT
|
|
if self.state.prev_output_type != output_type:
|
|
self.state.check_interrupt_vis = True
|
|
return
|
|
|
|
# check colormap type
|
|
colormap_type = self.state.vis["renderingState/colormap_choice"].read()
|
|
if colormap_type is None:
|
|
colormap_type = ColormapTypes.INIT
|
|
if self.state.prev_colormap_type != colormap_type:
|
|
self.state.check_interrupt_vis = True
|
|
return
|
|
|
|
# check max render
|
|
max_resolution = self.state.vis["renderingState/maxResolution"].read()
|
|
if max_resolution is not None:
|
|
if self.state.max_resolution != max_resolution:
|
|
self.state.check_interrupt_vis = True
|
|
return
|
|
|
|
# check min render
|
|
min_resolution = self.state.vis["renderingState/minResolution"].read()
|
|
if min_resolution is not None:
|
|
if self.state.min_resolution != min_resolution:
|
|
self.state.check_interrupt_vis = True
|
|
return
|
|
|
|
|
|
@decorate_all([check_visualizer_enabled])
|
|
class VisualizerState:
|
|
"""Class to hold state for visualizer variables
|
|
|
|
Args:
|
|
config: viewer setup configuration
|
|
"""
|
|
|
|
def __init__(self, config: cfg.ViewerConfig):
|
|
self.config = config
|
|
|
|
self.vis = None
|
|
self.viewer_url = None
|
|
if self.config.enable:
|
|
if self.config.launch_bridge_server:
|
|
# start the viewer bridge server
|
|
zmq_port = int(self.config.zmq_url.split(":")[-1])
|
|
websocket_port = self.config.websocket_port
|
|
run_viewer_bridge_server_as_subprocess(zmq_port, websocket_port)
|
|
# TODO(ethan): move this into the writer such that it's at the bottom
|
|
# of the logging stack and easy to see and click
|
|
# TODO(ethan): log the output of the viewer bridge server in a file where the training logs go
|
|
print("\n")
|
|
self.viewer_url = (
|
|
f"https://viewer.nerfactory.com/branch/master/?websocket_url=localhost:{websocket_port}"
|
|
)
|
|
viewer_url_local = f"http://localhost:4000/?websocket_url=localhost:{websocket_port}"
|
|
pub_open_viewer_instructions_string = f'[Public] Open the viewer at "{self.viewer_url}"'
|
|
dev_open_viewer_instructions_string = f'[Local] Open the viewer at "{viewer_url_local}"'
|
|
print("-" * len(pub_open_viewer_instructions_string))
|
|
print(pub_open_viewer_instructions_string)
|
|
print(dev_open_viewer_instructions_string)
|
|
print("-" * len(pub_open_viewer_instructions_string))
|
|
print("\n")
|
|
self.vis = Viewer(zmq_url=self.config.zmq_url)
|
|
else:
|
|
logging.info("Continuing without viewer.")
|
|
|
|
# visualizer specific variables
|
|
self.prev_camera_matrix = None
|
|
self.prev_output_type = OutputTypes.INIT
|
|
self.prev_colormap_type = ColormapTypes.INIT
|
|
self.output_type_changed = True
|
|
self.min_resolution = 50
|
|
self.max_resolution = 1000
|
|
self.res_upscale_factor = 1
|
|
self.check_interrupt_vis = False
|
|
self.check_done_render = True
|
|
self.last_render_time = time.time()
|
|
self.min_wait_time = 0.5 # 1.0 is on high side and will cause lag
|
|
|
|
self.outputs_set = False
|
|
|
|
def init_scene(self, image_dataset: ImageDataset, dataset_inputs: DatasetInputs) -> None:
|
|
"""Draw some images and the scene aabb in the viewer.
|
|
|
|
Args:
|
|
image_dataset: dataset to render in the scene
|
|
dataset_inputs: inputs to the image dataset and ray generator
|
|
"""
|
|
|
|
# clear the current scene
|
|
self.vis["sceneState/sceneBounds"].delete()
|
|
self.vis["sceneState/cameras"].delete()
|
|
|
|
# draw the training cameras and images
|
|
image_indices = range(len(image_dataset))
|
|
for idx in image_indices:
|
|
image = image_dataset[idx]["image"]
|
|
bgr = image[..., [2, 1, 0]]
|
|
camera_json = dataset_inputs.cameras.to_json(camera_idx=idx, image=bgr, resize_shape=(100, 100))
|
|
self.vis[f"sceneState/cameras/{idx:06d}"].write(camera_json)
|
|
|
|
# draw the scene bounds (i.e., the bounding box)
|
|
json_ = dataset_inputs.scene_bounds.to_json()
|
|
self.vis["sceneState/sceneBounds"].write(json_)
|
|
|
|
# set the properties of the camera
|
|
# self.vis["renderingState/camera"].write(json_)
|
|
|
|
# set the main camera intrinsics to one from the dataset
|
|
# K = camera.get_intrinsics_matrix()
|
|
# set_persp_intrinsics_matrix(self.vis, K.double().numpy())
|
|
|
|
def update_scene(self, step: int, graph: Model) -> None:
|
|
"""updates the scene based on the graph weights
|
|
|
|
Args:
|
|
step: iteration step of training
|
|
graph: the current checkpoint of the model
|
|
"""
|
|
|
|
is_training = self.vis["renderingState/isTraining"].read()
|
|
|
|
if is_training is None or is_training:
|
|
# in training mode, render every few steps
|
|
if self._is_render_step(step):
|
|
self._render_image_in_viewer(graph)
|
|
else:
|
|
# in pause training mode, enter render loop with set graph
|
|
local_step = step
|
|
run_loop = not is_training
|
|
while run_loop:
|
|
if self._is_render_step(local_step):
|
|
self._render_image_in_viewer(graph)
|
|
is_training = self.vis["renderingState/isTraining"].read()
|
|
run_loop = not is_training
|
|
local_step += 1
|
|
|
|
def check_interrupt(self, frame, event, arg): # pylint: disable=unused-argument
|
|
"""Raises interrupt when flag has been set and not already on lowest resolution.
|
|
Used in conjunction with SetTrace.
|
|
"""
|
|
if event == "line":
|
|
if self.check_interrupt_vis and self.res_upscale_factor > 1:
|
|
self.res_upscale_factor = 1
|
|
raise IOChangeException
|
|
return self.check_interrupt
|
|
|
|
def _is_render_step(self, step: int, default_steps: int = 2, max_steps: int = 10) -> bool:
|
|
"""dynamically calculate when to render grapic based on resolution of image"""
|
|
if step != 0:
|
|
if self.res_upscale_factor == 1:
|
|
return True
|
|
steps_per_render_image = min(default_steps * self.res_upscale_factor, max_steps)
|
|
steps_condition = step % steps_per_render_image == 0
|
|
if steps_condition:
|
|
if self.res_upscale_factor > 3:
|
|
if time.time() - self.last_render_time >= self.min_wait_time:
|
|
self.last_render_time = time.time()
|
|
return True # if higher res, and minimum wait time achieved
|
|
return False # if higher res, and minimum wait time NOT achieved
|
|
self.last_render_time = time.time()
|
|
return True # if not higher res, but steps met
|
|
return False # if init
|
|
|
|
def _apply_colormap(self, outputs: Dict[str, Any], stuff_colors: torch.Tensor = None, eps=1e-6):
|
|
"""Determines which colormap to use based on set colormap type
|
|
|
|
Args:
|
|
outputs: the output tensors for which to apply colormaps on
|
|
stuff_colors: is only set if colormap is for semantics. Defaults to None.
|
|
"""
|
|
# default for rgb images
|
|
if self.prev_colormap_type == ColormapTypes.DEFAULT and outputs[self.prev_output_type].shape[-1] == 3:
|
|
return outputs[self.prev_output_type]
|
|
|
|
# rendering depth outputs
|
|
if self.prev_colormap_type == ColormapTypes.DEPTH or (
|
|
self.prev_colormap_type == ColormapTypes.DEFAULT
|
|
and outputs[self.prev_output_type].dtype == torch.float
|
|
and (torch.max(outputs[self.prev_output_type]) - 1.0) > eps # handle floating point arithmetic
|
|
):
|
|
return visualization.apply_depth_colormap(
|
|
outputs[self.prev_output_type], accumulation=outputs["accumulation"]
|
|
)
|
|
|
|
# rendering accumulation outputs
|
|
if self.prev_colormap_type == ColormapTypes.TURBO or (
|
|
self.prev_colormap_type == ColormapTypes.DEFAULT and outputs[self.prev_output_type].dtype == torch.float
|
|
):
|
|
return visualization.apply_colormap(outputs[self.prev_output_type])
|
|
|
|
# rendering semantic outputs
|
|
if self.prev_colormap_type == ColormapTypes.SEMANTIC or (
|
|
self.prev_colormap_type == ColormapTypes.DEFAULT and outputs[self.prev_output_type].dtype == torch.int
|
|
):
|
|
logits = outputs[self.prev_output_type]
|
|
labels = torch.argmax(torch.nn.functional.softmax(logits, dim=-1), dim=-1) # type: ignore
|
|
assert stuff_colors is not None
|
|
return stuff_colors[labels]
|
|
|
|
# rendering boolean outputs
|
|
if self.prev_colormap_type == ColormapTypes.BOOLEAN or (
|
|
self.prev_colormap_type == ColormapTypes.DEFAULT and outputs[self.prev_output_type].dtype == torch.bool
|
|
):
|
|
return visualization.apply_boolean_colormap(outputs[self.prev_output_type])
|
|
|
|
raise NotImplementedError
|
|
|
|
def _send_output_to_viewer(self, outputs: Dict[str, Any], stuff_colors: torch.Tensor = None, eps=1e-6):
|
|
"""Chooses the correct output and sends it to the viewer
|
|
|
|
Args:
|
|
outputs: the dictionary of outputs to choose from, from the graph
|
|
stuff_colors: is only set if colormap is for semantics. Defaults to None.
|
|
"""
|
|
if not self.outputs_set:
|
|
self.vis["renderingState/output_options"].write(list(outputs.keys()))
|
|
self.outputs_set = True
|
|
# gross hack to get the image key, depending on which keys the graph uses
|
|
if self.prev_output_type == OutputTypes.INIT:
|
|
self.prev_output_type = OutputTypes.RGB if OutputTypes.RGB in outputs else OutputTypes.RGB_FINE
|
|
# re-register colormaps and send to viewer
|
|
if self.output_type_changed or self.prev_colormap_type == ColormapTypes.INIT:
|
|
self.prev_colormap_type = ColormapTypes.DEFAULT
|
|
colormap_options = [ColormapTypes.DEFAULT]
|
|
if (
|
|
outputs[self.prev_output_type].shape[-1] != 3
|
|
and outputs[self.prev_output_type].dtype == torch.float
|
|
and (torch.max(outputs[self.prev_output_type]) - 1.0) <= eps # handle floating point arithmetic
|
|
):
|
|
# accumulation can also include depth
|
|
colormap_options.extend(["depth"])
|
|
self.output_type_changed = False
|
|
self.vis["renderingState/colormap_choice"].write(self.prev_colormap_type)
|
|
self.vis["renderingState/colormap_options"].write(colormap_options)
|
|
selected_output = self._apply_colormap(outputs, stuff_colors).cpu().numpy()
|
|
image_output = selected_output * 255
|
|
image = (image_output).astype("uint8")
|
|
self.vis.set_image(image)
|
|
|
|
def _update_viewer_stats(self, render_time: float, image_height: int) -> None:
|
|
"""Function that calculates and populates all the rendering statistics accordingly
|
|
|
|
Args:
|
|
render_time: total time spent rendering current view
|
|
image_height: resolution of the current view
|
|
"""
|
|
is_training = self.vis["renderingState/isTraining"].read()
|
|
if is_training is None or is_training:
|
|
# process the current rendering fps
|
|
eval_fps = f"{1 / render_time:.2f} fps at {image_height} res"
|
|
self.vis["renderingState/eval_fps"].write(eval_fps)
|
|
# process remaining training ETA
|
|
self.vis["renderingState/train_eta"].write(GLOBAL_BUFFER["events"].get(EventName.ETA.value, "Starting"))
|
|
# process ratio time spent on vis vs train
|
|
if EventName.ITER_VIS_TIME.value in GLOBAL_BUFFER["events"]:
|
|
vis_time = GLOBAL_BUFFER["events"][EventName.ITER_VIS_TIME.value]["avg"]
|
|
train_time = GLOBAL_BUFFER["events"][EventName.ITER_TRAIN_TIME.value]["avg"]
|
|
vis_train_ratio = f"{int(vis_time / train_time * 100)}% spent on viewer"
|
|
self.vis["renderingState/vis_train_ratio"].write(vis_train_ratio)
|
|
else:
|
|
self.vis["renderingState/vis_train_ratio"] = "Starting"
|
|
else:
|
|
self.vis["renderingState/eval_fps"].write("Paused")
|
|
self.vis["renderingState/train_eta"].write("Paused")
|
|
self.vis["renderingState/vis_train_ratio"].write("100% spent on viewer")
|
|
|
|
@profiler.time_function
|
|
def _render_image_in_viewer(self, graph: Model) -> None:
|
|
"""
|
|
Draw an image using the current camera pose from the viewer.
|
|
The image is sent of a TCP connection and then uses WebRTC to send it to the viewer.
|
|
"""
|
|
# check and perform camera updates
|
|
data = self.vis["renderingState/camera"].read()
|
|
if data is None:
|
|
return
|
|
camera_object = data["object"]
|
|
# hacky way to prevent overflow check to see if < 100; TODO(make less hacky)
|
|
if self.prev_camera_matrix is not None and np.allclose(camera_object["matrix"], self.prev_camera_matrix):
|
|
self.res_upscale_factor = min(self.res_upscale_factor * 2, 100)
|
|
else:
|
|
self.prev_camera_matrix = camera_object["matrix"]
|
|
self.res_upscale_factor = 1
|
|
|
|
# check and perform output type updates
|
|
output_type = self.vis["renderingState/output_choice"].read()
|
|
output_type = OutputTypes.INIT if output_type is None else output_type
|
|
self.output_type_changed = self.prev_output_type != output_type
|
|
self.prev_output_type = output_type
|
|
|
|
# check and perform colormap type updates
|
|
colormap_type = self.vis["renderingState/colormap_choice"].read()
|
|
colormap_type = ColormapTypes.INIT if colormap_type is None else colormap_type
|
|
self.prev_colormap_type = colormap_type
|
|
|
|
# check and perform min/max update
|
|
max_resolution = self.vis["renderingState/maxResolution"].read()
|
|
if max_resolution:
|
|
self.max_resolution = max_resolution
|
|
|
|
min_resolution = self.vis["renderingState/minResolution"].read()
|
|
if min_resolution:
|
|
self.min_resolution = min_resolution
|
|
|
|
damped_upsacale_factor = 1 if self.res_upscale_factor < 8 else self.res_upscale_factor
|
|
image_height = min(
|
|
self.min_resolution * damped_upsacale_factor,
|
|
self.max_resolution,
|
|
)
|
|
intrinsics_matrix, camera_to_world_h = get_intrinsics_matrix_and_camera_to_world_h(
|
|
camera_object, image_height=image_height
|
|
)
|
|
|
|
camera_to_world = camera_to_world_h[:3, :]
|
|
camera_to_world = torch.stack(
|
|
[
|
|
camera_to_world[0, :],
|
|
camera_to_world[2, :],
|
|
camera_to_world[1, :],
|
|
],
|
|
dim=0,
|
|
)
|
|
|
|
camera = Cameras(
|
|
fx=intrinsics_matrix[0, 0],
|
|
fy=intrinsics_matrix[1, 1],
|
|
cx=intrinsics_matrix[0, 2],
|
|
cy=intrinsics_matrix[1, 2],
|
|
camera_to_worlds=camera_to_world[None, ...],
|
|
)
|
|
camera = camera.to(graph.device)
|
|
|
|
camera_ray_bundle = camera.generate_rays(camera_indices=0)
|
|
camera_ray_bundle.num_rays_per_chunk = self.config.num_rays_per_chunk
|
|
|
|
graph.eval()
|
|
|
|
check_thread = CheckThread(state=self)
|
|
render_thread = RenderThread(state=self, graph=graph, camera_ray_bundle=camera_ray_bundle)
|
|
start_time = time.time()
|
|
check_thread.start()
|
|
render_thread.start()
|
|
try:
|
|
render_thread.join()
|
|
check_thread.join()
|
|
except Exception: # pylint: disable=broad-except
|
|
pass
|
|
render_duration = time.time() - start_time
|
|
|
|
graph.train()
|
|
|
|
outputs = render_thread.vis_outputs
|
|
if outputs is not None:
|
|
stuff_colors = graph.stuff_colors if hasattr(graph, "stuff_colors") else None
|
|
self._send_output_to_viewer(outputs, stuff_colors=stuff_colors)
|
|
self._update_viewer_stats(render_duration, image_height)
|