Fix replay 2D black canvas and journey UX clarity

Stored analysis overlays now use the decoded frame; latch resets and draw failures surface. Verified with bare-video, replay-wiring, visual_policy, and analysis-progress tests.
This commit is contained in:
ykcheungab
2026-10-06 16:26:47 +08:00
parent a4313f300d
commit 425fa0d44d
13 changed files with 322 additions and 48 deletions
@@ -280,6 +280,18 @@ def magnitude_to_ramp_index(
return np.clip(scaled * 255.0, 0.0, 255.0).astype(np.uint8)
def carry_frame_diagnostic(image: np.ndarray, diagnostic=None) -> np.ndarray:
"""``image`` as a draw result that still carries ``frame_diagnostic``.
Used by :func:`draw_arrow_display` and by callers that must re-wrap after a
transform (orientation) that would otherwise drop the attribute. ``None`` means
the draw succeeded.
"""
out = np.asarray(image).view(_DrawResult)
out.frame_diagnostic = diagnostic
return out
def _flow_result(image: np.ndarray, diagnostic=None) -> np.ndarray:
"""``image`` as the result of :func:`draw_arrow_display`, carrying ``diagnostic``.
@@ -295,9 +307,7 @@ def _flow_result(image: np.ndarray, diagnostic=None) -> np.ndarray:
panel's job is ``code -> localized text``; this module deliberately carries no locale
string (see :data:`ARROW_RENDER_FAILED_MESSAGE`).
"""
out = image.view(_DrawResult)
out.frame_diagnostic = diagnostic
return out
return carry_frame_diagnostic(image, diagnostic)
class _DrawResult(np.ndarray):
@@ -29,6 +29,7 @@ from typing import Any
import numpy as np
from orisys_app_shared.acquisition.arrows import (
carry_frame_diagnostic,
depth_map_to_bgr,
draw_arrow_display,
flow_colormap,
@@ -451,21 +452,25 @@ def render_image(
flow = frame.flow
if sdk_result:
flow = np.nan_to_num(np.asarray(flow, dtype=np.float64), nan=0.0)
return apply_orientation(
draw_arrow_display(
frame.image_bgr,
flow,
background="captured" if wanted == ARROW_STITCHED else "pure_color",
canvas_bgr=canvas_bgr,
draw_style=draw_style,
shared_painter=str(frame.source or "") in RESULT_SOURCES and not sdk_result,
valid_mask=frame.valid_mask,
domain_mask=domain_mask,
colormap=flow_colormap(theme),
policy=arrow_policy,
),
turn,
drawn = draw_arrow_display(
frame.image_bgr,
flow,
background="captured" if wanted == ARROW_STITCHED else "pure_color",
canvas_bgr=canvas_bgr,
draw_style=draw_style,
shared_painter=str(frame.source or "") in RESULT_SOURCES and not sdk_result,
valid_mask=frame.valid_mask,
domain_mask=domain_mask,
colormap=flow_colormap(theme),
policy=arrow_policy,
)
# Orientation may rebuild the array; keep the draw failure on the result so the
# panel can say so instead of showing a silent black canvas.
diagnostic = getattr(drawn, "frame_diagnostic", None)
oriented = apply_orientation(drawn, turn)
if diagnostic is None:
return oriented
return carry_frame_diagnostic(oriented, diagnostic)
# ``recorded``: the frame is the picture, with nothing drawn over it — and the
# camera's own orientation, which no domain convention applies to.
return ensure_bgr(np.asarray(frame.image_bgr))
@@ -248,6 +248,11 @@ def select_flow_samples(
) * margin
dx = dx * scale
dy = dy * scale
# Non-finite tips are discarded by ``valid`` below; zero them before the integer
# cast so numpy does not warn on every sparse recovered frame during replay.
finite_tip = np.isfinite(dx) & np.isfinite(dy)
dx = np.where(finite_tip, dx, 0.0)
dy = np.where(finite_tip, dy, 0.0)
end_x = np.clip(np.round(cc + dx).astype(np.int32), 0, w - 1)
end_y = np.clip(np.round(rr + dy).astype(np.int32), 0, h - 1)
+13
View File
@@ -145,3 +145,16 @@ def test_paint_domain_background_colours() -> None:
)
assert tuple(canvas[0, 0]) == (1, 2, 3)
assert tuple(canvas[1, 1]) == (10, 20, 30)
def test_select_flow_samples_screens_non_finite_tips() -> None:
"""Sparse recovered fields put NaN off the recorded cells; cast must stay quiet."""
h, w = 30, 30
flow = np.full((h, w, 2), np.nan, dtype=np.float64)
flow[15, 15] = (0.5, 0.0)
policy = VisualPolicy(flow_on_px=0.4, flow_grid_spacing=10, flow_edge_margin=0)
with np.errstate(invalid="raise"):
samples = select_flow_samples(flow, None, policy)
pairs = set(zip(samples.rows.tolist(), samples.cols.tolist()))
assert (15, 15) in pairs
assert samples.active_count >= 1