fix Studio reset, 3D base colour and arrow-trigger defects

The tracker reset was decided by which panel was on screen rather than by the
session, the automatic colour range moved the 3D surface's base colour on the first
press, and the 2D canvas and the 3D overlay applied different flow thresholds (622
arrows against 432 on a real recording). ui_check 600/0; apps/studio 380 items and
shared 390 items with no failures; real-GL 18 items, only the pre-existing
colour-scale failure; AutoCal 358 passed; both views now select identical arrows on
all 42 frames of a real video.
This commit is contained in:
ykcheungab
2026-10-01 14:40:33 +08:00
parent 33a3d8c9c2
commit e0156a14c6
14 changed files with 446 additions and 72 deletions
@@ -254,6 +254,7 @@ def draw_arrow_display(
flow_off_px: float | None = None,
flow_grid_spacing: int | None = None,
max_arrows: int | None = None,
policy=None,
) -> np.ndarray:
"""Draw flow arrows on a solid canvas or over the processed frame.
@@ -267,8 +268,11 @@ def draw_arrow_display(
:func:`select_flow_samples`: the arrows stay this frame's own measurement, so
latching the boundary cannot put an arrow on a sample the device does not vouch for.
Finger style uses the shared :mod:`visual_policy` sampler so 2D and 3D arrows
share the same UV sample points and thresholds. Box style keeps the SDK path.
``policy`` is the **shared** :class:`~orisys_app_shared.scene3d.visual_policy.VisualPolicy`
the 3D surface also draws its overlay from (``visual_policy.policy_for_settings``).
Passing it is what makes the two figures trigger at the same strength; the
finger-style defaults below are only the fallback for a caller that has no pane
(a tool, a test). Box style keeps the SDK path either way.
"""
base = _ensure_bgr(processed_frame)
if flow is None:
@@ -309,18 +313,22 @@ def draw_arrow_display(
canvas = base
if finger:
policy = _policy_from_style(
draw_style,
flow_on_px=flow_on_px,
flow_off_px=flow_off_px,
flow_grid_spacing=flow_grid_spacing,
max_arrows=max_arrows,
selected = (
policy
if policy is not None
else _policy_from_style(
draw_style,
flow_on_px=flow_on_px,
flow_off_px=flow_off_px,
flow_grid_spacing=flow_grid_spacing,
max_arrows=max_arrows,
)
)
return _paint_shared_flow_arrows(
canvas,
np.asarray(flow),
valid_mask=valid_mask,
policy=policy,
policy=selected,
colormap=colormap,
below_threshold_color=tuple(draw_kw["below_threshold_color"]),
)
@@ -285,6 +285,7 @@ def render_image(
depth_range: tuple[float, float] | None = None,
canvas_bgr: Sequence[int] | None = None,
domain_mask: np.ndarray | None = None,
arrow_policy=None,
) -> np.ndarray | None:
"""Draw ``frame`` in ``mode``; ``None`` when there is nothing to draw.
@@ -298,6 +299,11 @@ def render_image(
latch to should do. It never gates the arrows or the depth values: those stay this
frame's own data, so a sample the device does not vouch for is still not drawn just
because the boundary around it was latched.
``arrow_policy`` is the shared flow-arrow policy (``visual_policy.policy_for_settings``
over the pane's display settings): the same object the 3D surface samples its overlay
with, so both figures trigger at the same strength. ``None`` means the draw style's
own defaults.
"""
plan = plan or plan_image(frame)
wanted = plan.resolve(mode)
@@ -328,6 +334,7 @@ def render_image(
valid_mask=frame.valid_mask,
domain_mask=domain_mask,
colormap=flow_colormap(theme),
policy=arrow_policy,
)
# ``recorded``: the frame is the picture, with nothing drawn over it.
return ensure_bgr(np.asarray(frame.image_bgr))
@@ -57,7 +57,10 @@ from orisys_app_shared.scene3d.sampling import (
check_field_domain,
sample_vertex_displacement,
)
from orisys_app_shared.scene3d.visual_policy import VisualPolicy, select_flow_samples
from orisys_app_shared.scene3d.visual_policy import (
policy_for_settings,
select_flow_samples,
)
#: Reported when a display gain is applied to a displacement that is only
#: approximate. Informational: the picture is legitimate as an illustration, and
@@ -89,8 +92,6 @@ class SceneComposer:
#: draw, the picture is the asset's own geometry, and recomputing its normals
#: every frame was pure cost (measured as the bulk of a 20k-vertex compose).
self._rest_geometry: dict[str, tuple[np.ndarray, np.ndarray]] = {}
#: Last UV grid of "arrow was on" samples for flow hysteresis.
self._flow_previously_on: np.ndarray | None = None
#: The resolved ``#RRGGBB,...`` ramp both views build their lookup table from.
#: Set by the view when the theme changes; empty means this build's default.
self.ramp = str(ramp)
@@ -252,12 +253,7 @@ class SceneComposer:
)
return flow_arrows, contact_arrow
policy = VisualPolicy(
flow_on_px=float(settings.flow_on_px),
flow_off_px=float(settings.flow_off_px),
flow_grid_spacing=int(settings.flow_grid_spacing),
max_arrows=int(settings.max_arrows),
).normalised()
policy = policy_for_settings(settings)
if (
settings.flow_arrows_enabled
and frame.flow_uv is not None
@@ -265,20 +261,17 @@ class SceneComposer:
):
try:
flow = np.asarray(frame.flow_uv)
h, w = int(flow.shape[0]), int(flow.shape[1])
previously_on = self._flow_previously_on
if previously_on is not None and previously_on.shape != (h, w):
previously_on = None
# Selection is a pure function of (field, mask, policy): the 2D canvas
# runs the same one on the same frame, so the two figures show the same
# arrows. Hysteresis used to live only here, which made this figure
# trigger at ``flow_off_px`` (0.3 px) where the canvas needed
# ``flow_on_px`` (0.4 px) — measured on a real recording as 622 arrows
# here against 432 there, and a difference on 34 of 41 frames.
samples = select_flow_samples(
flow,
frame.flow_valid_mask,
policy,
previously_on=previously_on,
)
on_mask = np.zeros((h, w), dtype=bool)
if samples.rows.size:
on_mask[samples.rows, samples.cols] = True
self._flow_previously_on = on_mask
flow_arrows, extra = build_flow_overlay(
samples,
mapping,
@@ -294,9 +287,6 @@ class SceneComposer:
)
)
flow_arrows = None
self._flow_previously_on = None
else:
self._flow_previously_on = None
if settings.contact_arrow_enabled and frame.contact_valid:
contact_arrow, extra = build_normal_overlay(
@@ -13,6 +13,7 @@ import numpy as np
from orisys_app_shared.scene3d.contracts import (
Diagnostic,
DisplaySettings,
Mask,
diagnostic,
error,
@@ -58,6 +59,32 @@ class VisualPolicy:
)
def policy_for_settings(settings: DisplaySettings) -> VisualPolicy:
"""The **one** flow-arrow policy both views use, from the display settings.
The 2D canvas and the 3D surface draw the same measurement, so they have to
trigger at the same strength: "arrows in one figure and not the other" was a
difference in the policy each path built for itself, not a difference in the
data. One mapping, from one settings object, is what removes it — the pane's
``DisplaySettings`` is where the thresholds and the sample grid are chosen (and
persisted), so neither view is free to invent its own.
``flow_off_px`` travels with the policy but neither view applies hysteresis today.
A band only one of the two views honoured was the mismatch itself (measured on a
real recording: 622 arrows in the 3D overlay against 432 in the canvas, differing
on 34 of 41 frames), and the band was never an operator-facing control. It stays in
the value so a *shared* stateful selection can use it later; a state owned by one
view cannot be shared with the other anyway — they run on different threads at
different rates, which is what made the two selections diverge in the first place.
"""
return VisualPolicy(
flow_on_px=float(settings.flow_on_px),
flow_off_px=float(settings.flow_off_px),
flow_grid_spacing=int(settings.flow_grid_spacing),
max_arrows=int(settings.max_arrows),
).normalised()
@dataclass(frozen=True)
class FlowSamples:
"""Shared UV sample points for 2D and 3D flow arrows."""