Merge branch 'main' of ssh://892887cvmk08.aicp.io:24928/orisys-tech/orisys-app

This commit is contained in:
2026-10-09 13:41:39 +08:00
8 changed files with 99 additions and 160 deletions
@@ -78,16 +78,16 @@ _ARROW_DRAW_KW = dict(
below_threshold_color=hex_to_bgr(FLOW_IDLE_HEX),
)
#: ``draw_arrows`` reads every point of this grid, including those under its threshold
#: (they become the idle dots). The cap is only a guard: a stitch image does not have
#: this many points, and a lower cap would drop cells the live painter still draws.
#: Box/Mini live and analysis walk every point of this grid, including those under
#: the threshold (they become the idle dots). The cap is only a guard: a stitch image
#: does not have this many points, and a lower cap would drop cells the painter still draws.
SDK_BOX_MAX_ARROWS = 100_000
def sdk_box_policy():
"""The sample grid :func:`orisys.util.draw_arrows` walks for a box frame.
"""The sample grid a box frame is selected and drawn with.
Live box arrows are not the shared selector's default (spacing 10, 400 strongest).
Box arrows are not the finger selector's default (spacing 10, 400 strongest).
A stored result has to record *this* grid or the replay cannot hand the same cells
back to the same painter.
"""
@@ -98,8 +98,7 @@ def sdk_box_policy():
flow_off_px=float(_ARROW_DRAW_KW["threshold"]) * 0.75,
flow_grid_spacing=int(_ARROW_DRAW_KW["grid_spacing"]),
flow_arrow_scale=float(_ARROW_DRAW_KW["arrow_scale"]),
# ``draw_arrows`` still folds the border cells into its colour range, then
# refuses to draw them. Recording them (edge 0) is what keeps that range.
# Edge 0 keeps border cells in the colour range the box pass has always used.
flow_edge_margin=0,
flow_length_margin=0.8,
max_arrows=SDK_BOX_MAX_ARROWS,
@@ -107,7 +106,7 @@ def sdk_box_policy():
def uses_sdk_box_grid(policy) -> bool:
"""True when ``policy`` is the box painter's grid, not the shared selector's."""
"""True when ``policy`` is the box sample grid, not the finger selector's."""
if policy is None:
return False
spacing = getattr(policy, "flow_grid_spacing", None)
@@ -223,8 +222,8 @@ def flow_colormap(theme: str = "dark") -> np.ndarray:
def arrow_display_scale(draw_style: str = "box") -> float:
"""How much the drawn arrows amplify the measured displacement.
``orisys.util.draw_arrows`` divides the displacement by ``arrow_scale``, so
the effective display magnification is its reciprocal (box ``0.5`` -> ``2x``,
The shared selector divides displacement by ``flow_arrow_scale``, so the
effective display magnification is its reciprocal (box ``0.5`` -> ``2x``,
finger ``1.0`` -> ``1x``). The drawn length is additionally capped at
``grid_spacing * 0.8`` pixels, so the returned factor describes the mapping
only while an arrow has not hit that cap — the UI says so in its tooltip.
@@ -345,9 +344,7 @@ def _policy_from_style(
#: The pixels ``cv2.circle(..., radius=1, thickness=-1)`` fills, as ``(dy, dx)`` offsets
#: from the centre it is given. One marker shape for every sensor type: the SDK's
#: ``orisys.util.draw_arrows`` paints a below-threshold sample with exactly that call, so
#: a dot on the finger canvas has to be the same plus-shaped 5-pixel blob a box dot is.
#: from the centre it is given. One plus-shaped 5-pixel blob for every sensor type.
#: Painting the offsets in one vectorised write, instead of one ``cv2.circle`` per dot,
#: is what keeps a full finger grid cheap: on a 276-candidate island the pass costs 47 us
#: against 223 us for the call loop, and the whole draw gains only ~35 us.
@@ -482,7 +479,6 @@ def draw_arrow_display(
magnitude_range: tuple[float, float] | None = None,
per_frame_colour: bool = False,
idle_dots: bool | None = None,
shared_painter: bool = False,
) -> np.ndarray:
"""Draw flow arrows on a solid canvas or over the processed frame.
@@ -498,9 +494,8 @@ def draw_arrow_display(
``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.
Passing it is what makes the two figures trigger at the same strength. Without it,
finger style falls back to the finger defaults and box style to :func:`sdk_box_policy`.
``magnitude_range`` fixes the colour scale, in the same unit as ``flow_on_px``
(uv pixels of displacement); the default is :data:`FLOW_MAGNITUDE_RANGE` and a
@@ -508,11 +503,8 @@ def draw_arrow_display(
back in to normalising every frame against its own maximum, which makes one
magnitude a different colour from one frame to the next.
``idle_dots`` shows or hides the below-threshold markers of the **shared finger
canvas**; ``None`` means :data:`DRAW_IDLE_GRID_DOTS`, which is on. Box/Mini draw
through the SDK's own ``orisys.util.draw_arrows``, which paints its own below-threshold
dots (same tint, same radius-1 marker) and has no such switch: this one governs the
canvas that draws here, and Box keeps the appearance it has.
``idle_dots`` shows or hides the below-threshold markers; ``None`` means
:data:`DRAW_IDLE_GRID_DOTS`, which is on.
Returns the drawn image, which carries ``frame_diagnostic``: ``None`` when the frame
was drawn, and otherwise a :class:`~orisys_app_shared.scene3d.contracts.Diagnostic`
@@ -521,22 +513,12 @@ def draw_arrow_display(
that shows it without reading the attribute reports "nothing measured" for "nothing
could be drawn". A ``flow`` of ``None`` is a legitimate empty frame and leaves the
attribute ``None``.
``shared_painter`` forces this module's per-sample painter for a field that came from
somewhere other than a live chain — a *recovered* field, finite only at the cells a
stored analysis result recorded. The SDK painter walks the dense grid, which on such a
field is almost entirely ``NaN``; measured on one stored box frame it paints **0 px**
where the shared painter paints **1,486 px** (the frame's own candidate cells). It is a
statement about the input, not about the sensor: the box *style* still decides the
magnification and the idle-dot tint, and a live box frame keeps the SDK path.
"""
base = _ensure_bgr(processed_frame)
if flow is None:
return _flow_result(base)
finger = str(draw_style).lower().startswith("f") or bool(shared_painter)
finger = str(draw_style).lower().startswith("f")
draw_kw = dict(_FINGER_ARROW_DRAW_KW if finger else _ARROW_DRAW_KW)
if colormap is not None:
draw_kw["colormap"] = colormap
try:
if background == "pure_color":
color_bgr = ARROW_BG_BGR if canvas_bgr is None else tuple(int(v) for v in canvas_bgr)
@@ -568,39 +550,33 @@ def draw_arrow_display(
else:
canvas = base
if finger:
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,
)
if policy is not None:
selected = policy
elif finger:
selected = _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 _flow_result(
_paint_shared_flow_arrows(
canvas,
np.asarray(flow),
valid_mask=valid_mask,
policy=selected,
colormap=colormap,
below_threshold_color=tuple(draw_kw["below_threshold_color"]),
magnitude_range=magnitude_range,
per_frame_colour=per_frame_colour,
# Read here rather than as a parameter default: the module constant is
# the display decision, and a caller that flips it (a build, a test)
# must be obeyed without every call site having to pass it.
idle_dots=DRAW_IDLE_GRID_DOTS if idle_dots is None else bool(idle_dots),
)
)
import orisys
else:
selected = sdk_box_policy()
return _flow_result(
orisys.util.draw_arrows(canvas, flow, valid_mask=valid_mask, **draw_kw)
_paint_shared_flow_arrows(
canvas,
np.asarray(flow),
valid_mask=valid_mask,
policy=selected,
colormap=colormap,
below_threshold_color=tuple(draw_kw["below_threshold_color"]),
magnitude_range=magnitude_range,
per_frame_colour=per_frame_colour,
# Read here rather than as a parameter default: the module constant is
# the display decision, and a caller that flips it (a build, a test)
# must be obeyed without every call site having to pass it.
idle_dots=DRAW_IDLE_GRID_DOTS if idle_dots is None else bool(idle_dots),
)
)
except Exception as exc:
# The frame is *not* idle: it failed to draw. The image alone cannot say so, so
@@ -33,7 +33,6 @@ from orisys_app_shared.acquisition.arrows import (
depth_map_to_bgr,
draw_arrow_display,
flow_colormap,
uses_sdk_box_grid,
)
from orisys_app_shared.acquisition.display_orientation import (
ORIENTATION_IDENTITY,
@@ -396,26 +395,14 @@ def _draw_flow(
domain_mask: np.ndarray | None,
arrow_policy,
canvas_bgr: Sequence[int] | None = None,
sdk_result: bool | None = None,
) -> np.ndarray:
"""Arrows on ``base``, using the same painter the pure-arrow views use."""
if sdk_result is None:
box_style = not str(draw_style).lower().startswith("f")
sdk_result = (
box_style
and str(frame.source or "") in RESULT_SOURCES
and uses_sdk_box_grid(arrow_policy)
)
flow = frame.flow
if sdk_result and flow is not None:
flow = np.nan_to_num(np.asarray(flow, dtype=np.float64), nan=0.0)
return draw_arrow_display(
base,
flow,
frame.flow,
background=background,
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),
@@ -534,9 +534,8 @@ class AnalysisService:
)
#: The policy the samples are selected with. Recorded, never keyed (see
#: ``model.py``): the shipped defaults make a stored result independent of the
#: operator's display preferences. A box chain records the live painter's own
#: grid; the shared defaults are the finger painter's and would drop every cell
#: ``draw_arrows`` still draws.
#: operator's display preferences. A box chain records the box sample grid;
#: the finger defaults would drop cells that grid still draws.
if policy is None and chain.sensor_type == "box":
from orisys_app_shared.acquisition.arrows import sdk_box_policy