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
+5
View File
@@ -25,6 +25,11 @@ apps/studio/preference.json
shared/tests/_scratch/ shared/tests/_scratch/
shared/tests/_scratch_*/ shared/tests/_scratch_*/
# Working artefacts: screenshots and probe output from 3D/UI diagnosis, and assets parked
# out of the shipping path. None of these are inputs to a build or a release.
.snap/
_parked_stale_assets/
# IDE / OS # IDE / OS
.idea/ .idea/
.vscode/ .vscode/
+28 -1
View File
@@ -30,6 +30,7 @@ from orisys_app_shared.acquisition.image_backend import (
SOURCE_LIVE, SOURCE_LIVE,
SOURCE_PREVIEW, SOURCE_PREVIEW,
SOURCE_RECOMPUTE, SOURCE_RECOMPUTE,
DomainMaskLatch,
ImageFrame, ImageFrame,
plan_image, plan_image,
render_image, render_image,
@@ -222,6 +223,11 @@ class StudioMainWindow(QMainWindow):
self._last_calibration_export = "" self._last_calibration_export = ""
self._depth_range_value: tuple[float, float] | None = None self._depth_range_value: tuple[float, float] | None = None
self._last_depth_frame = None self._last_depth_frame = None
#: The 2D domain boundary, latched for the session the way the depth range is:
#: the island is the device's own geometry and is rebuilt from the same remap
#: every frame, so re-deriving the edge per frame redrew a boundary that had not
#: moved (see ``image_backend.DomainMaskLatch``).
self._domain_mask = DomainMaskLatch()
icon = QIcon(str(logo_path())) icon = QIcon(str(logo_path()))
if not icon.isNull(): if not icon.isNull():
@@ -2028,6 +2034,10 @@ class StudioMainWindow(QMainWindow):
self._last_display_available = None self._last_display_available = None
self._depth_range_value = None self._depth_range_value = None
self._last_depth_frame = None self._last_depth_frame = None
# The new session has not established a domain boundary yet; the previous
# session's island must not survive into it (a different sensor is a different
# island, and the latch compares against what it holds).
self._domain_mask.reset()
# The session's own bookkeeping (its counter and telemetry) is the # The session's own bookkeeping (its counter and telemetry) is the
# controller's: telling it a run starts is what drops the previous run's # controller's: telling it a run starts is what drops the previous run's
# numbers, so none of them can be read as this run's. # numbers, so none of them can be read as this run's.
@@ -2461,6 +2471,19 @@ class StudioMainWindow(QMainWindow):
return "" return ""
return t("panels.depth_scale", low=f"{span[0]:.3f}", high=f"{span[1]:.3f}") return t("panels.depth_scale", low=f"{span[0]:.3f}", high=f"{span[1]:.3f}")
def _latched_domain_mask(self, frame: "ImageFrame"):
"""The 2D domain boundary to draw, latched for the session.
Exactly the treatment ``_depth_range`` gives the colour scale, and for the same
reason: the boundary is not a per-frame measurement. It is the device's own UV
island — geometry, rebuilt from the same remap on every frame — and the depth
view paints it as its "no depth" grey, so rebuilding it per frame redrew an edge
that had not moved while any difference in ``frame.valid_mask`` repainted the
whole boundary, which reads as lag. The latch holds the boundary until a frame
no longer describes the same island (see ``DomainMaskLatch``).
"""
return self._domain_mask.update(frame.valid_mask)
def _replay_owns_image(self) -> bool: def _replay_owns_image(self) -> bool:
"""True while the 2D panel shows the replay, not the live camera. """True while the 2D panel shows the replay, not the live camera.
@@ -2518,7 +2541,10 @@ class StudioMainWindow(QMainWindow):
draw_style = self._draw_style() draw_style = self._draw_style()
# The legend is decided by the same inputs as the picture: the arrow # The legend is decided by the same inputs as the picture: the arrow
# magnification of this sensor's draw style, and whether this frame # magnification of this sensor's draw style, and whether this frame
# carries a valid-area mask. # carries a valid-area mask. It stays the *frame's* statement rather than the
# latched boundary's, because that is what the chip claims: the arrows are
# gated by this frame's own mask on every path, latched boundary or not.
boundary = self._latched_domain_mask(frame)
self.arrow_panel.set_arrow_scale(arrow_display_scale(draw_style)) self.arrow_panel.set_arrow_scale(arrow_display_scale(draw_style))
self.arrow_panel.set_valid_area(frame.valid_mask is not None) self.arrow_panel.set_valid_area(frame.valid_mask is not None)
if mode == DEPTH: if mode == DEPTH:
@@ -2534,6 +2560,7 @@ class StudioMainWindow(QMainWindow):
draw_style=draw_style, draw_style=draw_style,
depth_range=self._depth_range(), depth_range=self._depth_range(),
canvas_bgr=hex_to_bgr(color(self._theme, "arrow_canvas", "#0B1830")), canvas_bgr=hex_to_bgr(color(self._theme, "arrow_canvas", "#0B1830")),
domain_mask=boundary,
) )
if rendered is None: if rendered is None:
# Never substitute another picture here: a missing depth map that showed # Never substitute another picture here: a missing depth map that showed
+53
View File
@@ -925,6 +925,59 @@ def test_depth_view_reports_missing_data_and_fixes_its_scale(make_window):
_finish(window, stuck) _finish(window, stuck)
def test_the_domain_boundary_is_latched_for_the_session(make_window):
"""The 2D boundary is the device's island: rebuilt per frame, decided once.
Same treatment as the depth colour scale above, and for the same reason — a mask
that is re-derived every frame redraws an edge that has not moved, and any pixel of
difference repaints the whole boundary (measured: a 0.73% area wobble repaints 69%
of the boundary band).
"""
import numpy as np
window, stuck = _running_window(make_window)
window.arrow_panel.set_view_mode("arrows_pure")
island = np.zeros((8, 8), dtype=bool)
island[1:7, 1:7] = True
island_wobble = island.copy()
island_wobble[1, 1] = False # one pixel of rim, i.e. 1.6% of a 36-pixel island
window._last_rendered_view_mode = None
stuck.emit(ui_check._fake_packet(0, valid_mask=island))
window._on_run_tick()
assert np.array_equal(window._domain_mask.mask, island), (
"the session never established the device's island"
)
window._last_rendered_view_mode = None
stuck.emit(ui_check._fake_packet(1, valid_mask=island_wobble))
window._on_run_tick()
assert np.array_equal(window._domain_mask.mask, island), (
"a rebuilt mask moved the session's boundary"
)
assert (window._domain_mask.adoptions, window._domain_mask.held) == (1, 1)
# An island that really changed is adopted rather than frozen out.
moved = np.zeros((8, 8), dtype=bool)
moved[2:8, 2:8] = True
window._last_rendered_view_mode = None
stuck.emit(ui_check._fake_packet(2, valid_mask=moved))
window._on_run_tick()
assert np.array_equal(window._domain_mask.mask, moved), (
"a moved island kept the old boundary on screen"
)
assert window._domain_mask.adoptions == 2
# A new run is a new session: the previous island must not survive into it.
_finish(window, stuck)
window._start_run()
assert window._domain_mask.mask is None, (
"the previous session's island survived into a new run"
)
window._run_timer.stop()
window._stop_run(reason="user_stop")
# ------------------------------------------------------------- experiment record # ------------------------------------------------------------- experiment record
def test_recording_writes_the_documented_experiment_layout(make_window, record_root): def test_recording_writes_the_documented_experiment_layout(make_window, record_root):
import csv import csv
+4
View File
@@ -177,6 +177,7 @@ def _fake_packet(
mono_t: float | None = None, mono_t: float | None = None,
shear_n: float | None = None, shear_n: float | None = None,
frame=None, frame=None,
valid_mask=None,
): ):
"""A packet shaped like the real worker's, for the record-channel checks. """A packet shaped like the real worker's, for the record-channel checks.
@@ -213,6 +214,9 @@ def _fake_packet(
invalid_reason=reason, invalid_reason=reason,
raw_available=True, raw_available=True,
depth=depth, depth=depth,
# The device's own UV-island mask, which the real workers pass through from the
# runtime (``runtime.valid_mask``); the 2D boundary is drawn from it.
valid_mask=valid_mask,
# Per-field provenance: a producer has to say whether the arrays it hands over # Per-field provenance: a producer has to say whether the arrays it hands over
# are this frame's own (the 3D sink refuses an unstated field rather than # are this frame's own (the 3D sink refuses an unstated field rather than
# colouring something it cannot claim). A failed frame is stale here, exactly # colouring something it cannot claim). A failed frame is stale here, exactly
@@ -199,6 +199,7 @@ def draw_arrow_display(
canvas_bgr: Sequence[int] | None = None, canvas_bgr: Sequence[int] | None = None,
draw_style: str = "box", draw_style: str = "box",
valid_mask: Optional[np.ndarray] = None, valid_mask: Optional[np.ndarray] = None,
domain_mask: Optional[np.ndarray] = None,
colormap: Optional[np.ndarray] = None, colormap: Optional[np.ndarray] = None,
exterior_bgr: Sequence[int] | None = None, exterior_bgr: Sequence[int] | None = None,
island_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 inside, slightly darker outside). Without a mask the whole rectangle uses
``canvas_bgr`` / ``island_bgr`` for backward compatibility. ``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 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. 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 if exterior_bgr is None
else tuple(int(v) for v in exterior_bgr) 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 ( from orisys_app_shared.scene3d.visual_policy import (
build_domain_mask, build_domain_mask,
paint_domain_background, paint_domain_background,
) )
domain, _ = build_domain_mask( domain, _ = build_domain_mask(
valid_mask, shape_hw=base.shape[:2] boundary, shape_hw=base.shape[:2]
) )
canvas = paint_domain_background( canvas = paint_domain_background(
base.shape[:2], base.shape[:2],
@@ -248,7 +255,6 @@ def draw_arrow_display(
island_bgr=inside, # type: ignore[arg-type] island_bgr=inside, # type: ignore[arg-type]
exterior_bgr=outside, # type: ignore[arg-type] exterior_bgr=outside, # type: ignore[arg-type]
) )
valid_mask = domain
else: else:
canvas = np.empty_like(base) canvas = np.empty_like(base)
canvas[:, :] = inside canvas[:, :] = inside
@@ -61,6 +61,94 @@ MISSING_NOTE_KEY = "panels.frame_missing_note"
#: Sources whose frames carry derived fields. #: Sources whose frames carry derived fields.
DERIVED_SOURCES = (SOURCE_LIVE, SOURCE_RECOMPUTE) 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) @dataclass(frozen=True)
class ImageFrame: class ImageFrame:
@@ -196,15 +284,24 @@ def render_image(
draw_style: str = "box", draw_style: str = "box",
depth_range: Optional[tuple[float, float]] = None, depth_range: Optional[tuple[float, float]] = None,
canvas_bgr: Optional[Sequence[int]] = None, canvas_bgr: Optional[Sequence[int]] = None,
domain_mask: Optional[np.ndarray] = None,
) -> Optional[np.ndarray]: ) -> Optional[np.ndarray]:
"""Draw ``frame`` in ``mode``; ``None`` when there is nothing to draw. """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 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 frame cannot support gets the plan's default rather than a picture assembled from
an array that is not there. 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) plan = plan or plan_image(frame)
wanted = plan.resolve(mode) wanted = plan.resolve(mode)
boundary = frame.valid_mask if domain_mask is None else domain_mask
if wanted == DEPTH: if wanted == DEPTH:
# Never substitute another picture for a missing depth map: that made "no # Never substitute another picture for a missing depth map: that made "no
# depth" look like a depth reading. The panel says so instead. # depth" look like a depth reading. The panel says so instead.
@@ -214,7 +311,7 @@ def render_image(
frame.depth, frame.depth,
flow_colormap(theme), flow_colormap(theme),
value_range=depth_range, value_range=depth_range,
invalid_mask=frame.valid_mask, invalid_mask=boundary,
) )
if frame.image_bgr is None: if frame.image_bgr is None:
return None return None
@@ -229,6 +326,7 @@ def render_image(
canvas_bgr=canvas_bgr, canvas_bgr=canvas_bgr,
draw_style=draw_style, draw_style=draw_style,
valid_mask=frame.valid_mask, valid_mask=frame.valid_mask,
domain_mask=domain_mask,
colormap=flow_colormap(theme), colormap=flow_colormap(theme),
) )
# ``recorded``: the frame is the picture, with nothing drawn over it. # ``recorded``: the frame is the picture, with nothing drawn over it.
+169
View File
@@ -191,3 +191,172 @@ def test_a_missing_field_is_reported_not_invented():
base = backend.render_image(no_flow, mode="arrows_pure") base = backend.render_image(no_flow, mode="arrows_pure")
assert base is not None assert base is not None
assert np.array_equal(base, _image()), "the base picture must not be redrawn" assert np.array_equal(base, _image()), "the base picture must not be redrawn"
# ------------------------------------------------------- the latched boundary
#: A domain the same size and the same kind of shape as the shipped finger sensor's
#: 300x280 UV island: one large blob with a long smooth rim, so "a one-pixel wobble"
#: and "the island moved" have the overlap the real package has (measured on the
#: shipped island: a 1 px wobble scores IoU 0.9633, a 6 px move 0.9102).
DOMAIN_HW = (300, 280)
ISLAND_RADIUS_PX = 95
def _island(h: int = DOMAIN_HW[0], w: int = DOMAIN_HW[1]) -> np.ndarray:
yy, xx = np.mgrid[0:h, 0:w]
return ((yy - h / 2.0) ** 2 + (xx - w / 2.0) ** 2) <= float(ISLAND_RADIUS_PX) ** 2
def _blank_image(h: int = DOMAIN_HW[0], w: int = DOMAIN_HW[1]) -> np.ndarray:
"""A base picture the two views do not read: only depth and the mask are drawn."""
return np.zeros((h, w, 3), dtype=np.uint8)
def _shift(mask: np.ndarray, dy: int, dx: int) -> np.ndarray:
out = np.zeros_like(mask)
h, w = mask.shape
ys = slice(max(0, dy), h + min(0, dy))
xs = slice(max(0, dx), w + min(0, dx))
ys2 = slice(max(0, -dy), h + min(0, -dy))
xs2 = slice(max(0, -dx), w + min(0, -dx))
out[ys, xs] = mask[ys2, xs2]
return out
def test_overlap_of_two_identical_masks_is_one():
mask = _island()
assert backend.mask_overlap(mask, mask) == 1.0
assert backend.mask_overlap(mask, ~mask) == 0.0
# Two frames that agree there is no island at all are not a change.
empty = np.zeros((4, 4), dtype=bool)
assert backend.mask_overlap(empty, empty) == 1.0
def test_the_gate_separates_a_rebuilt_mask_from_a_moved_one():
"""The threshold itself, on the two cases it exists to tell apart."""
island = _island()
for dy, dx in ((1, 0), (0, 1), (1, 1), (-1, 1), (2, 0)):
assert backend.mask_overlap(island, _shift(island, dy, dx)) >= (
backend.DOMAIN_MASK_MIN_IOU
), f"a {dy},{dx} rebuild would be taken for a moved boundary"
assert backend.mask_overlap(island, _shift(island, 6, 0)) < backend.DOMAIN_MASK_MIN_IOU
def test_a_stable_domain_mask_is_latched_once_for_the_session():
"""The device's island is geometry: rebuilt per frame, decided once."""
latch = backend.DomainMaskLatch()
mask = _island()
seen = {latch.update(mask).tobytes() for _ in range(20)}
assert len(seen) == 1, "a stable mask was rebuilt into a different array"
assert np.array_equal(latch.mask, mask)
assert (latch.adoptions, latch.held) == (1, 19)
def test_a_wobbling_domain_mask_does_not_repaint_the_boundary():
"""A one-pixel rim wobble is the mask being rebuilt, not the island moving.
The measured case: on the shipped finger island this is a 0.73% area flip that
repaints 69% of the boundary band and 35% of the rendered edge pixels.
"""
latch = backend.DomainMaskLatch()
image = _blank_image()
# A zero field, so nothing but the boundary can change the picture.
flow = np.zeros((*DOMAIN_HW, 2), dtype=np.float32)
depth = np.ones(DOMAIN_HW, dtype=np.float32)
wobbles = [(0, 0), (1, 0), (0, -1), (1, 1), (-1, 0), (0, 1), (-1, -1)]
arrows, maps = [], []
for dy, dx in wobbles:
mask = _shift(_island(), dy, dx)
frame = backend.ImageFrame(image, flow=flow, depth=depth, valid_mask=mask)
used = latch.update(mask)
arrows.append(
backend.render_image(
frame, mode="arrows_pure", draw_style="finger", domain_mask=used
)
)
maps.append(
backend.render_image(
frame, mode="depth", depth_range=(0.0, 1.0), domain_mask=used
)
)
assert all(np.array_equal(arrows[0], other) for other in arrows), (
"the arrow canvas repainted a boundary that had not moved"
)
assert all(np.array_equal(maps[0], other) for other in maps), (
"the depth view's no-data boundary repainted a boundary that had not moved"
)
assert latch.adoptions == 1, "a rim wobble was taken for a moved boundary"
def test_a_real_move_of_the_domain_is_followed():
"""The latch must not freeze a boundary that actually moved."""
latch = backend.DomainMaskLatch()
island = _island()
latch.update(island)
moved = _shift(island, 6, 0)
used = latch.update(moved)
assert np.array_equal(used, moved), "a moved island kept the old boundary"
assert latch.adoptions == 2
frame = backend.ImageFrame(
_blank_image(), flow=np.zeros((*DOMAIN_HW, 2), np.float32)
)
before = backend.render_image(
frame, mode="arrows_pure", draw_style="finger", domain_mask=island
)
after = backend.render_image(
frame, mode="arrows_pure", draw_style="finger", domain_mask=used
)
assert not np.array_equal(before, after)
def test_a_frame_without_a_mask_cannot_move_the_boundary():
latch = backend.DomainMaskLatch()
assert latch.update(None) is None, "a session with no mask invented one"
island = _island()
latch.update(island)
assert np.array_equal(latch.update(None), island), (
"a frame that states no mask dropped the session's boundary"
)
assert (latch.adoptions, latch.held) == (1, 0)
def test_the_latched_boundary_never_gates_the_arrows():
"""Latching the boundary must not put an arrow on an unvouched-for sample."""
image = _blank_image()
flow = np.zeros((*DOMAIN_HW, 2), dtype=np.float32)
flow[::10, ::10, 0] = 3.0
# The frame claims nothing; the session's boundary is the whole domain.
frame = backend.ImageFrame(image, flow=flow, valid_mask=np.zeros(DOMAIN_HW, bool))
boundary = np.ones(DOMAIN_HW, dtype=bool)
got = backend.render_image(
frame, mode="arrows_pure", draw_style="finger", domain_mask=boundary
)
expected = draw_arrow_display(
image,
flow,
background="pure_color",
draw_style="finger",
valid_mask=frame.valid_mask,
domain_mask=boundary,
colormap=flow_colormap("dark"),
)
assert np.array_equal(got, expected)
# A whole-domain boundary paints one tone; an empty frame mask means no arrows,
# so the picture stays that one tone rather than gaining arrow pixels.
tones = {tuple(int(v) for v in tone) for tone in np.unique(got.reshape(-1, 3), axis=0)}
assert tones == {(0x16, 0x13, 0x12)}, (
f"arrows were drawn from a mask the frame does not claim: {tones}"
)
def test_resetting_the_latch_clears_the_session_boundary():
latch = backend.DomainMaskLatch()
latch.update(_island())
latch.reset()
assert latch.mask is None
assert (latch.adoptions, latch.held) == (0, 0)
other = _shift(_island(), 5, 5)
assert np.array_equal(latch.update(other), other), (
"a new session kept the previous session's island"
)