diff --git a/nerfview/render_panel.py b/nerfview/render_panel.py index a81fa3e..bce9cb5 100644 --- a/nerfview/render_panel.py +++ b/nerfview/render_panel.py @@ -935,17 +935,17 @@ def compute_and_update_preview_camera_state() -> ( return time = None if len(maybe_pose_and_fov_rad) == 3: # Time is enabled. - pose, fov_rad, time = maybe_pose_and_fov_rad + pose, fov, time = maybe_pose_and_fov_rad render_tab_state.preview_time = time else: - pose, fov_rad = maybe_pose_and_fov_rad - render_tab_state.preview_fov = fov_rad + pose, fov = maybe_pose_and_fov_rad + render_tab_state.preview_fov = fov render_tab_state.preview_aspect = camera_path.get_aspect() if time is not None: - return pose, fov_rad, time + return pose, fov, time else: - return pose, fov_rad + return pose, fov def add_preview_frame_slider() -> Optional[viser.GuiInputHandle[int]]: """Helper for creating the current frame # slider. This is removed and @@ -974,13 +974,13 @@ def _(_) -> None: if maybe_pose_and_fov_rad is None: return if len(maybe_pose_and_fov_rad) == 3: # Time is enabled. - pose, fov_rad, time = maybe_pose_and_fov_rad + pose, fov, time = maybe_pose_and_fov_rad else: - pose, fov_rad = maybe_pose_and_fov_rad + pose, fov = maybe_pose_and_fov_rad preview_camera_handle = server.scene.add_camera_frustum( "/preview_camera", - fov=fov_rad, + fov=fov, aspect=render_res_vec2.value[0] / render_res_vec2.value[1], scale=0.35, wxyz=pose.rotation().wxyz, @@ -988,11 +988,12 @@ def _(_) -> None: color=(10, 200, 30), ) if render_tab_state.preview_render: - for client in server.get_clients().values(): - # aspect ratio is not assignable, pass args in get_render instead - client.camera.wxyz = pose.rotation().wxyz - client.camera.position = pose.translation() - client.camera.fov = fov_rad + with server.atomic(): + for client in server.get_clients().values(): + # aspect ratio is not assignable, pass args in get_render instead + client.camera.wxyz = pose.rotation().wxyz + client.camera.position = pose.translation() + client.camera.fov = fov return preview_frame_slider @@ -1020,33 +1021,35 @@ def _(_) -> None: server.scene.set_global_visibility(False) # Back up and then set camera poses. - for client in server.get_clients().values(): - camera_pose_backup_from_id[client.client_id] = ( - client.camera.position, - client.camera.look_at, - client.camera.up_direction, - ) - client.camera.wxyz = pose.rotation().wxyz - client.camera.position = pose.translation() + with server.atomic(): + for client in server.get_clients().values(): + camera_pose_backup_from_id[client.client_id] = ( + client.camera.position, + client.camera.look_at, + client.camera.up_direction, + ) + client.camera.wxyz = pose.rotation().wxyz + client.camera.position = pose.translation() @preview_render_stop_button.on_click def _(_) -> None: render_tab_state.preview_render = False preview_save_camera_path_button.visible = True preview_render_stop_button.visible = False - dump_video_button.disabled = False + dump_video_button.disabled = not play_button.visible # Revert camera poses. - for client in server.get_clients().values(): - if client.client_id not in camera_pose_backup_from_id: - continue - cam_position, cam_look_at, cam_up = camera_pose_backup_from_id.pop( - client.client_id - ) - client.camera.position = cam_position - client.camera.look_at = cam_look_at - client.camera.up_direction = cam_up - client.flush() + with server.atomic(): + for client in server.get_clients().values(): + if client.client_id not in camera_pose_backup_from_id: + continue + cam_position, cam_look_at, cam_up = camera_pose_backup_from_id.pop( + client.client_id + ) + client.camera.position = cam_position + client.camera.look_at = cam_look_at + client.camera.up_direction = cam_up + client.flush() # Un-hide scene nodes. server.scene.set_global_visibility(True) @@ -1119,7 +1122,7 @@ def play() -> None: play_thread = threading.Thread(target=play) play_thread.start() play_thread.join() - dump_video_button.disabled = False + dump_video_button.disabled = not preview_save_camera_path_button.visible # Play the camera trajectory when the play button is pressed. @pause_button.on_click @@ -1203,7 +1206,7 @@ def _(_) -> None: # visualize the camera path server.scene.set_global_visibility(True) - cancel_button = event.client.gui.add_button("Cancel") + cancel_button = event.client.gui.add_button("Cancel", color="gray") @cancel_button.on_click def _(_) -> None: @@ -1212,192 +1215,216 @@ def _(_) -> None: @save_camera_path_button.on_click def _(event: viser.GuiEvent) -> None: assert event.client is not None - num_frames = int(framerate_number.value * duration_number.value) - json_data = {} - # json data has the properties: - # keyframes: list of keyframes with - # matrix : flattened 4x4 matrix - # fov: float in degrees - # aspect: float - # render_height: int - # render_width: int - # fps: int - # seconds: float - # is_cycle: bool - # smoothness_value: float - # camera_path: list of frames with properties - # camera_to_world: flattened 4x4 matrix - # fov: float in degrees - # aspect: float - # first populate the keyframes: - keyframes = [] - for keyframe, dummy in camera_path._keyframes.values(): - pose = tf.SE3.from_rotation_and_translation( - tf.SO3(keyframe.wxyz) @ tf.SO3.from_x_radians(np.pi), - keyframe.position / scale_ratio, - ) - keyframe_dict = { - "matrix": pose.as_matrix().flatten().tolist(), - "fov": ( - np.rad2deg(keyframe.override_fov_rad) - if keyframe.override_fov_enabled - else fov_degrees_slider.value - ), - "aspect": keyframe.aspect, - "override_transition_enabled": keyframe.override_transition_enabled, - "override_transition_sec": keyframe.override_transition_sec, - } - keyframes.append(keyframe_dict) - json_data["default_fov"] = fov_degrees_slider.value - json_data["default_transition_sec"] = transition_sec_number.value - json_data["keyframes"] = keyframes - json_data["render_height"] = render_res_vec2.value[1] - json_data["render_width"] = render_res_vec2.value[0] - json_data["fps"] = framerate_number.value - json_data["seconds"] = duration_number.value - json_data["is_cycle"] = loop_checkbox.value - json_data["smoothness_value"] = tension_slider.value - # now populate the camera path: - camera_path_list = [] - for i in range(num_frames): - maybe_pose_and_fov = camera_path.interpolate_pose_and_fov_rad( - i / num_frames - ) - if maybe_pose_and_fov is None: - return - time = None - if len(maybe_pose_and_fov) == 3: # Time is enabled. - pose, fov, time = maybe_pose_and_fov - else: - pose, fov = maybe_pose_and_fov - # rotate the axis of the camera 180 about x axis - pose = tf.SE3.from_rotation_and_translation( - pose.rotation() @ tf.SO3.from_x_radians(np.pi), - pose.translation() / scale_ratio, - ) - camera_path_list_dict = { - "camera_to_world": pose.as_matrix().flatten().tolist(), - "fov": np.rad2deg(fov), - "aspect": render_res_vec2.value[0] / render_res_vec2.value[1], - } - if time is not None: - camera_path_list_dict["render_time"] = time - camera_path_list.append(camera_path_list_dict) - json_data["camera_path"] = camera_path_list - # finally add crop data if crop is enabled - # if control_panel is not None: - # if control_panel.crop_viewport: - # obb = control_panel.crop_obb - # rpy = tf.SO3.from_matrix(obb.R.numpy()).as_rpy_radians() - # color = control_panel.background_color - # json_data["crop"] = { - # "crop_center": obb.T.tolist(), - # "crop_scale": obb.S.tolist(), - # "crop_rot": [rpy.roll, rpy.pitch, rpy.yaw], - # "crop_bg_color": {"r": color[0], "g": color[1], "b": color[2]}, - # } - - # now write the json file - try: - json_outfile = ( - output_dir / "camera_paths" / f"{trajectory_name_text.value}.json" - ) - json_outfile.parent.mkdir(parents=True, exist_ok=True) - except Exception: - Console(width=120).print( - "[bold yellow]Warning: Failed to write the camera path to the data directory. Saving to the output directory instead." - ) - json_outfile = ( - output_dir / "camera_paths" / f"{trajectory_name_text.value}.json" - ) + + json_outfile = ( + output_dir / "camera_paths" / f"{trajectory_name_text.value}.json" + ) + + def save_camera_path() -> None: json_outfile.parent.mkdir(parents=True, exist_ok=True) - with open(json_outfile.absolute(), "w") as outfile: - json.dump(json_data, outfile) - print(f"Camera path saved to {json_outfile.absolute()}") + + num_frames = int(framerate_number.value * duration_number.value) + json_data = {} + # json data has the properties: + # keyframes: list of keyframes with + # matrix : flattened 4x4 matrix + # fov: float in degrees + # aspect: float + # render_height: int + # render_width: int + # fps: int + # seconds: float + # is_cycle: bool + # smoothness_value: float + # camera_path: list of frames with properties + # camera_to_world: flattened 4x4 matrix + # fov: float in degrees + # aspect: float + # first populate the keyframes: + keyframes = [] + for keyframe, dummy in camera_path._keyframes.values(): + pose = tf.SE3.from_rotation_and_translation( + tf.SO3(keyframe.wxyz) @ tf.SO3.from_x_radians(np.pi), + keyframe.position / scale_ratio, + ) + keyframe_dict = { + "matrix": pose.as_matrix().flatten().tolist(), + "fov": ( + np.rad2deg(keyframe.override_fov_rad) + if keyframe.override_fov_enabled + else fov_degrees_slider.value + ), + "aspect": keyframe.aspect, + "override_transition_enabled": keyframe.override_transition_enabled, + "override_transition_sec": keyframe.override_transition_sec, + } + keyframes.append(keyframe_dict) + json_data["default_fov"] = fov_degrees_slider.value + json_data["default_transition_sec"] = transition_sec_number.value + json_data["keyframes"] = keyframes + json_data["render_height"] = render_res_vec2.value[1] + json_data["render_width"] = render_res_vec2.value[0] + json_data["fps"] = framerate_number.value + json_data["seconds"] = duration_number.value + json_data["is_cycle"] = loop_checkbox.value + json_data["smoothness_value"] = tension_slider.value + # now populate the camera path: + camera_path_list = [] + for i in range(num_frames): + maybe_pose_and_fov = camera_path.interpolate_pose_and_fov_rad( + i / num_frames + ) + if maybe_pose_and_fov is None: + return + time = None + if len(maybe_pose_and_fov) == 3: # Time is enabled. + pose, fov, time = maybe_pose_and_fov + else: + pose, fov = maybe_pose_and_fov + # rotate the axis of the camera 180 about x axis + pose = tf.SE3.from_rotation_and_translation( + pose.rotation() @ tf.SO3.from_x_radians(np.pi), + pose.translation() / scale_ratio, + ) + camera_path_list_dict = { + "camera_to_world": pose.as_matrix().flatten().tolist(), + "fov": np.rad2deg(fov), + "aspect": render_res_vec2.value[0] / render_res_vec2.value[1], + } + if time is not None: + camera_path_list_dict["render_time"] = time + camera_path_list.append(camera_path_list_dict) + json_data["camera_path"] = camera_path_list + + with open(json_outfile.absolute(), "w") as outfile: + json.dump(json_data, outfile) + print(f"Camera path saved to {json_outfile.absolute()}") + + if json_outfile.exists(): + with event.client.gui.add_modal("Save Path") as modal: + event.client.gui.add_markdown( + "Path already exists. Do you want to overwrite?" + ) + overwrite_button = event.client.gui.add_button("Overwrite") + cancel_button = event.client.gui.add_button("Cancel", color="gray") + + @overwrite_button.on_click + def _(_) -> None: + modal.close() + save_camera_path() + + @cancel_button.on_click + def _(_) -> None: + modal.close() + + else: + save_camera_path() @dump_video_button.on_click def _(event: viser.GuiEvent) -> None: client = event.client assert client is not None - # enter into preview render mode - render_tab_state.preview_render = True - maybe_pose_and_fov_rad = compute_and_update_preview_camera_state() - if maybe_pose_and_fov_rad is None: - remove_preview_camera() - return - if len(maybe_pose_and_fov_rad) == 3: # Time is enabled. - pose, fov, time = maybe_pose_and_fov_rad - else: - pose, fov = maybe_pose_and_fov_rad - del fov + video_outfile = output_dir / "videos" / f"traj_{trajectory_name_text.value}.mp4" - # Hide all scene nodes when we're previewing the render. - server.scene.set_global_visibility(False) + def dump_video() -> None: + # enter into preview render mode + render_tab_state.preview_render = True + maybe_pose_and_fov_rad = compute_and_update_preview_camera_state() + if maybe_pose_and_fov_rad is None: + remove_preview_camera() + return + if len(maybe_pose_and_fov_rad) == 3: # Time is enabled. + pose, fov, time = maybe_pose_and_fov_rad + else: + pose, fov = maybe_pose_and_fov_rad + del fov - # Back up and then set camera poses. - for client in server.get_clients().values(): - camera_pose_backup_from_id[client.client_id] = ( - client.camera.position, - client.camera.look_at, - client.camera.up_direction, - ) - client.camera.wxyz = pose.rotation().wxyz - client.camera.position = pose.translation() - - # disable all the trajectory control widgets - handles_to_disable = list(handles.values()) + list(extra_handles.values()) - original_disabled = [handle.disabled for handle in handles_to_disable] - for handle in handles_to_disable: - handle.disabled = True - - def dump() -> None: - os.makedirs(output_dir / "videos", exist_ok=True) - writer = imageio.get_writer( - f"{output_dir}/videos/traj_{trajectory_name_text.value}.mp4", - fps=framerate_number.value, - ) - max_frame = int(framerate_number.value * duration_number.value) - assert max_frame > 0 and preview_frame_slider is not None - preview_frame_slider.value = 0 - for _ in range(max_frame): - preview_frame_slider.value = ( - preview_frame_slider.value + 1 - ) % max_frame - # should we use get_render here? - image = client.camera.get_render( - height=render_res_vec2.value[1], - width=render_res_vec2.value[0], - ) - writer.append_data(image) - writer.close() - print(f"Video saved to videos/traj_{trajectory_name_text.value}.mp4") + # Hide all scene nodes when we're previewing the render. + server.scene.set_global_visibility(False) - dump_thread = threading.Thread(target=dump) - dump_thread.start() - dump_thread.join() + # Back up and then set camera poses. + with server.atomic(): + for client in server.get_clients().values(): + camera_pose_backup_from_id[client.client_id] = ( + client.camera.position, + client.camera.look_at, + client.camera.up_direction, + ) + client.camera.wxyz = pose.rotation().wxyz + client.camera.position = pose.translation() - # restore the original disabled state - for handle, original_disabled in zip(handles_to_disable, original_disabled): - handle.disabled = original_disabled + # disable all the trajectory control widgets + handles_to_disable = list(handles.values()) + list(extra_handles.values()) + original_disabled = [handle.disabled for handle in handles_to_disable] + for handle in handles_to_disable: + handle.disabled = True - # exit preview render mode - render_tab_state.preview_render = False + def dump() -> None: + os.makedirs(output_dir / "videos", exist_ok=True) + writer = imageio.get_writer(video_outfile, fps=framerate_number.value) + max_frame = int(framerate_number.value * duration_number.value) + assert max_frame > 0 and preview_frame_slider is not None + preview_frame_slider.value = 0 + for _ in range(max_frame): + preview_frame_slider.value = ( + preview_frame_slider.value + 1 + ) % max_frame + # should we use get_render here? + image = client.camera.get_render( + height=render_res_vec2.value[1], + width=render_res_vec2.value[0], + ) + writer.append_data(image) + writer.close() + print(f"Video saved to {video_outfile}") - # Revert camera poses. - for client in server.get_clients().values(): - if client.client_id not in camera_pose_backup_from_id: - continue - cam_position, cam_look_at, cam_up = camera_pose_backup_from_id.pop( - client.client_id - ) - client.camera.position = cam_position - client.camera.look_at = cam_look_at - client.camera.up_direction = cam_up - client.flush() + dump_thread = threading.Thread(target=dump) + dump_thread.start() + dump_thread.join() - # Un-hide scene nodes. - server.scene.set_global_visibility(True) + # restore the original disabled state + for handle, original_disabled in zip(handles_to_disable, original_disabled): + handle.disabled = original_disabled + + # exit preview render mode + render_tab_state.preview_render = False + + # Revert camera poses. + with server.atomic(): + for client in server.get_clients().values(): + if client.client_id not in camera_pose_backup_from_id: + continue + cam_position, cam_look_at, cam_up = camera_pose_backup_from_id.pop( + client.client_id + ) + client.camera.position = cam_position + client.camera.look_at = cam_look_at + client.camera.up_direction = cam_up + client.flush() + + # Un-hide scene nodes. + server.scene.set_global_visibility(True) + + if video_outfile.exists(): + with event.client.gui.add_modal("Dump Video") as modal: + event.client.gui.add_markdown( + "Video already exists. Do you want to overwrite?" + ) + overwrite_button = event.client.gui.add_button("Overwrite") + cancel_button = event.client.gui.add_button("Cancel", color="gray") + + @overwrite_button.on_click + def _(_) -> None: + modal.close() + dump_video() + + @cancel_button.on_click + def _(_) -> None: + modal.close() + + else: + dump_video() camera_path = CameraPath(server, duration_number) camera_path.tension = tension_slider.value diff --git a/nerfview/version.py b/nerfview/version.py index b3f4756..ae73625 100644 --- a/nerfview/version.py +++ b/nerfview/version.py @@ -1 +1 @@ -__version__ = "0.1.2" +__version__ = "0.1.3" diff --git a/pyproject.toml b/pyproject.toml index 4fa1b90..485682f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "nerfview" -version = "0.1.2" +version = "0.1.3" description = "Interactive NeRF rendering web viewer" readme = "README.md" license = { text = "MIT" }