# Copyright 2022 the Regents of the University of California, Nerfstudio Team and contributors. 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."""Viewer GUI elements for the nerfstudio viewer"""from__future__importannotationsimportwarningsfromabcimportabstractmethodfromdataclassesimportdataclassfromtypingimportTYPE_CHECKING,Any,Callable,Generic,List,Literal,Optional,Tuple,Union,overloadimportnumpyasnpimporttorchimportviser.transformsasvtffromtyping_extensionsimportLiteralString,TypeVarfromviserimport(GuiButtonGroupHandle,GuiButtonHandle,GuiDropdownHandle,GuiInputHandle,ScenePointerEvent,ViserServer,)fromnerfstudio.cameras.camerasimportCameras,CameraTypefromnerfstudio.utils.rich_utilsimportCONSOLEfromnerfstudio.viewer.utilsimportCameraState,get_cameraifTYPE_CHECKING:fromnerfstudio.viewer.viewerimportViewerTValue=TypeVar("TValue")TString=TypeVar("TString",default=str,bound=str)
[docs]@dataclassclassViewerClick:""" Class representing a click in the viewer as a ray. """# the information here matches the information in the ClickMessage,# but we implement a wrapper as an abstraction layerorigin:Tuple[float,float,float]"""The origin of the click in world coordinates (center of camera)"""direction:Tuple[float,float,float]""" The direction of the click if projected from the camera through the clicked pixel, in world coordinates """screen_pos:Tuple[float,float]"""The screen position of the click in OpenCV screen coordinates, normalized to [0, 1]"""
[docs]@dataclassclassViewerRectSelect:""" Class representing a rectangle selection in the viewer (screen-space). The screen coordinates follow OpenCV image coordinates, with the origin at the top-left corner, but the bounds are also normalized to [0, 1] in both dimensions. """min_bounds:Tuple[float,float]"""The minimum bounds of the rectangle selection in screen coordinates."""max_bounds:Tuple[float,float]"""The maximum bounds of the rectangle selection in screen coordinates."""
[docs]classViewerControl:""" class for exposing non-gui controls of the viewer to the user """def_setup(self,viewer:Viewer):""" Internal use only, setup the viewer control with the viewer state object Args: viewer: The viewer object (viewer.py) """self.viewer:Viewer=viewerself.viser_server:ViserServer=viewer.viser_server
[docs]defset_pose(self,position:Optional[Tuple[float,float,float]]=None,look_at:Optional[Tuple[float,float,float]]=None,instant:bool=False,):""" Set the camera position of the viewer camera. Args: position: The new position of the camera in world coordinates look_at: The new look_at point of the camera in world coordinates instant: If the camera should move instantly or animate to the new position """raiseNotImplementedError()
[docs]defset_fov(self,fov):""" Set the FOV of the viewer camera Args: fov: The new FOV of the camera in degrees """raiseNotImplementedError()
[docs]defset_crop(self,min_point:Tuple[float,float,float],max_point:Tuple[float,float,float]):""" Set the scene crop box of the viewer to the specified min,max point Args: min_point: The minimum point of the crop box max_point: The maximum point of the crop box """raiseNotImplementedError()
[docs]defget_camera(self,img_height:int,img_width:int,client_id:Optional[int]=None)->Optional[Cameras]:""" Returns the Cameras object representing the current camera for the viewer, or None if the viewer is not connected yet Args: img_height: The height of the image to get camera intrinsics for img_width: The width of the image to get camera intrinsics for """clients=self.viser_server.get_clients()iflen(clients)==0:returnNoneifnotclient_id:client_id=list(clients.keys())[0]fromnerfstudio.viewer.viewerimportVISER_NERFSTUDIO_SCALE_RATIOclient=clients[client_id]R=vtf.SO3(wxyz=client.camera.wxyz)R=R@vtf.SO3.from_x_radians(np.pi)R=torch.tensor(R.as_matrix())pos=torch.tensor(client.camera.position,dtype=torch.float64)/VISER_NERFSTUDIO_SCALE_RATIOc2w=torch.concatenate([R,pos[:,None]],dim=1)camera_state=CameraState(fov=client.camera.fov,aspect=client.camera.aspect,c2w=c2w,camera_type=CameraType.PERSPECTIVE)returnget_camera(camera_state,img_height,img_width)
[docs]defregister_click_cb(self,cb:Callable):"""Deprecated, use register_pointer_cb instead."""CONSOLE.log("`register_click_cb` is deprecated, use `register_pointer_cb` instead.")self.register_pointer_cb("click",cb)
[docs]defregister_pointer_cb(self,event_type:Literal["click","rect-select"],cb:Callable[[ViewerClick],None]|Callable[[ViewerRectSelect],None],removed_cb:Optional[Callable[[],None]]=None,):""" Add a callback which will be called when a scene pointer event is detected in the viewer. Scene pointer events include: - "click": A click event, which includes the origin and direction of the click - "rect-select": A rectangle selection event, which includes the screen bounds of the box selection The callback should take a ViewerClick object as an argument if the event type is "click", and a ViewerRectSelect object as an argument if the event type is "rect-select". Args: cb: The callback to call when a click or a rect-select is detected. removed_cb: The callback to run when the pointer event is removed. """fromnerfstudio.viewer.viewerimportVISER_NERFSTUDIO_SCALE_RATIOdefwrapped_cb(scene_pointer_msg:ScenePointerEvent):# Check that the event type is the same as the one we are interested in.ifscene_pointer_msg.event_type!=event_type:raiseValueError(f"Expected event type {event_type}, got {scene_pointer_msg.event_type}")ifscene_pointer_msg.event_type=="click":origin=scene_pointer_msg.ray_origindirection=scene_pointer_msg.ray_directionscreen_pos=scene_pointer_msg.screen_pos[0]assert(originisnotNone)and(directionisnotNone),("Origin and direction should not be None for click event.")origin=tuple([x/VISER_NERFSTUDIO_SCALE_RATIOforxinorigin])assertlen(origin)==3pointer_event=ViewerClick(origin,direction,screen_pos)elifscene_pointer_msg.event_type=="rect-select":pointer_event=ViewerRectSelect(scene_pointer_msg.screen_pos[0],scene_pointer_msg.screen_pos[1])else:raiseValueError(f"Unknown event type: {scene_pointer_msg.event_type}")cb(pointer_event)# type: ignorecb_overriden=Falsewithwarnings.catch_warnings(record=True)asw:# Register the callback with the viser server.self.viser_server.scene.on_pointer_event(event_type=event_type)(wrapped_cb)# If there exists a warning, it's because a callback was overriden.cb_overriden=len(w)>0ifcb_overriden:warnings.warn("A ScenePointer callback has already been registered for this event type. ""The new callback will override the existing one.")# If there exists a cleanup callback after the pointer event is done, register it.ifremoved_cbisnotNone:self.viser_server.scene.on_pointer_callback_removed(removed_cb)
[docs]defunregister_click_cb(self,cb:Optional[Callable]=None):"""Deprecated, use unregister_pointer_cb instead. `cb` is ignored."""warnings.warn("`unregister_click_cb` is deprecated, use `unregister_pointer_cb` instead.")ifcbisnotNone:# raise warningwarnings.warn("cb argument is ignored in unregister_click_cb.")self.unregister_pointer_cb()
[docs]defunregister_pointer_cb(self):""" Remove a callback which will be called, when a scene pointer event is detected in the viewer. Args: cb: The callback to remove """self.viser_server.scene.remove_pointer_callback()
@propertydefserver(self):returnself.viser_server
[docs]classViewerElement(Generic[TValue]):"""Base class for all viewer elements Args: name: The name of the element disabled: If the element is disabled visible: If the element is visible """def__init__(self,name:str,disabled:bool=False,visible:bool=True,cb_hook:Callable=lambdaelement:None,)->None:self.name=nameself.gui_handle:Optional[Union[GuiInputHandle[TValue],GuiButtonHandle,GuiButtonGroupHandle]]=Noneself.disabled=disabledself.visible=visibleself.cb_hook=cb_hook@abstractmethoddef_create_gui_handle(self,viser_server:ViserServer)->None:""" Returns the GuiInputHandle object which actually controls the parameter in the gui. Args: viser_server: The server to install the gui element into. """...
[docs]defremove(self)->None:"""Removes the gui element from the viewer"""ifself.gui_handleisnotNone:self.gui_handle.remove()self.gui_handle=None
[docs]defset_hidden(self,hidden:bool)->None:"""Sets the hidden state of the gui element"""assertself.gui_handleisnotNoneself.gui_handle.visible=nothidden
[docs]defset_disabled(self,disabled:bool)->None:"""Sets the disabled state of the gui element"""assertself.gui_handleisnotNoneself.gui_handle.disabled=disabled
[docs]defset_visible(self,visible:bool)->None:"""Sets the visible state of the gui element"""assertself.gui_handleisnotNoneself.gui_handle.visible=visible
[docs]@abstractmethoddefinstall(self,viser_server:ViserServer)->None:"""Installs the gui element into the given viser_server"""...
[docs]classViewerButton(ViewerElement[bool]):"""A button in the viewer Args: name: The name of the button cb_hook: The function to call when the button is pressed disabled: If the button is disabled visible: If the button is visible """gui_handle:GuiButtonHandledef__init__(self,name:str,cb_hook:Callable[[ViewerButton],Any],disabled:bool=False,visible:bool=True):super().__init__(name,disabled=disabled,visible=visible,cb_hook=cb_hook)def_create_gui_handle(self,viser_server:ViserServer)->None:self.gui_handle=viser_server.gui.add_button(label=self.name,disabled=self.disabled,visible=self.visible)
[docs]classViewerParameter(ViewerElement[TValue],Generic[TValue]):"""A viewer element with state Args: name: The name of the element default_value: The default value of the element disabled: If the element is disabled visible: If the element is visible cb_hook: Callback to call on update """gui_handle:GuiInputHandledef__init__(self,name:str,default_value:TValue,disabled:bool=False,visible:bool=True,cb_hook:Callable=lambdaelement:None,)->None:super().__init__(name,disabled=disabled,visible=visible,cb_hook=cb_hook)self.default_value=default_value
[docs]definstall(self,viser_server:ViserServer)->None:""" Based on the type provided by default_value, installs a gui element inside the given viser_server Args: viser_server: The server to install the gui element into. """self._create_gui_handle(viser_server)assertself.gui_handleisnotNoneself.gui_handle.on_update(lambda_:self.cb_hook(self))
@abstractmethoddef_create_gui_handle(self,viser_server:ViserServer)->None:...@propertydefvalue(self)->TValue:"""Returns the current value of the viewer element"""ifself.gui_handleisNone:returnself.default_valuereturnself.gui_handle.value@value.setterdefvalue(self,value:TValue)->None:ifself.gui_handleisnotNone:self.gui_handle.value=valueelse:self.default_value=value
IntOrFloat=TypeVar("IntOrFloat",int,float)
[docs]classViewerSlider(ViewerParameter[IntOrFloat],Generic[IntOrFloat]):"""A slider in the viewer Args: name: The name of the slider default_value: The default value of the slider min_value: The minimum value of the slider max_value: The maximum value of the slider step: The step size of the slider disabled: If the slider is disabled visible: If the slider is visible cb_hook: Callback to call on update hint: The hint text """def__init__(self,name:str,default_value:IntOrFloat,min_value:IntOrFloat,max_value:IntOrFloat,step:IntOrFloat=0.1,disabled:bool=False,visible:bool=True,cb_hook:Callable[[ViewerSlider],Any]=lambdaelement:None,hint:Optional[str]=None,):assertisinstance(default_value,(float,int))super().__init__(name,default_value,disabled=disabled,visible=visible,cb_hook=cb_hook)self.min=min_valueself.max=max_valueself.step=stepself.hint=hintdef_create_gui_handle(self,viser_server:ViserServer)->None:assertself.gui_handleisNone,"gui_handle should be initialized once"self.gui_handle=viser_server.gui.add_slider(self.name,self.min,self.max,self.step,self.default_value,disabled=self.disabled,visible=self.visible,hint=self.hint,)
[docs]classViewerText(ViewerParameter[str]):"""A text field in the viewer Args: name: The name of the text field default_value: The default value of the text field disabled: If the text field is disabled visible: If the text field is visible cb_hook: Callback to call on update hint: The hint text """def__init__(self,name:str,default_value:str,disabled:bool=False,visible:bool=True,cb_hook:Callable[[ViewerText],Any]=lambdaelement:None,hint:Optional[str]=None,):assertisinstance(default_value,str)super().__init__(name,default_value,disabled=disabled,visible=visible,cb_hook=cb_hook)self.hint=hintdef_create_gui_handle(self,viser_server:ViserServer)->None:assertself.gui_handleisNone,"gui_handle should be initialized once"self.gui_handle=viser_server.gui.add_text(self.name,self.default_value,disabled=self.disabled,visible=self.visible,hint=self.hint)
[docs]classViewerNumber(ViewerParameter[IntOrFloat],Generic[IntOrFloat]):"""A number field in the viewer Args: name: The name of the number field default_value: The default value of the number field disabled: If the number field is disabled visible: If the number field is visible cb_hook: Callback to call on update hint: The hint text """default_value:IntOrFloatdef__init__(self,name:str,default_value:IntOrFloat,disabled:bool=False,visible:bool=True,cb_hook:Callable[[ViewerNumber],Any]=lambdaelement:None,hint:Optional[str]=None,):assertisinstance(default_value,(float,int))super().__init__(name,default_value,disabled=disabled,visible=visible,cb_hook=cb_hook)self.hint=hintdef_create_gui_handle(self,viser_server:ViserServer)->None:assertself.gui_handleisNone,"gui_handle should be initialized once"self.gui_handle=viser_server.gui.add_number(self.name,self.default_value,disabled=self.disabled,visible=self.visible,hint=self.hint)
[docs]classViewerCheckbox(ViewerParameter[bool]):"""A checkbox in the viewer Args: name: The name of the checkbox default_value: The default value of the checkbox disabled: If the checkbox is disabled visible: If the checkbox is visible cb_hook: Callback to call on update hint: The hint text """def__init__(self,name:str,default_value:bool,disabled:bool=False,visible:bool=True,cb_hook:Callable[[ViewerCheckbox],Any]=lambdaelement:None,hint:Optional[str]=None,):assertisinstance(default_value,bool)super().__init__(name,default_value,disabled=disabled,visible=visible,cb_hook=cb_hook)self.hint=hintdef_create_gui_handle(self,viser_server:ViserServer)->None:assertself.gui_handleisNone,"gui_handle should be initialized once"self.gui_handle=viser_server.gui.add_checkbox(self.name,self.default_value,disabled=self.disabled,visible=self.visible,hint=self.hint)
[docs]classViewerDropdown(ViewerParameter[TString],Generic[TString]):"""A dropdown in the viewer Args: name: The name of the dropdown default_value: The default value of the dropdown options: The options of the dropdown disabled: If the dropdown is disabled visible: If the dropdown is visible cb_hook: Callback to call on update hint: The hint text """gui_handle:Optional[GuiDropdownHandle[TString]]def__init__(self,name:str,default_value:TString,options:List[TString],disabled:bool=False,visible:bool=True,cb_hook:Callable[[ViewerDropdown],Any]=lambdaelement:None,hint:Optional[str]=None,):assertdefault_valueinoptionssuper().__init__(name,default_value,disabled=disabled,visible=visible,cb_hook=cb_hook)self.options=optionsself.hint=hintdef_create_gui_handle(self,viser_server:ViserServer)->None:assertself.gui_handleisNone,"gui_handle should be initialized once"self.gui_handle=viser_server.gui.add_dropdown(self.name,self.options,self.default_value,disabled=self.disabled,visible=self.visible,hint=self.hint,# type: ignore)
[docs]defset_options(self,new_options:List[TString])->None:""" Sets the options of the dropdown, Args: new_options: The new options. If the current option isn't in the new options, the first option is selected. """self.options=new_optionsifself.gui_handleisnotNone:self.gui_handle.options=new_options
[docs]classViewerButtonGroup(ViewerParameter[TString],Generic[TString]):"""A button group in the viewer. Unlike other fields, cannot be disabled. Args: name: The name of the button group visible: If the button group is visible options: The options of the button group cb_hook: Callback to call on update """gui_handle:GuiButtonGroupHandledef__init__(self,name:str,default_value:TString,options:List[TString],visible:bool=True,cb_hook:Callable[[ViewerDropdown],Any]=lambdaelement:None,):super().__init__(name,disabled=False,visible=visible,default_value=default_value,cb_hook=cb_hook)self.options=optionsdef_create_gui_handle(self,viser_server:ViserServer)->None:assertself.gui_handleisNone,"gui_handle should be initialized once"self.gui_handle=viser_server.gui.add_button_group(self.name,self.options,visible=self.visible)
[docs]classViewerRGB(ViewerParameter[Tuple[int,int,int]]):""" An RGB color picker for the viewer Args: name: The name of the color picker default_value: The default value of the color picker disabled: If the color picker is disabled visible: If the color picker is visible cb_hook: Callback to call on update hint: The hint text """def__init__(self,name,default_value:Tuple[int,int,int],disabled=False,visible=True,cb_hook:Callable[[ViewerRGB],Any]=lambdaelement:None,hint:Optional[str]=None,):assertlen(default_value)==3super().__init__(name,default_value,disabled=disabled,visible=visible,cb_hook=cb_hook)self.hint=hintdef_create_gui_handle(self,viser_server:ViserServer)->None:self.gui_handle=viser_server.gui.add_rgb(self.name,self.default_value,disabled=self.disabled,visible=self.visible,hint=self.hint)
[docs]classViewerVec3(ViewerParameter[Tuple[float,float,float]]):""" 3 number boxes in a row to input a vector Args: name: The name of the vector default_value: The default value of the vector step: The step of the vector disabled: If the vector is disabled visible: If the vector is visible cb_hook: Callback to call on update hint: The hint text """def__init__(self,name,default_value:Tuple[float,float,float],step=0.1,disabled=False,visible=True,cb_hook:Callable[[ViewerVec3],Any]=lambdaelement:None,hint:Optional[str]=None,):assertlen(default_value)==3super().__init__(name,default_value,disabled=disabled,visible=visible,cb_hook=cb_hook)self.step=stepself.hint=hintdef_create_gui_handle(self,viser_server:ViserServer)->None:self.gui_handle=viser_server.gui.add_vector3(self.name,self.default_value,step=self.step,disabled=self.disabled,visible=self.visible,hint=self.hint)