Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CameraTrajectoryPanel to view/edit camera trajectories #153

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions examples/22_custom_gui_component.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"""Advanced GUI - custom GUI components"""

import time
from pathlib import Path

import numpy as onp

import trimesh
import viser
import viser.transforms as tf
from viser import Icon

mesh = trimesh.load_mesh(Path(__file__).parent / "assets/dragon.obj")
assert isinstance(mesh, trimesh.Trimesh)
mesh.apply_scale(0.05)

vertices = mesh.vertices
faces = mesh.faces
print(f"Loaded mesh with {vertices.shape} vertices, {faces.shape} faces")

server = viser.ViserServer()

import_button = server.add_gui_button("Import", icon=Icon.FOLDER_OPEN)
export_button = server.add_gui_button("Export", icon=Icon.DOWNLOAD)

fps = server.add_gui_number("FPS", 24, min=1, icon=Icon.KEYFRAMES, hint="Frames per second")
duration = server.add_gui_number("Duration", 4.0, min=0.1, icon=Icon.CLOCK_HOUR_5, hint="Duration in seconds")
width = server.add_gui_number("Width", 1920, min=100, icon=Icon.ARROWS_HORIZONTAL, hint="Width in px")
height = server.add_gui_number("Height", 1080, min=100, icon=Icon.ARROWS_VERTICAL, hint="Height in px")
fov = server.add_gui_number("FOV", 75, min=1, max=179, icon=Icon.CAMERA, hint="Field of view")
smoothness = server.add_gui_slider("Smoothness", 0.5, min=0.0, max=1.0, step=0.01, hint="Trajectory smoothing")


duration = 4
cameras_slider = server.add_gui_multi_slider(
"Timeline",
min=0.,
max=1.,
step=0.01,
initial_value=[0.0, 0.5, 1.0],
disabled=False,
marks=[(x, f'{x*duration:.1f}s') for x in [0., 0.5, 1.0]],
)

@duration.on_update
def _(_) -> None:
cameras_slider.marks=[(x, f'{x*duration.value:.1f}s') for x in [0., 0.5, 1.0]],

server.add_mesh_simple(
name="/simple",
vertices=vertices,
faces=faces,
wxyz=tf.SO3.from_x_radians(onp.pi / 2).wxyz,
position=(0.0, 0.0, 0.0),
)
server.add_mesh_trimesh(
name="/trimesh",
mesh=mesh.smoothed(),
wxyz=tf.SO3.from_x_radians(onp.pi / 2).wxyz,
position=(0.0, 5.0, 0.0),
)
panel = server.add_gui_camera_trajectory_panel()

while True:
time.sleep(10.0)
117 changes: 114 additions & 3 deletions src/viser/_gui_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,11 @@
GuiModalHandle,
GuiTabGroupHandle,
SupportsRemoveProtocol,
GuiCameraTrajectoryPanelHandle,
_GuiHandleState,
_GuiInputHandle,
_make_unique_id,
)
from ._icons import base64_from_icon
from ._icons_enum import Icon
from ._message_api import MessageApi, cast_vector

