update exe GUI build
This commit is contained in:
+102
-4
@@ -3,7 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Literal
|
||||
from typing import Callable, Literal
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
@@ -396,21 +396,32 @@ class FlowArrowOverlay:
|
||||
|
||||
self.mesh = mesh
|
||||
self.style = style or DEFAULT_FLOW_ARROW_STYLE
|
||||
self._enabled = bool(self.style.enabled)
|
||||
self._mesh_span = _mesh_span(mesh.rest_vertices)
|
||||
_cylinder_template(o3d)
|
||||
_cone_template(o3d)
|
||||
self.arrow_mesh = create_flow_mesh_geometry(o3d) if self.style.enabled else None
|
||||
self.arrow_mesh = create_flow_mesh_geometry(o3d) if self._enabled else None
|
||||
|
||||
@property
|
||||
def enabled(self) -> bool:
|
||||
return self.style.enabled
|
||||
return self._enabled
|
||||
|
||||
def set_enabled(self, enabled: bool) -> None:
|
||||
enabled = bool(enabled)
|
||||
if enabled == self._enabled:
|
||||
return
|
||||
self._enabled = enabled
|
||||
if enabled and self.arrow_mesh is None:
|
||||
import open3d as o3d
|
||||
|
||||
self.arrow_mesh = create_flow_mesh_geometry(o3d)
|
||||
|
||||
def compute(
|
||||
self,
|
||||
flow,
|
||||
valid_mask: np.ndarray | None = None,
|
||||
) -> tuple[np.ndarray, np.ndarray, np.ndarray] | None:
|
||||
if not self.style.enabled or self.arrow_mesh is None:
|
||||
if not self._enabled or self.arrow_mesh is None:
|
||||
return None
|
||||
|
||||
return build_flow_arrow_mesh(
|
||||
@@ -429,3 +440,90 @@ class FlowArrowOverlay:
|
||||
self.arrow_mesh.triangles = o3d.utility.Vector3iVector(arrow_triangles.astype(np.int32))
|
||||
self.arrow_mesh.vertex_colors = o3d.utility.Vector3dVector(arrow_colors.astype(np.float64))
|
||||
self.arrow_mesh.compute_vertex_normals()
|
||||
|
||||
|
||||
FLOW_ARROWS_GEOMETRY_NAME = "flow_arrows"
|
||||
|
||||
|
||||
@dataclass
|
||||
class FlowArrowFilamentSlot:
|
||||
added: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class FlowArrowHiddenSlot:
|
||||
in_vis: bool = False
|
||||
|
||||
|
||||
def ensure_flow_arrow_in_hidden_vis(
|
||||
overlay: FlowArrowOverlay,
|
||||
vis,
|
||||
slot: FlowArrowHiddenSlot,
|
||||
) -> None:
|
||||
"""Add flow-arrow geometry to a HiddenVisualizer window when enabled at init."""
|
||||
if overlay.enabled and overlay.arrow_mesh is not None and not slot.in_vis:
|
||||
vis.add_geometry(overlay.arrow_mesh)
|
||||
slot.in_vis = True
|
||||
|
||||
|
||||
def update_flow_arrows_filament(
|
||||
overlay: FlowArrowOverlay,
|
||||
flow,
|
||||
valid_mask,
|
||||
*,
|
||||
scene,
|
||||
material,
|
||||
slot: FlowArrowFilamentSlot,
|
||||
mesh_to_o3d: Callable,
|
||||
) -> None:
|
||||
"""Sync flow-arrow TriangleMesh geometry to a Filament offscreen scene."""
|
||||
flow_result = overlay.compute(flow, valid_mask=valid_mask)
|
||||
if not overlay.enabled:
|
||||
if slot.added:
|
||||
scene.remove_geometry(FLOW_ARROWS_GEOMETRY_NAME)
|
||||
slot.added = False
|
||||
return
|
||||
if flow_result is None or overlay.arrow_mesh is None:
|
||||
return
|
||||
|
||||
flow_vertices, flow_triangles, flow_colors = flow_result
|
||||
if len(flow_vertices) > 0:
|
||||
flow_mesh = mesh_to_o3d(flow_vertices, flow_triangles, flow_colors)
|
||||
if not slot.added:
|
||||
scene.add_geometry(FLOW_ARROWS_GEOMETRY_NAME, flow_mesh, material)
|
||||
slot.added = True
|
||||
else:
|
||||
scene.remove_geometry(FLOW_ARROWS_GEOMETRY_NAME)
|
||||
scene.add_geometry(FLOW_ARROWS_GEOMETRY_NAME, flow_mesh, material)
|
||||
elif slot.added:
|
||||
scene.remove_geometry(FLOW_ARROWS_GEOMETRY_NAME)
|
||||
slot.added = False
|
||||
|
||||
|
||||
def update_flow_arrows_hidden(
|
||||
overlay: FlowArrowOverlay,
|
||||
flow,
|
||||
valid_mask,
|
||||
*,
|
||||
vis,
|
||||
slot: FlowArrowHiddenSlot,
|
||||
) -> None:
|
||||
"""Sync flow-arrow TriangleMesh geometry to a HiddenVisualizer window."""
|
||||
flow_result = overlay.compute(flow, valid_mask=valid_mask)
|
||||
if not overlay.enabled:
|
||||
if slot.in_vis and overlay.arrow_mesh is not None:
|
||||
empty = np.empty((0, 3), dtype=np.float64)
|
||||
empty_i = np.empty((0, 3), dtype=np.int32)
|
||||
overlay.apply(empty, empty_i, empty)
|
||||
vis.update_geometry(overlay.arrow_mesh)
|
||||
return
|
||||
if overlay.arrow_mesh is None:
|
||||
return
|
||||
if not slot.in_vis:
|
||||
vis.add_geometry(overlay.arrow_mesh)
|
||||
slot.in_vis = True
|
||||
if flow_result is not None:
|
||||
flow_vertices, flow_triangles, flow_colors = flow_result
|
||||
overlay.apply(flow_vertices, flow_triangles, flow_colors)
|
||||
vis.update_geometry(overlay.arrow_mesh)
|
||||
|
||||
|
||||
@@ -38,6 +38,12 @@ def resolve_viz_pose_path(path: str | Path | None = None) -> Path:
|
||||
Path.cwd() / raw,
|
||||
here.parents[2] / raw,
|
||||
]
|
||||
try:
|
||||
from app_paths import app_root
|
||||
|
||||
candidates.insert(0, app_root() / raw)
|
||||
except ImportError:
|
||||
pass
|
||||
for candidate in candidates:
|
||||
if candidate.is_file():
|
||||
return candidate.resolve()
|
||||
@@ -74,6 +80,79 @@ def camera_look_at_from_world_to_cam(
|
||||
return lookat, eye, up
|
||||
|
||||
|
||||
def default_look_at_view(
|
||||
mesh_vertices: np.ndarray,
|
||||
lookat: np.ndarray,
|
||||
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
||||
"""Fallback look_at triple when no world_to_cam pose is available."""
|
||||
center = np.asarray(lookat, dtype=np.float64).reshape(3)
|
||||
extent = np.ptp(np.asarray(mesh_vertices, dtype=np.float64), axis=0)
|
||||
span = float(np.max(extent))
|
||||
eye = center + np.array([0.0, 0.0, span * 2.0], dtype=np.float64)
|
||||
up = np.array([0.0, 1.0, 0.0], dtype=np.float64)
|
||||
return center, eye, up
|
||||
|
||||
|
||||
def _rotation_matrix(axis: np.ndarray, angle_rad: float) -> np.ndarray:
|
||||
axis = np.asarray(axis, dtype=np.float64).reshape(3)
|
||||
norm = float(np.linalg.norm(axis))
|
||||
if norm < 1e-12:
|
||||
return np.eye(3, dtype=np.float64)
|
||||
axis = axis / norm
|
||||
x, y, z = axis
|
||||
cross = np.array(
|
||||
[[0.0, -z, y], [z, 0.0, -x], [-y, x, 0.0]],
|
||||
dtype=np.float64,
|
||||
)
|
||||
return np.eye(3, dtype=np.float64) + np.sin(angle_rad) * cross + (1.0 - np.cos(angle_rad)) * (cross @ cross)
|
||||
|
||||
|
||||
def orbit_eye_about_target(
|
||||
target: np.ndarray,
|
||||
eye: np.ndarray,
|
||||
up: np.ndarray,
|
||||
dx_deg: float,
|
||||
dy_deg: float,
|
||||
) -> tuple[np.ndarray, np.ndarray]:
|
||||
"""Orbit ``eye`` around ``target``; dx/dy are drag deltas in degrees."""
|
||||
target = np.asarray(target, dtype=np.float64).reshape(3)
|
||||
eye = np.asarray(eye, dtype=np.float64).reshape(3)
|
||||
up = np.asarray(up, dtype=np.float64).reshape(3)
|
||||
|
||||
offset = eye - target
|
||||
if float(np.linalg.norm(offset)) < 1e-9:
|
||||
offset = np.array([0.0, 0.0, 1.0], dtype=np.float64)
|
||||
|
||||
world_up = up.copy()
|
||||
if float(np.linalg.norm(world_up)) < 1e-9:
|
||||
world_up = np.array([0.0, 1.0, 0.0], dtype=np.float64)
|
||||
world_up /= np.linalg.norm(world_up)
|
||||
|
||||
if abs(dx_deg) > 1e-9:
|
||||
rot = _rotation_matrix(world_up, np.deg2rad(dx_deg))
|
||||
offset = rot @ offset
|
||||
up = rot @ up
|
||||
|
||||
forward = -offset
|
||||
forward_norm = float(np.linalg.norm(forward))
|
||||
if forward_norm > 1e-9:
|
||||
forward = forward / forward_norm
|
||||
right = np.cross(forward, up)
|
||||
right_norm = float(np.linalg.norm(right))
|
||||
if right_norm > 1e-9:
|
||||
right = right / right_norm
|
||||
if abs(dy_deg) > 1e-9:
|
||||
rot = _rotation_matrix(right, np.deg2rad(dy_deg))
|
||||
offset = rot @ offset
|
||||
up = rot @ up
|
||||
|
||||
new_eye = target + offset
|
||||
up_norm = float(np.linalg.norm(up))
|
||||
if up_norm > 1e-9:
|
||||
up = up / up_norm
|
||||
return new_eye, up
|
||||
|
||||
|
||||
def apply_view_from_world_to_cam(vis, world_to_cam: np.ndarray, *, lookat: np.ndarray) -> None:
|
||||
"""Apply Blender-exported world_to_cam as the Open3D default view."""
|
||||
world_to_cam = np.asarray(world_to_cam, dtype=np.float64)
|
||||
|
||||
+105
-35
@@ -10,12 +10,21 @@ from orisys.finger.mesh import UVSurfaceMesh
|
||||
from orisys.finger.viewer import flow_magnitude_vertex_colors, scalar_map_vertex_colors
|
||||
|
||||
from .contact_arrow import ContactArrowOverlay
|
||||
from .flow_arrow import FlowArrowOverlay
|
||||
from .flow_arrow import (
|
||||
FlowArrowFilamentSlot,
|
||||
FlowArrowHiddenSlot,
|
||||
FlowArrowOverlay,
|
||||
ensure_flow_arrow_in_hidden_vis,
|
||||
update_flow_arrows_filament,
|
||||
update_flow_arrows_hidden,
|
||||
)
|
||||
from .mesh_camera import (
|
||||
apply_filament_sun_light,
|
||||
apply_shade_factors,
|
||||
apply_view_from_world_to_cam,
|
||||
camera_look_at_from_world_to_cam,
|
||||
default_look_at_view,
|
||||
orbit_eye_about_target,
|
||||
precompute_shade_factors,
|
||||
)
|
||||
|
||||
@@ -132,19 +141,35 @@ class _FilamentOffscreenBackend:
|
||||
self._material = _make_lit_material(o3d)
|
||||
self._mesh_added = False
|
||||
self._arrow_added = False
|
||||
self._flow_arrow_added = False
|
||||
self._flow_arrow_slot = FlowArrowFilamentSlot()
|
||||
self._reset_view_state()
|
||||
self._apply_camera()
|
||||
|
||||
def _apply_camera(self) -> None:
|
||||
def _reset_view_state(self) -> None:
|
||||
if self.world_to_cam is not None:
|
||||
center, eye, up = camera_look_at_from_world_to_cam(self.world_to_cam, lookat=self.lookat)
|
||||
else:
|
||||
center = self.lookat.astype(np.float64)
|
||||
extent = np.ptp(self.mesh.rest_vertices, axis=0)
|
||||
span = float(np.max(extent))
|
||||
eye = center + np.array([0.0, 0.0, span * 2.0], dtype=np.float64)
|
||||
up = np.array([0.0, 1.0, 0.0], dtype=np.float64)
|
||||
self._renderer.scene.camera.look_at(center, eye, up)
|
||||
center, eye, up = default_look_at_view(self.mesh.rest_vertices, self.lookat)
|
||||
self._view_center = center
|
||||
self._view_eye = eye
|
||||
self._view_up = up
|
||||
|
||||
def _apply_camera(self) -> None:
|
||||
self._renderer.scene.camera.look_at(self._view_center, self._view_eye, self._view_up)
|
||||
|
||||
def rotate_view(self, dx: float, dy: float) -> None:
|
||||
self._view_eye, self._view_up = orbit_eye_about_target(
|
||||
self._view_center,
|
||||
self._view_eye,
|
||||
self._view_up,
|
||||
dx,
|
||||
dy,
|
||||
)
|
||||
self._apply_camera()
|
||||
|
||||
def reset_view(self) -> None:
|
||||
self._reset_view_state()
|
||||
self._apply_camera()
|
||||
|
||||
def resize(self, width: int, height: int) -> None:
|
||||
width = max(int(width), 64)
|
||||
@@ -164,7 +189,18 @@ class _FilamentOffscreenBackend:
|
||||
self._material = _make_lit_material(self._o3d)
|
||||
self._mesh_added = False
|
||||
self._arrow_added = False
|
||||
self._flow_arrow_added = False
|
||||
self._flow_arrow_slot = FlowArrowFilamentSlot()
|
||||
center, eye, up = self._view_center, self._view_eye, self._view_up
|
||||
self._renderer = self._o3d.visualization.rendering.OffscreenRenderer(self.width, self.height)
|
||||
self._renderer.scene.set_background(_background_rgba01(self.mesh_background))
|
||||
if self.mesh_lighting is not None:
|
||||
apply_filament_sun_light(
|
||||
self._renderer,
|
||||
direction=self.mesh_lighting.direction,
|
||||
intensity=self.mesh_lighting.intensity,
|
||||
)
|
||||
self._material = _make_lit_material(self._o3d)
|
||||
self._view_center, self._view_eye, self._view_up = center, eye, up
|
||||
self._apply_camera()
|
||||
|
||||
def render(
|
||||
@@ -211,20 +247,15 @@ class _FilamentOffscreenBackend:
|
||||
self._renderer.scene.remove_geometry("contact_arrow")
|
||||
self._arrow_added = False
|
||||
|
||||
flow_result = self._flow_arrow.compute(flow, valid_mask=valid_mask)
|
||||
if flow_result is not None and self._flow_arrow.arrow_mesh is not None:
|
||||
flow_vertices, flow_triangles, flow_colors = flow_result
|
||||
if len(flow_vertices) > 0:
|
||||
flow_mesh = _mesh_to_o3d(o3d, flow_vertices, flow_triangles, flow_colors)
|
||||
if not self._flow_arrow_added:
|
||||
self._renderer.scene.add_geometry("flow_arrows", flow_mesh, self._material)
|
||||
self._flow_arrow_added = True
|
||||
else:
|
||||
self._renderer.scene.remove_geometry("flow_arrows")
|
||||
self._renderer.scene.add_geometry("flow_arrows", flow_mesh, self._material)
|
||||
elif self._flow_arrow_added:
|
||||
self._renderer.scene.remove_geometry("flow_arrows")
|
||||
self._flow_arrow_added = False
|
||||
update_flow_arrows_filament(
|
||||
self._flow_arrow,
|
||||
flow,
|
||||
valid_mask,
|
||||
scene=self._renderer.scene,
|
||||
material=self._material,
|
||||
slot=self._flow_arrow_slot,
|
||||
mesh_to_o3d=lambda v, t, c: _mesh_to_o3d(o3d, v, t, c),
|
||||
)
|
||||
|
||||
return _numpy_o3d_image_to_rgb(self._renderer.render_to_image())
|
||||
|
||||
@@ -280,6 +311,7 @@ class _HiddenVisualizerBackend:
|
||||
self._flow_arrow = FlowArrowOverlay(mesh, style=flow_arrow_style)
|
||||
self.vis = None
|
||||
self.tri_mesh = None
|
||||
self._flow_arrow_slot = FlowArrowHiddenSlot()
|
||||
self._create_window()
|
||||
|
||||
def _create_window(self) -> None:
|
||||
@@ -313,8 +345,7 @@ class _HiddenVisualizerBackend:
|
||||
if self._contact_arrow.arrow_mesh is not None:
|
||||
self.vis.add_geometry(self._contact_arrow.arrow_mesh)
|
||||
|
||||
if self._flow_arrow.enabled and self._flow_arrow.arrow_mesh is not None:
|
||||
self.vis.add_geometry(self._flow_arrow.arrow_mesh)
|
||||
ensure_flow_arrow_in_hidden_vis(self._flow_arrow, self.vis, self._flow_arrow_slot)
|
||||
|
||||
render_option = self.vis.get_render_option()
|
||||
render_option.mesh_show_back_face = True
|
||||
@@ -322,21 +353,44 @@ class _HiddenVisualizerBackend:
|
||||
if hasattr(render_option, "light_on"):
|
||||
render_option.light_on = False
|
||||
|
||||
self._apply_default_view()
|
||||
|
||||
def _apply_default_view(self) -> None:
|
||||
if self.vis is None:
|
||||
return
|
||||
if self.world_to_cam is not None:
|
||||
apply_view_from_world_to_cam(self.vis, self.world_to_cam, lookat=self.lookat)
|
||||
else:
|
||||
view_control = self.vis.get_view_control()
|
||||
center = self.lookat.astype(np.float64)
|
||||
center, eye, up = default_look_at_view(self.mesh.rest_vertices, self.lookat)
|
||||
view_control.set_lookat(center)
|
||||
front = center - eye
|
||||
front_norm = float(np.linalg.norm(front))
|
||||
if front_norm > 1e-9:
|
||||
front = front / front_norm
|
||||
else:
|
||||
front = np.array([0.0, 0.0, -1.0], dtype=np.float64)
|
||||
view_control.set_front(front)
|
||||
view_control.set_up(up)
|
||||
extent = np.ptp(self.mesh.rest_vertices, axis=0)
|
||||
span = float(np.max(extent))
|
||||
view_control.set_lookat(center)
|
||||
view_control.set_front(np.array([0.0, 0.0, -1.0]))
|
||||
view_control.set_up(np.array([0.0, 1.0, 0.0]))
|
||||
view_control.set_zoom(0.5 if span > 0 else 0.7)
|
||||
|
||||
self.vis.poll_events()
|
||||
self.vis.update_renderer()
|
||||
|
||||
def rotate_view(self, dx: float, dy: float) -> None:
|
||||
if self.vis is None:
|
||||
return
|
||||
self.vis.get_view_control().rotate(float(dx), float(dy))
|
||||
self.vis.poll_events()
|
||||
self.vis.update_renderer()
|
||||
|
||||
def reset_view(self) -> None:
|
||||
if self.vis is None:
|
||||
return
|
||||
self._apply_default_view()
|
||||
|
||||
def resize(self, width: int, height: int) -> None:
|
||||
width = max(int(width), 64)
|
||||
height = max(int(height), 64)
|
||||
@@ -377,11 +431,13 @@ class _HiddenVisualizerBackend:
|
||||
self._contact_arrow.apply(arrow_vertices, arrow_colors)
|
||||
self.vis.update_geometry(self._contact_arrow.arrow_mesh)
|
||||
|
||||
flow_result = self._flow_arrow.compute(flow, valid_mask=valid_mask)
|
||||
if flow_result is not None and self._flow_arrow.arrow_mesh is not None:
|
||||
flow_vertices, flow_triangles, flow_colors = flow_result
|
||||
self._flow_arrow.apply(flow_vertices, flow_triangles, flow_colors)
|
||||
self.vis.update_geometry(self._flow_arrow.arrow_mesh)
|
||||
update_flow_arrows_hidden(
|
||||
self._flow_arrow,
|
||||
flow,
|
||||
valid_mask,
|
||||
vis=self.vis,
|
||||
slot=self._flow_arrow_slot,
|
||||
)
|
||||
|
||||
self.vis.poll_events()
|
||||
self.vis.update_renderer()
|
||||
@@ -452,6 +508,20 @@ class OffscreenFingerMeshRenderer:
|
||||
def resize(self, width: int, height: int) -> None:
|
||||
self._backend.resize(width, height)
|
||||
|
||||
def rotate_view(self, dx: float, dy: float) -> None:
|
||||
rotate = getattr(self._backend, "rotate_view", None)
|
||||
if rotate is not None:
|
||||
rotate(dx, dy)
|
||||
|
||||
def reset_view(self) -> None:
|
||||
reset = getattr(self._backend, "reset_view", None)
|
||||
if reset is not None:
|
||||
reset()
|
||||
|
||||
@property
|
||||
def flow_arrow(self) -> FlowArrowOverlay:
|
||||
return self._backend._flow_arrow
|
||||
|
||||
def render(
|
||||
self,
|
||||
*,
|
||||
|
||||
Reference in New Issue
Block a user