fixed display problem

This commit is contained in:
ykcheungab
2026-09-25 17:53:07 +08:00
parent 12781854ca
commit 3daee6d407
7 changed files with 367 additions and 5 deletions
@@ -199,6 +199,7 @@ def draw_arrow_display(
canvas_bgr: Sequence[int] | None = None,
draw_style: str = "box",
valid_mask: Optional[np.ndarray] = None,
domain_mask: Optional[np.ndarray] = None,
colormap: Optional[np.ndarray] = None,
exterior_bgr: Sequence[int] | None = None,
island_bgr: Sequence[int] | None = None,
@@ -214,6 +215,11 @@ def draw_arrow_display(
inside, slightly darker outside). Without a mask the whole rectangle uses
``canvas_bgr`` / ``island_bgr`` for backward compatibility.
``domain_mask`` is the boundary to paint that canvas from, when the caller holds a
latched one; ``None`` means this frame's own ``valid_mask``. It never reaches
: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.
"""
@@ -233,14 +239,15 @@ def draw_arrow_display(
if exterior_bgr is None
else tuple(int(v) for v in exterior_bgr)
)
if valid_mask is not None:
boundary = valid_mask if domain_mask is None else domain_mask
if boundary is not None:
from orisys_app_shared.scene3d.visual_policy import (
build_domain_mask,
paint_domain_background,
)
domain, _ = build_domain_mask(
valid_mask, shape_hw=base.shape[:2]
boundary, shape_hw=base.shape[:2]
)
canvas = paint_domain_background(
base.shape[:2],
@@ -248,7 +255,6 @@ def draw_arrow_display(
island_bgr=inside, # type: ignore[arg-type]
exterior_bgr=outside, # type: ignore[arg-type]
)
valid_mask = domain
else:
canvas = np.empty_like(base)
canvas[:, :] = inside
@@ -61,6 +61,94 @@ MISSING_NOTE_KEY = "panels.frame_missing_note"
#: Sources whose frames carry derived fields.
DERIVED_SOURCES = (SOURCE_LIVE, SOURCE_RECOMPUTE)
#: How much a frame's domain mask must overlap the latched one before the latched one
#: is kept. Measured on the shipped finger island (32937 px, 39.2% of the 300x280
#: domain): a rebuilt mask that wobbles by one pixel of rim overlap the held one at
#: IoU >= 0.9633, a 200-pixel speckle at 0.9879, while an island that really moved
#: 6 px falls to 0.9102. 0.95 therefore absorbs the wobble -- which is what repaints
#: the edge, 69% of the boundary band for a 0.73% area flip -- and still adopts a real
#: move. A per-pixel frame-count hysteresis was rejected: the measured flips are spread
#: around the whole rim rather than persistent at any pixel, so it would need a long
#: count to suppress them and would then delay a genuine move by that many frames.
DOMAIN_MASK_MIN_IOU = 0.95
def mask_overlap(left: np.ndarray, right: np.ndarray) -> float:
"""Intersection over union of two equally shaped boolean masks.
Both empty counts as total agreement: two frames that agree there is no island at
all are not a change.
"""
a = np.asarray(left, dtype=bool)
b = np.asarray(right, dtype=bool)
union = int(np.count_nonzero(a | b))
if union == 0:
return 1.0
return float(np.count_nonzero(a & b)) / float(union)
class DomainMaskLatch:
"""The 2D domain boundary, kept until it stops describing the same island.
``build_domain_mask``/``paint_domain_background`` rebuild the island/exterior canvas
from ``ImageFrame.valid_mask`` on every frame, and the depth view paints that same
boundary as its "no depth" grey. The mask is the device's own UV island: geometry,
rebuilt from the same remap every frame, and the divergence map the depth view reads
is zero-filled everywhere outside it. So the edge being redrawn is the same edge,
and any difference in the mask repaints the whole boundary, which reads as the
picture lagging rather than as the boundary moving.
Latching it is the same treatment the depth *colour scale* already gets
(``StudioMainWindow._depth_range``): one decision, kept for the session, so frames
stay comparable. The latch compares against the mask it is holding — not against the
previous frame — so a slow drift accumulates and is adopted once it is material,
while a per-frame flicker around the same island never is.
``adoptions`` and ``held`` make the behaviour observable: a session that keeps
re-latching a moving boundary and a session that correctly holds a static one look
the same on screen, and only these counters tell them apart.
The array is held, not copied. A producer that hands a mask over must not write into
it again — the same contract ``scene3d.contracts.freeze`` enforces for every other
array a frame carries, which is what makes holding it safe.
"""
__slots__ = ("_mask", "min_iou", "adoptions", "held")
def __init__(self, *, min_iou: float = DOMAIN_MASK_MIN_IOU) -> None:
self._mask: Optional[np.ndarray] = None
self.min_iou = float(min_iou)
self.adoptions = 0
self.held = 0
@property
def mask(self) -> Optional[np.ndarray]:
return self._mask
def reset(self) -> None:
"""Drop the latched boundary: a new session has not established one yet."""
self._mask = None
self.adoptions = 0
self.held = 0
def update(self, mask: Optional[np.ndarray]) -> Optional[np.ndarray]:
"""The mask to draw the boundary with, given this frame's own mask.
A frame that carries no mask cannot move the boundary, so it inherits the
session's: the island belongs to the sensor, not to the frame that happens not
to state it (a preview frame, a frame from a source that never had one).
"""
if mask is None:
return self._mask
current = np.asarray(mask, dtype=bool)
if self._mask is not None and current.shape == self._mask.shape:
if mask_overlap(self._mask, current) >= self.min_iou:
self.held += 1
return self._mask
self._mask = current
self.adoptions += 1
return self._mask
@dataclass(frozen=True)
class ImageFrame:
@@ -196,15 +284,24 @@ def render_image(
draw_style: str = "box",
depth_range: Optional[tuple[float, float]] = None,
canvas_bgr: Optional[Sequence[int]] = None,
domain_mask: Optional[np.ndarray] = None,
) -> Optional[np.ndarray]:
"""Draw ``frame`` in ``mode``; ``None`` when there is nothing to draw.
The mode is resolved through the plan first, so a caller that asks for a view the
frame cannot support gets the plan's default rather than a picture assembled from
an array that is not there.
``domain_mask`` is the *latched* boundary to paint the island/exterior canvas and
the depth view's "no depth" grey from (see :class:`DomainMaskLatch`); ``None`` means
"use this frame's own ``valid_mask``", which is what a caller with no session to
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.
"""
plan = plan or plan_image(frame)
wanted = plan.resolve(mode)
boundary = frame.valid_mask if domain_mask is None else domain_mask
if wanted == DEPTH:
# Never substitute another picture for a missing depth map: that made "no
# depth" look like a depth reading. The panel says so instead.
@@ -214,7 +311,7 @@ def render_image(
frame.depth,
flow_colormap(theme),
value_range=depth_range,
invalid_mask=frame.valid_mask,
invalid_mask=boundary,
)
if frame.image_bgr is None:
return None
@@ -229,6 +326,7 @@ def render_image(
canvas_bgr=canvas_bgr,
draw_style=draw_style,
valid_mask=frame.valid_mask,
domain_mask=domain_mask,
colormap=flow_colormap(theme),
)
# ``recorded``: the frame is the picture, with nothing drawn over it.