Expand Down Expand Up @@ -272,7 +272,7 @@ def add_gui_tab_group(
return GuiTabGroupHandle(
_tab_group_id=tab_group_id,
_labels=[],
_icons_base64=[],
_icons=[],
_tabs=[],
_gui_api=self,
_container_id=self._get_container_id(),
Expand Down Expand Up @@ -366,7 +366,7 @@ def add_gui_button(
hint=hint,
initial_value=False,
color=color,
icon_base64=None if icon is None else base64_from_icon(icon),
icon=None if icon is None else icon.value,
),
disabled=disabled,
visible=visible,
Expand Down Expand Up @@ -534,6 +534,7 @@ def add_gui_number(
visible: bool = True,
hint: Optional[str] = None,
order: Optional[float] = None,
icon: Optional[Icon] = None,
) -> GuiInputHandle[IntOrFloat]:
"""Add a number input to the GUI, with user-specifiable bound and precision parameters.

Expand Down Expand Up @@ -584,6 +585,7 @@ def add_gui_number(
min=min,
max=max,
precision=_compute_precision_digits(step),
icon=icon.value if icon is not None else None,
step=step,
),
disabled=disabled,
Expand Down Expand Up @@ -740,6 +742,41 @@ def add_gui_dropdown(
) -> GuiDropdownHandle[TString]:
...

def add_gui_camera_trajectory_panel(
self,
order: Optional[float] = None,
visible: bool = True,
) -> GuiCameraTrajectoryPanelHandle:
"""
Add a camera trajectory panel to the GUI.

Returns:
A handle that can be used to interact with the GUI element.
"""
id = _make_unique_id()
order = _apply_default_order(order)

# Send add GUI input message.
self._get_api()._queue(_messages.GuiAddCameraTrajectoryPanelMessage(
order=order,
id=id,
container_id=self._get_container_id(),
))

# Construct handle.
handle = GuiCameraTrajectoryPanelHandle(
_gui_api=self,
_container_id=self._get_container_id(),
_visible=True,
_id=id,
_order=order,
)

# Set the visible field. These will queue messages under-the-hood.
if not visible:
handle.visible = visible
return handle

def add_gui_dropdown(
self,
label: str,
Expand Down Expand Up @@ -852,6 +889,80 @@ def add_gui_slider(
is_button=False,
)

def add_gui_multi_slider(
self,
label: str,
min: IntOrFloat,
max: IntOrFloat,
step: IntOrFloat,
initial_value: List[IntOrFloat],
disabled: bool = False,
visible: bool = True,
min_range: Optional[IntOrFloat] = None,
hint: Optional[str] = None,
order: Optional[float] = None,
marks: Optional[List[Tuple[IntOrFloat, Optional[str]]]] = None,
) -> GuiInputHandle[IntOrFloat]:
"""Add a multi slider to the GUI. Types of the min, max, step, and initial value should match.

Args:
label: Label to display on the slider.
min: Minimum value of the slider.
max: Maximum value of the slider.
step: Step size of the slider.
initial_value: Initial values of the slider.
disabled: Whether the slider is disabled.
visible: Whether the slider is visible.
min_range: Optional minimum difference between two values of the slider.
hint: Optional hint to display on hover.
order: Optional ordering, smallest values will be displayed first.

Returns:
A handle that can be used to interact with the GUI element.
"""
assert max >= min
if step > max - min:
step = max - min
assert all(max >= x >= min for x in initial_value)

# GUI callbacks cast incoming values to match the type of the initial value. If
# the min, max, or step is a float, we should cast to a float.
if len(initial_value) > 0 and (type(initial_value[0]) is int and (
type(min) is float or type(max) is float or type(step) is float
)):
initial_value = [float(x) for x in initial_value] # type: ignore

# TODO: as of 6/5/2023, this assert will break something in nerfstudio. (at
# least LERF)
#
# assert type(min) == type(max) == type(step) == type(initial_value)

id = _make_unique_id()
order = _apply_default_order(order)
return self._create_gui_input(
initial_value=initial_value,
message=_messages.GuiAddMultiSliderMessage(
order=order,
id=id,
label=label,
container_id=self._get_container_id(),
hint=hint,
min=min,
min_range=min_range,
max=max,
step=step,
initial_value=initial_value,
precision=_compute_precision_digits(step),
marks=[
_messages.GuiSliderMark(value=x, label=label)
for x, label in marks
] if marks is not None else None,
),
disabled=disabled,
visible=visible,
is_button=False,
)

def add_gui_rgb(
self,
label: str,
Expand Down
47 changes: 42 additions & 5 deletions src/viser/_gui_handles.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
import numpy as onp
from typing_extensions import Protocol

from ._icons import base64_from_icon
from ._icons_enum import Icon
from ._message_api import _encode_image_base64
from ._messages import (
Expand Down Expand Up @@ -332,7 +331,7 @@ def options(self, options: Iterable[StringType]) -> None:
class GuiTabGroupHandle:
_tab_group_id: str
_labels: List[str]
_icons_base64: List[Optional[str]]
_icons: List[Optional[str]]
_tabs: List[GuiTabHandle]
_gui_api: GuiApi
_container_id: str # Parent.
Expand All @@ -352,7 +351,7 @@ def add_tab(self, label: str, icon: Optional[Icon] = None) -> GuiTabHandle:
out = GuiTabHandle(_parent=self, _id=id)

self._labels.append(label)
self._icons_base64.append(None if icon is None else base64_from_icon(icon))
self._icons.append(None if icon is None else icon.value)
self._tabs.append(out)

self._sync_with_client()
Expand All @@ -372,7 +371,7 @@ def _sync_with_client(self) -> None:
id=self._tab_group_id,
container_id=self._container_id,
tab_labels=tuple(self._labels),
tab_icons_base64=tuple(self._icons_base64),
tab_icons=tuple(self._icons),
tab_container_ids=tuple(tab._id for tab in self._tabs),
)
)
Expand Down Expand Up @@ -495,7 +494,7 @@ def remove(self) -> None:
self._parent._gui_api._container_handle_from_id.pop(self._id)

self._parent._labels.pop(container_index)
self._parent._icons_base64.pop(container_index)
self._parent._icons.pop(container_index)
self._parent._tabs.pop(container_index)
self._parent._sync_with_client()

Expand Down Expand Up @@ -596,3 +595,41 @@ def remove(self) -> None:
"""Permanently remove this markdown from the visualizer."""
api = self._gui_api._get_api()
api._queue(GuiRemoveMessage(self._id))


@dataclasses.dataclass
class GuiCameraTrajectoryPanelHandle:
_gui_api: GuiApi
_id: str
_visible: bool
_container_id: str # Parent.
_order: float

@property
def order(self) -> float:
"""Read-only order value, which dictates the position of the GUI element."""
return self._order

@property
def visible(self) -> bool:
"""Temporarily show or hide this GUI element from the visualizer. Synchronized
automatically when assigned."""
return self._visible

@visible.setter
def visible(self, visible: bool) -> None:
if visible == self.visible:
return

self._gui_api._get_api()._queue(GuiSetVisibleMessage(self._id, visible=visible))
self._visible = visible

def __post_init__(self) -> None:
"""We need to register ourself after construction for callbacks to work."""
parent = self._gui_api._container_handle_from_id[self._container_id]
parent._children[self._id] = self

def remove(self) -> None:
"""Permanently remove this markdown from the visualizer."""
api = self._gui_api._get_api()
api._queue(GuiRemoveMessage(self._id))
Loading