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:
@@ -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."""
|
||||
|
||||
@@ -110,6 +110,44 @@ def test_a_mode_the_plan_does_not_offer_falls_back_to_the_default():
|
||||
|
||||
|
||||
# --------------------------------------------------------------- one renderer
|
||||
def test_the_2d_canvas_draws_from_the_policy_it_is_given():
|
||||
"""The threshold is the caller's — the pane's — not a per-view default.
|
||||
|
||||
``arrow_policy`` is the policy the 3D surface samples its overlay with
|
||||
(``visual_policy.policy_for_settings`` over the display settings). A sample inside
|
||||
the hysteresis band is the case that used to differ between the two figures, so it
|
||||
is the case pinned here: with a 0.4 px threshold nothing is drawn, with 0.3 px the
|
||||
same frame carries arrows.
|
||||
"""
|
||||
from orisys_app_shared.scene3d.contracts import DisplaySettings
|
||||
from orisys_app_shared.scene3d.visual_policy import policy_for_settings
|
||||
|
||||
h, w = 12, 16
|
||||
flow = np.zeros((h, w, 2), dtype=np.float32)
|
||||
flow[..., 0] = 0.175 # |2*flow| = 0.35 px: above flow_off_px, below flow_on_px
|
||||
frame = backend.ImageFrame(_image(h, w), flow=flow, flow_valid=True)
|
||||
base = np.zeros((h, w, 3), dtype=np.uint8)
|
||||
|
||||
strict = policy_for_settings(DisplaySettings(flow_on_px=0.4, flow_off_px=0.3))
|
||||
loose = policy_for_settings(DisplaySettings(flow_on_px=0.3, flow_off_px=0.2))
|
||||
strict_image = backend.render_image(
|
||||
frame, mode="arrows_pure", draw_style="finger", arrow_policy=strict
|
||||
)
|
||||
loose_image = backend.render_image(
|
||||
frame, mode="arrows_pure", draw_style="finger", arrow_policy=loose
|
||||
)
|
||||
assert len(np.unique(strict_image.reshape(-1, 3), axis=0)) == 1, (
|
||||
"a 0.4 px threshold drew something for a 0.35 px sample"
|
||||
)
|
||||
assert len(np.unique(loose_image.reshape(-1, 3), axis=0)) > 1, (
|
||||
"a 0.3 px threshold drew nothing for a 0.35 px sample"
|
||||
)
|
||||
# The same policy governs the primitive directly, which is what the window calls
|
||||
# through the backend.
|
||||
direct = draw_arrow_display(base, flow, draw_style="finger", policy=strict)
|
||||
assert len(np.unique(direct.reshape(-1, 3), axis=0)) == 1
|
||||
|
||||
|
||||
def test_the_backend_renders_what_the_primitives_rendered_before():
|
||||
"""Live went through these two calls; the backend must not change a pixel."""
|
||||
image = _image()
|
||||
|
||||
Reference in New Issue
Block a user