Files
nerfstudio/nerfactory/viewer/server/viewer_utils.py
T
Matthew Tancik 6dc704c551 Rename repo to nerfactory (#204)
* Rename to nerfactory

* Fix missing files
2022-07-25 13:52:20 -07:00

553 lines
20 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 copy
import logging
import random
import sys
import threading
import time
from typing import Any, Dict, List, Optional
import numpy as np
import torch
from nerfactory.utils.decorators import check_visualizer_enabled, decorate_all
import nerfactory.viewer.server.cameras as c
import nerfactory.viewer.server.geometry as g
from nerfactory.cameras.cameras import Camera, get_camera, get_intrinsics_from_intrinsics_matrix
from nerfactory.cameras.rays import RayBundle
from nerfactory.data.image_dataset import ImageDataset
from nerfactory.data.structs import DatasetInputs
from nerfactory.graphs.base import Graph
from nerfactory.utils import profiler
from nerfactory.utils.config import ViewerConfig
from nerfactory.viewer.server.transformations import get_translation_matrix
from nerfactory.viewer.server.utils import get_intrinsics_matrix_and_camera_to_world_h
from nerfactory.viewer.server.visualizer import Viewer
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: Graph, camera_ray_bundle: RayBundle):
threading.Thread.__init__(self)
self.state = state
self.graph = graph
self.camera_ray_bundle = camera_ray_bundle
self.exc = 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.graph.process_outputs_as_images(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["/Cameras/Main Camera"].get_object()
if data is not None:
camera_object = data["object"]["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
data = self.state.vis["/Output Type"].get_object()
if data is None:
output_type = "default"
else:
output_type = data["output_type"]
if self.state.prev_output_type != output_type:
self.state.check_interrupt_vis = True
return
# check max render
data = self.state.vis["/Max Resolution"].get_object()
if data is not None:
max_resolution = int(data["max_resolution"])
if self.state.max_resolution != max_resolution:
self.state.check_interrupt_vis = True
return
# check min render
data = self.state.vis["/Min Resolution"].get_object()
if data is not None:
min_resolution = int(data["min_resolution"])
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: ViewerConfig):
self.config = config
self.vis = None
if self.config.enable:
zmq_url = self.config.zmq_url
self.vis = Viewer(zmq_url=zmq_url)
self.vis.delete()
else:
logging.info("Continuing without viewer.")
# visualizer specific variables
self.prev_camera_matrix = None
self.prev_output_type = "default"
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
"""
indices = random.sample(range(len(image_dataset)), k=10)
for idx in indices:
image = image_dataset[idx]["image"]
camera = get_camera(dataset_inputs.intrinsics[idx], dataset_inputs.camera_to_world[idx], None)
pose = camera.get_camera_to_world().double().numpy()
K = camera.get_intrinsics_matrix().double().numpy()
draw_camera_frustum(
self.vis,
image=(image.double().numpy() * 255.0),
pose=pose,
K=K,
height=1.0,
name=f"image_dataset/{idx:06d}",
displayed_focal_length=0.5,
realistic=False,
)
aabb = dataset_inputs.scene_bounds.aabb
draw_aabb(self.vis, aabb, name="dataset_inputs_train/scene_bounds/aabb")
# 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: Graph) -> None:
"""updates the scene based on the graph weights
Args:
step: iteration step of training
graph: the current checkpoint of the model
"""
data = self.vis["/Training State"].get_object()
if data is None or not data["training_state"]:
# 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 = data["training_state"]
while run_loop:
if self._is_render_step(local_step):
self._render_image_in_viewer(graph)
data = self.vis["/Training State"].get_object()
run_loop = data["training_state"]
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 _send_output_to_viewer(self, outputs: Dict[str, Any]):
"""Chooses the correct output and sends it to the viewer
Args:
outputs: the dictionary of outputs to choose from, from the graph
"""
if not self.outputs_set:
set_output_options(self.vis, 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 == "default":
self.prev_output_type = "rgb" if "rgb" in outputs else "rgb_fine"
image_output = outputs[self.prev_output_type].cpu().numpy() * 255
image = (image_output).astype("uint8")
self.vis["/Cameras/Main Camera"].set_image(image)
@profiler.time_function
def _render_image_in_viewer(self, graph: Graph) -> 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["/Cameras/Main Camera"].get_object()
if data is None:
return
camera_object = data["object"]["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
data = self.vis["/Output Type"].get_object()
if data is None:
output_type = "default"
else:
output_type = data["output_type"]
self.prev_output_type = output_type
# check and perform min/max update
data = self.vis["/Max Resolution"].get_object()
if data is not None:
self.max_resolution = int(data["max_resolution"])
data = self.vis["/Min Resolution"].get_object()
if data is not None:
self.min_resolution = int(data["min_resolution"])
image_height = min(
self.min_resolution * self.res_upscale_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,
)
intrinsics = get_intrinsics_from_intrinsics_matrix(intrinsics_matrix)
camera = get_camera(intrinsics, camera_to_world)
camera_ray_bundle = camera.get_camera_ray_bundle(device=graph.get_device())
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)
check_thread.start()
render_thread.start()
try:
render_thread.join()
check_thread.join()
except Exception: # pylint: disable=broad-except
pass
graph.train()
outputs = graph.vis_outputs
if outputs is not None:
self._send_output_to_viewer(outputs)
def get_default_vis() -> Viewer:
"""Returns the default Visualizer."""
zmq_url = "tcp://0.0.0.0:6000"
viewer = Viewer(zmq_url=zmq_url)
return viewer
def set_output_options(vis: Viewer, output_options: List[str]):
"""Sets the possible list of output options for user to toggle
Args:
vis: current Viewer
output_options: list of possible output types to select from
"""
vis["output_options"].set_output_options(output_options)
def show_box_test(vis: Viewer):
"""Simple test to draw a box and make sure everything is working.
Args:
vis: current Viewer
"""
vis["box"].set_object(g.Box([1.0, 1.0, 1.0]), material=g.MeshPhongMaterial(color=0xFF0000))
def show_ply(vis: Viewer, ply_path: str, name: str = "ply", color: Optional[int] = None):
"""Show the PLY file in the 3D viewer. Specify the full filename as input.
Args:
vis: current Viewer
ply_path: path to ply to load in
name: reference name within viewer storage
color: color to render the ply in
"""
assert ply_path.endswith(".ply")
if color:
material = g.MeshPhongMaterial(color=color)
else:
material = g.MeshPhongMaterial(vertexColors=True)
vis[name].set_object(g.PlyMeshGeometry.from_file(ply_path), material)
def show_obj(vis: Viewer, obj_path: str, name: str = "obj", color: Optional[int] = None):
"""Show the PLY file in the 3D viewer. Specify the full filename as input.
Args:
vis: current Viewer
obj_path: path to obj to load in
name: reference name within viewer storage
color: color to render the obj in
"""
assert obj_path.endswith(".obj")
if color:
material = g.MeshPhongMaterial(color=color)
else:
material = g.MeshPhongMaterial(vertexColors=True)
vis[name].set_object(g.ObjMeshGeometry.from_file(obj_path), material)
def draw_camera_frustum(
vis: Viewer,
image: np.ndarray = np.random.rand(100, 100, 3) * 255.0,
pose: np.ndarray = get_translation_matrix([0, 0, 0]),
K: Optional[np.ndarray] = None,
name: str = "0000000",
displayed_focal_length: Optional[float] = None,
shift_forward: Optional[float] = None,
height: Optional[float] = None,
realistic: bool = True,
):
"""Draw the camera in the scene.
Args:
vis: current Viewer
image: image associated with the current camera
pose: pose of the camera
K: camera matrix
name: reference name within viewer storage
displayed_focal_length: the focal length to be displayed
shift_forward: how much to shift the drawn camera forward
height: height of the associated image plane
realistic: whether to render the image plane at the "realistic" focal length
"""
assert K[0, 0] == K[1, 1]
focal_length = K[0, 0]
pp_w = K[0, 2]
pp_h = K[1, 2]
if displayed_focal_length:
assert height is None or not realistic
if height:
assert displayed_focal_length is None or not realistic
if height:
dfl = height / (2.0 * (pp_h / focal_length))
width = 2.0 * (pp_w / focal_length) * dfl
if displayed_focal_length is None:
displayed_focal_length = dfl
elif displayed_focal_length:
width = 2.0 * (pp_w / focal_length) * displayed_focal_length
height = 2.0 * (pp_h / focal_length) * displayed_focal_length
else:
assert not realistic
if pose.shape == (3, 4):
pose = np.concatenate([pose, np.zeros_like(pose[:1])], axis=0)
pose[3, 3] = 1.0
# draw the frustum
g_frustum = c.frustum(scale=1.0, focal_length=displayed_focal_length, width=width, height=height)
vis[name + "/frustum"].set_object(g_frustum)
if not realistic:
vis[name + "/frustum"].set_transform(get_translation_matrix([0, 0, displayed_focal_length]))
# draw the image plane
g_image_plane = c.ImagePlane(image, width=width, height=height)
vis[name + "/image_plane"].set_object(g_image_plane)
if realistic:
assert displayed_focal_length is not None, "Need to set displayed focal length"
vis[name + "/image_plane"].set_transform(get_translation_matrix([0, 0, -1.0 * displayed_focal_length]))
if shift_forward:
matrix = get_translation_matrix([0, 0, displayed_focal_length])
matrix2 = get_translation_matrix([0, 0, -1.0 * shift_forward])
vis[name + "/frustum"].set_transform(matrix2 @ matrix)
vis[name + "/image_plane"].set_transform(matrix2)
# set the transform of the camera
vis[name].set_transform(pose)
def set_persp_intrinsics_matrix(vis, K):
pp_w = K[0, 2]
pp_h = K[1, 2]
assert K[0, 0] == K[1, 1]
focal_length = K[0, 0]
x = pp_h / (focal_length)
fov = 2.0 * np.arctan(x) * (180.0 / np.pi)
vis["/Cameras/Main Camera/<object>"].set_property("fov", fov)
vis["/Cameras/Main Camera/<object>"].set_property("aspect", float(pp_w / pp_h)) # three.js expects width/height
def set_persp_pose(vis, pose, colmap=True):
pose_processed = copy.deepcopy(pose)
if colmap:
pose_processed[:, 1:3] *= -1
vis["/Cameras/Main Camera/<object>"].set_transform(pose_processed)
def set_persp_camera(vis, pose, K, colmap=True):
"""Assumes simple pinhole model for intrinsics.
Args:
colmap: whether to use the colmap camera coordinate convention or not
"""
set_persp_intrinsics_matrix(vis, K)
set_persp_pose(vis, pose, colmap=colmap)
def set_camera(vis, camera: Camera):
pose = camera.get_camera_to_world_h()
K = camera.get_intrinsics_matrix()
set_persp_camera(vis, pose=pose.double().numpy(), K=K.double().numpy())
def draw_aabb(vis, aabb, name="aabb"):
"""Draw the axis-aligned bounding box."""
lengths = aabb[1] - aabb[0]
w = 1
aaa = np.array([w, w, w])
aab = np.array([w, w, -w])
aba = np.array([w, -w, w])
baa = np.array([-w, w, w])
abb = np.array([w, -w, -w])
bba = np.array([-w, -w, w])
bab = np.array([-w, w, -w])
bbb = np.array([-w, -w, -w])
camera_points = [aaa, aab, aaa, aba, aab, abb, aba, abb]
camera_points.extend([baa, bab, baa, bba, bab, bbb, bba, bbb])
camera_points.extend([aaa, baa, aab, bab, aba, bba, abb, bbb])
lines = np.stack(camera_points)
lines = lines * np.array(lengths) / 2.0
line_segments = g.LineSegments(
g.PointsGeometry(position=lines.astype(np.float32).T),
g.LineBasicMaterial(vertexColors=True),
)
vis[name].set_object(line_segments)
center = aabb[0] + lengths / 2.0
vis[name].set_transform(get_translation_matrix(center.tolist()))