c2293d4d41
- What(颜色来源): `DisplaySettings` 增 `field`(`"depth"` 默认 / `"flow"`),随显示设置持久化 (`scene3d/prefs.py`:新增 `FIELD_CHOICES`/`DEFAULT_FIELD`,手改文件里的非法值修回默认)。 `FingerFrameAdapter` 按所选字段先试、失败再用另一个,并把"换了量"这件事记在 `last_substitution`(新诊断码 `field_substituted`,info 级);两个字段都拿不到时, `last_diagnostic` 取**信息量最大**的那条(域不符/过期/未声明 > 没这个字段),而不是"所选字段"的 那条空话。设置对话框新增「颜色来源」下拉,下面一行 `field_note` 直接写"这一帧画的是 X,不是所选的 Y"; 图例(`legend_label`)永远标注**实际**画出来的量,所以设定与画面不一致时两处都能看出来。 深度响应 `[1]`、光流幅值 `[px]`,两者都是 `approximate`——代码与文案都不把它们说成毫米。 - What(诊断信息开关): 视图菜单新增 `诊断信息`(可勾选、持久化 `show_diagnostics`、**默认关**, 由 `DisplayPrefsController` 持有并推给两个面板)。关掉后:2D 面板只留"画面 + 计划说明 + 深度色标", 三条解释流水线的标签(箭头按位移放大、原始图像 → 算法输出、全部帧)隐藏;3D 面板只留 色条 + 一行图例 + 来源,info 级诊断隐藏。**永不隐藏**:warning/error 级诊断(测量是否过期、 字段是否本帧的、是否与设备核对过——`geometry_unverified` 虽然是 info 也白名单保留)与 深度色标。3D 图例改成 `ShrinkableLabel`(一行、裁切 + tooltip,不再折行),色标数值从 4 位有效数字 收到 3 位("0.0334 – 0.899")。设置对话框控件宽度 88 → 108 px(不再需要迁就 240 px 面板)。 - What(取景脚本): `tools/ui_snapshot.py --scene flow3d` 现在同时给 flow 与 depth(各带 provenance), 所以「颜色来源」开关有东西可切;再加 `.repro/shot_variants.py` 一次出四张图 (深度版 / 光流版 / 诊断开 / 设置对话框)。 - What(测试): `test_scene3d_adapters.py` 增 3 项(默认深度、换量时给出 `field_substituted` 并在恢复后 清掉、depth 不在场景栅格时按 `field_domain_mismatch` 拒绝而不是缩放);`test_scene3d_view.py` 增 1 项(field 的持久化与修复,并把 `field in FIELD_CHOICES` 加进"手改文件会被修复"的参数化); `test_scene3d_pane_ui.py` 增 3 项(两种量可选 + 说明行 + 恢复的偏好会移动下拉;诊断开关只隐藏 info、 保留 warning 与"未核对";2D 脚注三标签的显隐与窗口开关一致);`test_display_prefs.py` 增 1 项 (开关推到两个面板、写偏好)。`test_ui_smoke.py` 与 `tools/ui_check.py` 里"箭头图例可见"的四条断言 改为按新规则检查(默认隐藏、开诊断后可见、关掉再隐藏)。 - Why: ①"深度"比"光流"更贴近操作员口中的"压得多深",但两者都只是近似量,所以做成开关而不是替换—— 并且换量必须说出来(否则画面会被读成所选的量)。②面板脚下的解释文字是"内部管线"而不是"画面是什么", 拍视频与日常看都只需要后者;用一个开关收起,比做一个滚动日志面板诚实(这些是每帧状态,不是事件)。 - Verification: pytest shared/tests apps/studio/tests(offscreen)658 passed / 0 failed / 15 skipped; 真 GL 组(gl_smoke + view + pane_ui)70 passed / 0 failed / 1 skipped; `tools/ui_check.py` 590 项检查 0 失败;`.repro/locale_check.py` 0 缺失(新增 7 个 key 双语齐备)。 目视验收:同一批合成数据渲染的四张图(深色主题)——深度版(默认)、光流版、诊断打开、设置对话框。 - Not in this change: 不改采集/记录/契约之外的三维渲染;不做页签容器与窄窗口回退;曲线量程仍是出厂 ±20 N。 真机待验证:手指路径上 `packet.depth` 的栅格是否就是场景的 300×280(若不是,面板会写明并回退到光流)。
578 lines
23 KiB
Python
578 lines
23 KiB
Python
"""Stage 3 acceptance for the frame adapters.
|
|
|
|
The adapters are the boundary between the acquisition thread's arrays and the
|
|
scene, so the tests are about *what they publish and what they refuse*: a field in
|
|
the right grid, read-only, with the right unit and quality — and nothing at all when
|
|
the grid does not match, with a diagnostic that says which shapes were seen.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from typing import Optional
|
|
|
|
import numpy as np
|
|
import pytest
|
|
|
|
from orisys_app_shared.scene3d import synthetic
|
|
from orisys_app_shared.scene3d.contracts import (
|
|
ExpectedGeometry,
|
|
FrameIdentity,
|
|
ThreeDError,
|
|
validate_surface_frame,
|
|
)
|
|
from orisys_studio.scene3d.adapters import (
|
|
DEPTH_RESPONSE,
|
|
FLOW_MAGNITUDE,
|
|
BoxFrameAdapter,
|
|
FingerFrameAdapter,
|
|
flow_magnitude,
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class FakePacket:
|
|
"""The parts of ``SdkPacket`` the adapters look at.
|
|
|
|
The provenance flags default to *stated fresh*: that is what both production
|
|
workers now write, and it keeps the older tests about grids and units about grids
|
|
and units. A test that wants a stale or unstated field says so — that is the
|
|
behaviour under test.
|
|
"""
|
|
|
|
flow: Optional[np.ndarray] = None
|
|
depth: Optional[np.ndarray] = None
|
|
valid_mask: Optional[np.ndarray] = None
|
|
frame_id: int = 1
|
|
invalid_reason: str = ""
|
|
flow_valid: Optional[bool] = True
|
|
flow_frame_id: Optional[int] = None
|
|
depth_valid: Optional[bool] = True
|
|
depth_frame_id: Optional[int] = None
|
|
forces_valid: Optional[bool] = True
|
|
|
|
|
|
def _identity(generation: int = 1) -> FrameIdentity:
|
|
return FrameIdentity("live", 1, 1, 0.1, generation=generation)
|
|
|
|
|
|
@pytest.fixture
|
|
def scene():
|
|
return synthetic.poc_scene()
|
|
|
|
|
|
# ------------------------------------------------------------------- flow helper
|
|
def test_the_flow_magnitude_handles_both_shapes():
|
|
flow = np.zeros((2, 3, 2), dtype=np.float32)
|
|
flow[..., 0] = 3.0
|
|
flow[..., 1] = 4.0
|
|
assert np.allclose(flow_magnitude(FakePacket(flow=flow)), 5.0)
|
|
planar = np.arange(4, dtype=np.float32).reshape(2, 2)
|
|
assert np.array_equal(flow_magnitude(FakePacket(flow=planar)), planar)
|
|
assert flow_magnitude(FakePacket()) is None
|
|
assert flow_magnitude(FakePacket(flow=np.zeros((2, 2, 3), dtype=np.float32))) is None
|
|
|
|
|
|
def test_the_finger_adapter_prefers_the_quantity_the_operator_chose(scene):
|
|
"""The choice between the two quantities is a display setting, honoured per frame.
|
|
|
|
The depth response is what an operator reads as "how deep", so it is the default —
|
|
and when the frame carries it, that is what is drawn.
|
|
"""
|
|
adapter = FingerFrameAdapter(
|
|
geometry_id=scene.geometry_id,
|
|
field_domain_id=scene.field_domain_id,
|
|
shape_hw=scene.shape_hw,
|
|
)
|
|
assert adapter.preferred_field == "depth"
|
|
depth = np.full(scene.shape_hw, 2.5, dtype=np.float32)
|
|
flow = np.zeros((*scene.shape_hw, 2), dtype=np.float32)
|
|
flow[..., 0] = 7.0 # |flow| = 7 px
|
|
|
|
frame = adapter.snapshot(FakePacket(flow=flow, depth=depth), runtime=None, identity=_identity())
|
|
assert frame is not None
|
|
assert frame.scalar_descriptor == DEPTH_RESPONSE
|
|
assert frame.scalar_descriptor.unit == "1"
|
|
assert np.allclose(np.asarray(frame.scalar), 2.5)
|
|
assert adapter.last_substitution is None, "the chosen field was drawn; nothing to say"
|
|
|
|
adapter.set_preferred_field("flow")
|
|
frame = adapter.snapshot(FakePacket(flow=flow, depth=depth), runtime=None, identity=_identity())
|
|
assert frame is not None
|
|
assert frame.scalar_descriptor == FLOW_MAGNITUDE
|
|
assert np.allclose(np.asarray(frame.scalar), 7.0)
|
|
assert adapter.last_substitution is None
|
|
|
|
|
|
def test_a_frame_that_cannot_honour_the_choice_says_which_quantity_it_drew(scene):
|
|
"""Falling back is allowed; falling back silently is not.
|
|
|
|
The frame is still a measurement and the legend names it, but the *setting* said
|
|
something else, and the pane has to be able to say so instead of letting the picture
|
|
read as the quantity the operator asked for.
|
|
"""
|
|
adapter = FingerFrameAdapter(
|
|
geometry_id=scene.geometry_id,
|
|
field_domain_id=scene.field_domain_id,
|
|
shape_hw=scene.shape_hw,
|
|
)
|
|
# No depth in this frame: the flow is all it has.
|
|
flow = np.zeros((*scene.shape_hw, 2), dtype=np.float32)
|
|
flow[..., 0] = 4.0 # |flow| = 4 px
|
|
frame = adapter.snapshot(FakePacket(flow=flow), runtime=None, identity=_identity())
|
|
assert frame is not None
|
|
assert frame.scalar_descriptor == FLOW_MAGNITUDE
|
|
note = adapter.last_substitution
|
|
assert note is not None and note.code == "field_substituted"
|
|
assert note.details["wanted"] == "depth" and note.details["shown"] == "flow"
|
|
assert "no depth response" in note.details["why"]
|
|
|
|
# ...and the note goes away as soon as the chosen field can be drawn again.
|
|
depth = np.full(scene.shape_hw, 1.0, dtype=np.float32)
|
|
frame = adapter.snapshot(
|
|
FakePacket(flow=flow, depth=depth), runtime=None, identity=_identity()
|
|
)
|
|
assert frame is not None and frame.scalar_descriptor == DEPTH_RESPONSE
|
|
assert adapter.last_substitution is None
|
|
|
|
|
|
def test_a_depth_response_on_another_grid_is_refused_not_resized(scene):
|
|
"""The depth field gets the same treatment as the flow: no resizing, ever."""
|
|
adapter = FingerFrameAdapter(
|
|
geometry_id=scene.geometry_id,
|
|
field_domain_id=scene.field_domain_id,
|
|
shape_hw=scene.shape_hw,
|
|
)
|
|
wrong = np.zeros((7, 7), dtype=np.float32)
|
|
frame = adapter.snapshot(FakePacket(depth=wrong), runtime=None, identity=_identity())
|
|
assert frame is None, "a depth response that is not on the scene's grid was coloured"
|
|
item = adapter.last_diagnostic
|
|
assert item is not None and item.code == "field_domain_mismatch"
|
|
assert item.details["depth"] == "7x7"
|
|
|
|
|
|
# ----------------------------------------------------------------- finger adapter
|
|
def test_the_finger_adapter_publishes_a_field_already_in_the_domain(scene):
|
|
adapter = FingerFrameAdapter(
|
|
geometry_id=scene.geometry_id,
|
|
field_domain_id=scene.field_domain_id,
|
|
shape_hw=scene.shape_hw,
|
|
)
|
|
flow = np.zeros((*scene.shape_hw, 2), dtype=np.float32)
|
|
flow[0, 0, 0] = 3.0
|
|
frame = adapter.snapshot(FakePacket(flow=flow), runtime=None, identity=_identity())
|
|
assert frame is not None
|
|
assert validate_surface_frame(frame) == ()
|
|
assert frame.geometry_id == scene.geometry_id
|
|
assert frame.field_domain_id == scene.field_domain_id
|
|
assert frame.scalar_descriptor == FLOW_MAGNITUDE
|
|
assert frame.scalar_descriptor.unit == "px"
|
|
assert frame.scalar_descriptor.quality == "approximate"
|
|
assert np.asarray(frame.scalar)[0, 0] == 3.0
|
|
assert np.asarray(frame.valid).all()
|
|
assert not np.asarray(frame.scalar).flags["WRITEABLE"]
|
|
assert frame.displacement_sensor_m is None, (
|
|
"an uncalibrated displacement must not be invented"
|
|
)
|
|
|
|
|
|
def test_the_finger_adapter_remaps_through_the_unroll_map():
|
|
map_x = np.array([[0, 1, 2], [0, 1, 2], [0, 1, 2]], dtype=np.float32)
|
|
map_y = np.array([[2, 2, 2], [1, 1, 1], [0, 0, 0]], dtype=np.float32)
|
|
adapter = FingerFrameAdapter(
|
|
geometry_id="g", field_domain_id="d", shape_hw=(3, 3),
|
|
map_x=map_x, map_y=map_y,
|
|
)
|
|
flow = np.zeros((3, 3, 2), dtype=np.float32)
|
|
flow[..., 0] = 2.0
|
|
frame = adapter.snapshot(FakePacket(flow=flow), runtime=None, identity=_identity())
|
|
assert frame is not None
|
|
assert np.allclose(np.asarray(frame.scalar), 2.0)
|
|
assert np.asarray(frame.valid).all()
|
|
|
|
|
|
def test_a_remap_with_a_hole_is_invalid_there_and_does_not_read_as_zero():
|
|
map_x = np.array([[0, -1], [0, 0]], dtype=np.float32)
|
|
map_y = np.array([[0, -1], [0, 0]], dtype=np.float32)
|
|
adapter = FingerFrameAdapter(
|
|
geometry_id="g", field_domain_id="d", shape_hw=(2, 2),
|
|
map_x=map_x, map_y=map_y,
|
|
)
|
|
flow = np.full((2, 2, 2), 1.0, dtype=np.float32)
|
|
frame = adapter.snapshot(FakePacket(flow=flow), runtime=None, identity=_identity())
|
|
assert frame is not None
|
|
assert np.asarray(frame.valid).tolist() == [[True, False], [True, True]]
|
|
assert np.isnan(np.asarray(frame.scalar)[0, 1])
|
|
|
|
|
|
def test_the_finger_adapter_refuses_a_grid_it_cannot_place(scene):
|
|
"""No remap and the wrong grid: say which shapes were seen, and publish nothing."""
|
|
adapter = FingerFrameAdapter(
|
|
geometry_id=scene.geometry_id,
|
|
field_domain_id=scene.field_domain_id,
|
|
shape_hw=scene.shape_hw,
|
|
)
|
|
flow = np.zeros((40, 30, 2), dtype=np.float32)
|
|
frame = adapter.snapshot(FakePacket(flow=flow), runtime=None, identity=_identity())
|
|
assert frame is None
|
|
item = adapter.last_diagnostic
|
|
assert item is not None and item.code == "field_domain_mismatch"
|
|
assert item.details["flow"] == "40x30"
|
|
assert item.details["domain"] == f"{scene.shape_hw[0]}x{scene.shape_hw[1]}"
|
|
|
|
|
|
def test_the_finger_adapter_without_flow_says_so(scene):
|
|
adapter = FingerFrameAdapter(
|
|
geometry_id=scene.geometry_id,
|
|
field_domain_id=scene.field_domain_id,
|
|
shape_hw=scene.shape_hw,
|
|
)
|
|
assert adapter.snapshot(FakePacket(), runtime=None, identity=_identity()) is None
|
|
assert adapter.last_diagnostic is not None
|
|
assert adapter.last_diagnostic.code == "field_unavailable"
|
|
|
|
|
|
def test_the_finger_adapter_can_be_built_from_a_domain(scene):
|
|
from orisys_app_shared.scene3d.legacy import load_legacy_domain
|
|
|
|
map_path = _run_map()
|
|
if map_path is None:
|
|
pytest.skip("the shipped unroll map is not present")
|
|
domain = load_legacy_domain(map_path)
|
|
adapter = FingerFrameAdapter.from_domain(domain, geometry_id="geom")
|
|
assert adapter.shape_hw == domain.shape_hw
|
|
assert adapter.field_domain_id == domain.domain_id
|
|
assert adapter.map_x is not None and adapter.map_y is not None, (
|
|
"the domain must carry the unroll map for the adapter to use"
|
|
)
|
|
assert domain.source_hw == (288, 512), domain.source_hw
|
|
|
|
|
|
def test_the_shipped_map_cannot_be_applied_to_the_shipped_flow():
|
|
"""Why the app must not remap the finger's flow: the map is domain→*source*.
|
|
|
|
The unroll map is the same size as the UV domain, but its values are pixel
|
|
coordinates in the 512x288 source image — not in the 300x280 domain. The live
|
|
flow is already computed on the unrolled frame, so feeding it through this map
|
|
would be a double remap: it would index the flow at source-pixel coordinates.
|
|
This test pins the reason the app passes no map, so a later "improvement" has to
|
|
argue with it.
|
|
|
|
Note the asymmetry that makes the mistake look plausible: in x the map exceeds
|
|
the domain's column count (so it would read out of range), while in y it stays
|
|
below the row count (so it would silently read the wrong rows). Half of a
|
|
double remap crashes and half of it lies.
|
|
"""
|
|
map_path = _run_map()
|
|
if map_path is None:
|
|
pytest.skip("the shipped unroll map is not present")
|
|
from orisys_app_shared.scene3d.legacy import load_legacy_domain
|
|
|
|
domain = load_legacy_domain(map_path)
|
|
assert domain.map_x is not None and domain.map_y is not None
|
|
rows, columns = domain.shape_hw
|
|
source_rows, source_columns = domain.source_hw
|
|
assert (rows, columns) == (300, 280) and (source_rows, source_columns) == (288, 512)
|
|
|
|
# Bounded by the source, not by the domain: that is what makes them source pixels.
|
|
assert 0.0 <= float(domain.map_x[domain.map_x >= 0].max()) <= source_columns - 1
|
|
assert 0.0 <= float(domain.map_y[domain.map_y >= 0].max()) <= source_rows - 1
|
|
assert float(domain.map_x.max()) > columns - 1, (
|
|
"x would index past the last column of a 280-wide flow field"
|
|
)
|
|
assert float(domain.map_y.max()) > rows - 1 or source_rows <= rows, (
|
|
"y stays inside the 300-row field: the y half of the mistake is silent"
|
|
)
|
|
|
|
|
|
def test_the_live_flow_is_already_the_scene_domain():
|
|
"""The measurement behind that decision, taken from the shipping config.
|
|
|
|
``camera.resolution_stitch`` is what ``Sensor`` sizes its flow buffer from and
|
|
what the unroll stage outputs, so the flow grid *is* the UV domain. If a future
|
|
config changes that, this test fails and the adapter wiring has to be revisited
|
|
rather than the numbers quietly not matching.
|
|
"""
|
|
import json
|
|
from pathlib import Path
|
|
|
|
config = Path(__file__).resolve().parents[1] / "config" / "sensors" / "finger"
|
|
map_path = _run_map()
|
|
if not (config / "sensor.json").is_file() or map_path is None:
|
|
pytest.skip("the finger config and the unroll map are needed")
|
|
camera = json.loads((config / "sensor.json").read_text(encoding="utf-8"))["camera"]
|
|
resolution = camera["resolution_stitch"]
|
|
# Sensor builds `flow` as (resolution_stitch[1], resolution_stitch[0]).
|
|
flow_hw = (int(resolution[1]), int(resolution[0]))
|
|
from orisys_app_shared.scene3d.legacy import load_legacy_domain
|
|
|
|
domain = load_legacy_domain(map_path)
|
|
assert flow_hw == tuple(domain.shape_hw) == (300, 280)
|
|
|
|
|
|
def test_the_finger_adapter_does_not_colour_outside_the_uv_island(scene):
|
|
"""The device mask is what says where the surface is; outside it there is none."""
|
|
adapter = FingerFrameAdapter(
|
|
geometry_id=scene.geometry_id,
|
|
field_domain_id=scene.field_domain_id,
|
|
shape_hw=scene.shape_hw,
|
|
)
|
|
flow = np.zeros((*scene.shape_hw, 2), dtype=np.float32)
|
|
flow[..., 0] = 1.0
|
|
mask = np.zeros(scene.shape_hw, dtype=bool)
|
|
mask[1, 1] = True
|
|
frame = adapter.snapshot(
|
|
FakePacket(flow=flow, valid_mask=mask), runtime=None, identity=_identity()
|
|
)
|
|
assert frame is not None
|
|
valid = np.asarray(frame.valid)
|
|
assert valid.sum() == 1 and bool(valid[1, 1])
|
|
assert np.asarray(frame.scalar)[0, 0] == 1.0, (
|
|
"the scalar is kept as measured; only validity says it is not a measurement"
|
|
)
|
|
|
|
|
|
def test_a_device_mask_from_another_grid_refuses_the_frame(scene):
|
|
"""Neither stretching it nor ignoring it is honest — so the frame is refused."""
|
|
adapter = FingerFrameAdapter(
|
|
geometry_id=scene.geometry_id,
|
|
field_domain_id=scene.field_domain_id,
|
|
shape_hw=scene.shape_hw,
|
|
)
|
|
flow = np.ones((*scene.shape_hw, 2), dtype=np.float32)
|
|
wrong = np.zeros((7, 7), dtype=bool)
|
|
frame = adapter.snapshot(
|
|
FakePacket(flow=flow, valid_mask=wrong), runtime=None, identity=_identity()
|
|
)
|
|
assert frame is None, "a mask from another grid must not widen the trusted area"
|
|
item = adapter.last_diagnostic
|
|
assert item is not None and item.code == "field_domain_mismatch"
|
|
assert item.details["mask"] == "7x7"
|
|
assert item.details["domain"] == f"{scene.shape_hw[0]}x{scene.shape_hw[1]}"
|
|
|
|
|
|
# ------------------------------------------------- field provenance (stage 6, R2)
|
|
def _finger(scene) -> FingerFrameAdapter:
|
|
return FingerFrameAdapter(
|
|
geometry_id=scene.geometry_id,
|
|
field_domain_id=scene.field_domain_id,
|
|
shape_hw=scene.shape_hw,
|
|
)
|
|
|
|
|
|
def test_a_stale_flow_is_refused_instead_of_coloured(scene):
|
|
"""R2: the array is still finite, but it is not this frame's measurement.
|
|
|
|
This is the audit's reproduction: a packet with ``valid=False``/``stale_forces``
|
|
and a filled ``flow`` used to be coloured as valid data with no diagnostic.
|
|
"""
|
|
adapter = _finger(scene)
|
|
flow = np.full((*scene.shape_hw, 2), 4.0, dtype=np.float32)
|
|
frame = adapter.snapshot(
|
|
FakePacket(
|
|
flow=flow,
|
|
flow_valid=False,
|
|
invalid_reason="compute: ValueError: boom",
|
|
),
|
|
runtime=None,
|
|
identity=_identity(),
|
|
)
|
|
assert frame is None, "a failed chain's leftover flow was coloured as current"
|
|
item = adapter.last_diagnostic
|
|
assert item is not None and item.code == "field_stale"
|
|
assert "boom" in item.message
|
|
|
|
|
|
def test_an_unstated_flow_is_unverified_not_fresh(scene):
|
|
"""No statement is not the same as "it is mine"."""
|
|
adapter = _finger(scene)
|
|
flow = np.full((*scene.shape_hw, 2), 1.0, dtype=np.float32)
|
|
frame = adapter.snapshot(
|
|
FakePacket(flow=flow, flow_valid=None), runtime=None, identity=_identity()
|
|
)
|
|
assert frame is None
|
|
item = adapter.last_diagnostic
|
|
assert item is not None and item.code == "field_unverified"
|
|
|
|
|
|
def test_flow_computed_for_another_frame_is_refused(scene):
|
|
adapter = _finger(scene)
|
|
flow = np.full((*scene.shape_hw, 2), 2.0, dtype=np.float32)
|
|
frame = adapter.snapshot(
|
|
FakePacket(flow=flow, flow_frame_id=3),
|
|
runtime=None,
|
|
identity=_identity(), # frame_id == 1
|
|
)
|
|
assert frame is None
|
|
item = adapter.last_diagnostic
|
|
assert item is not None and item.code == "field_stale"
|
|
assert item.details["computed"] == "3" and item.details["frame"] == "1"
|
|
|
|
|
|
def test_a_fresh_flow_records_the_frame_it_belongs_to(scene):
|
|
"""The identity travels with the frame, so the display side can re-check it."""
|
|
adapter = _finger(scene)
|
|
flow = np.full((*scene.shape_hw, 2), 3.0, dtype=np.float32)
|
|
frame = adapter.snapshot(
|
|
FakePacket(flow=flow, flow_frame_id=1), runtime=None, identity=_identity()
|
|
)
|
|
assert frame is not None
|
|
assert frame.computed_frame_id == 1
|
|
assert validate_surface_frame(frame) == ()
|
|
|
|
|
|
def test_the_box_adapter_skips_a_stale_depth_and_uses_the_flow(scene):
|
|
"""A failed deformation chain does not have to cost the flow as well."""
|
|
adapter = BoxFrameAdapter(
|
|
geometry_id=scene.geometry_id,
|
|
field_domain_id=scene.field_domain_id,
|
|
shape_hw=scene.shape_hw,
|
|
)
|
|
depth = np.full(scene.shape_hw, 0.5, dtype=np.float32)
|
|
flow = np.zeros((*scene.shape_hw, 2), dtype=np.float32)
|
|
flow[..., 0] = 1.0
|
|
frame = adapter.snapshot(
|
|
FakePacket(depth=depth, depth_valid=False, flow=flow),
|
|
_identity(),
|
|
ExpectedGeometry("box", "rev", scene.geometry_id, scene.field_domain_id, scene.shape_hw),
|
|
)
|
|
assert frame is not None and frame.scalar_descriptor == FLOW_MAGNITUDE
|
|
assert np.allclose(np.asarray(frame.scalar), 1.0)
|
|
|
|
|
|
def test_the_box_adapter_refuses_when_no_field_is_its_own(scene):
|
|
adapter = BoxFrameAdapter(
|
|
geometry_id=scene.geometry_id,
|
|
field_domain_id=scene.field_domain_id,
|
|
shape_hw=scene.shape_hw,
|
|
)
|
|
depth = np.full(scene.shape_hw, 0.5, dtype=np.float32)
|
|
frame = adapter.snapshot(
|
|
FakePacket(depth=depth, depth_valid=False),
|
|
_identity(),
|
|
ExpectedGeometry("box", "rev", scene.geometry_id, scene.field_domain_id, scene.shape_hw),
|
|
)
|
|
assert frame is None
|
|
item = adapter.last_diagnostic
|
|
assert item is not None and item.code == "field_stale"
|
|
|
|
|
|
def _run_map():
|
|
from pathlib import Path
|
|
|
|
root = Path(__file__).resolve().parents[1] / "config" / "sensors" / "finger" / "assets"
|
|
for name in ("30fps-0703", "visualization"):
|
|
path = root / name / "unroll_map_uv_s0.4.npz"
|
|
if path.is_file():
|
|
return path
|
|
for path in sorted(root.glob("*/unroll_map_uv*.npz")):
|
|
return path
|
|
return None
|
|
|
|
|
|
# -------------------------------------------------------------------- box adapter
|
|
def test_the_box_adapter_prefers_the_depth_response(scene):
|
|
adapter = BoxFrameAdapter(
|
|
geometry_id=scene.geometry_id,
|
|
field_domain_id=scene.field_domain_id,
|
|
shape_hw=scene.shape_hw,
|
|
)
|
|
depth = np.full(scene.shape_hw, 0.25, dtype=np.float32)
|
|
flow = np.ones((*scene.shape_hw, 2), dtype=np.float32)
|
|
frame = adapter.snapshot(
|
|
FakePacket(depth=depth, flow=flow),
|
|
_identity(),
|
|
ExpectedGeometry("box", "rev", scene.geometry_id, scene.field_domain_id, scene.shape_hw),
|
|
)
|
|
assert frame is not None
|
|
assert frame.scalar_descriptor == DEPTH_RESPONSE
|
|
assert frame.scalar_descriptor.unit == "1", "a deformation response is not a length"
|
|
assert np.allclose(np.asarray(frame.scalar), 0.25)
|
|
|
|
|
|
def test_the_box_adapter_falls_back_to_the_flow_magnitude(scene):
|
|
adapter = BoxFrameAdapter(
|
|
geometry_id=scene.geometry_id,
|
|
field_domain_id=scene.field_domain_id,
|
|
shape_hw=scene.shape_hw,
|
|
)
|
|
flow = np.zeros((*scene.shape_hw, 2), dtype=np.float32)
|
|
flow[..., 0] = 1.0
|
|
frame = adapter.snapshot(
|
|
FakePacket(flow=flow),
|
|
_identity(),
|
|
ExpectedGeometry("box", "rev", scene.geometry_id, scene.field_domain_id, scene.shape_hw),
|
|
)
|
|
assert frame is not None and frame.scalar_descriptor == FLOW_MAGNITUDE
|
|
assert np.allclose(np.asarray(frame.scalar), 1.0)
|
|
|
|
|
|
def test_the_device_mask_wins_over_the_finiteness_test(scene):
|
|
adapter = BoxFrameAdapter(
|
|
geometry_id=scene.geometry_id,
|
|
field_domain_id=scene.field_domain_id,
|
|
shape_hw=scene.shape_hw,
|
|
)
|
|
depth = np.ones(scene.shape_hw, dtype=np.float32)
|
|
mask = np.ones(scene.shape_hw, dtype=bool)
|
|
mask[0, 0] = False
|
|
frame = adapter.snapshot(
|
|
FakePacket(depth=depth, valid_mask=mask),
|
|
_identity(),
|
|
ExpectedGeometry("box", "rev", scene.geometry_id, scene.field_domain_id, scene.shape_hw),
|
|
)
|
|
assert frame is not None
|
|
assert not bool(np.asarray(frame.valid)[0, 0])
|
|
assert np.asarray(frame.valid).sum() == mask.sum()
|
|
|
|
|
|
def test_the_box_adapter_refuses_a_device_with_another_domain(scene):
|
|
adapter = BoxFrameAdapter(
|
|
geometry_id=scene.geometry_id,
|
|
field_domain_id=scene.field_domain_id,
|
|
shape_hw=scene.shape_hw,
|
|
)
|
|
frame = adapter.snapshot(
|
|
FakePacket(depth=np.ones(scene.shape_hw, dtype=np.float32)),
|
|
_identity(),
|
|
ExpectedGeometry("box", "rev", scene.geometry_id, scene.field_domain_id, (9, 9)),
|
|
)
|
|
assert frame is None
|
|
assert adapter.last_diagnostic is not None
|
|
assert adapter.last_diagnostic.code == "field_domain_mismatch"
|
|
|
|
|
|
def test_the_box_adapter_with_nothing_usable_says_what_it_saw(scene):
|
|
adapter = BoxFrameAdapter(
|
|
geometry_id=scene.geometry_id,
|
|
field_domain_id=scene.field_domain_id,
|
|
shape_hw=scene.shape_hw,
|
|
)
|
|
frame = adapter.snapshot(
|
|
FakePacket(depth=np.ones((5, 5), dtype=np.float32)),
|
|
_identity(),
|
|
ExpectedGeometry("box", "rev", scene.geometry_id, scene.field_domain_id, scene.shape_hw),
|
|
)
|
|
assert frame is None
|
|
assert adapter.last_diagnostic is not None
|
|
assert adapter.last_diagnostic.details["depth"] == "5x5"
|
|
|
|
|
|
def test_a_published_frame_must_not_share_memory_with_the_packet(scene):
|
|
"""The producer keeps writing its arrays; the frame must not alias them."""
|
|
adapter = BoxFrameAdapter(
|
|
geometry_id=scene.geometry_id,
|
|
field_domain_id=scene.field_domain_id,
|
|
shape_hw=scene.shape_hw,
|
|
)
|
|
depth = np.zeros(scene.shape_hw, dtype=np.float32)
|
|
frame = adapter.snapshot(
|
|
FakePacket(depth=depth),
|
|
_identity(),
|
|
ExpectedGeometry("box", "rev", scene.geometry_id, scene.field_domain_id, scene.shape_hw),
|
|
)
|
|
assert frame is not None
|
|
assert not frame.scalar.flags["WRITEABLE"]
|
|
with pytest.raises(ValueError):
|
|
np.asarray(frame.scalar)[0, 0] = 5.0
|