Files
nerfstudio/nerfactory/viewer/server/viewer_utils.py
T
Ethan Weber 2cca012ad8 Clickable viewer deployment (#308)
* first commit for clickable web viewer

* pylint ignore

* functional clickable viewer with updated docs

* fixed logging
2022-08-29 22:50:57 -07:00

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)