diff --git a/apps/studio/src/orisys_studio/app_window.py b/apps/studio/src/orisys_studio/app_window.py index b534741..6d63437 100644 --- a/apps/studio/src/orisys_studio/app_window.py +++ b/apps/studio/src/orisys_studio/app_window.py @@ -1734,16 +1734,15 @@ class StudioMainWindow(QMainWindow): return wanted def _arrow_policy(self): - """The one flow-arrow policy the 2D canvas and the 3D surface both sample with. + """Use the box recording grid or the finger's shared 2D/3D display policy. - The thresholds and the sample grid are display settings (the pane's), so the - policy is built from them here and handed to the 2D renderer; the 3D composer - builds the same value from the same settings object - (``scene3d.composer`` via ``visual_policy.policy_for_settings``). That is what - makes the two figures trigger at the same strength: previously each path built - its own policy *and* the 3D one applied a hysteresis the canvas did not, so the - same frame could carry arrows in one figure and none in the other. + Box and mini analysis store samples on the SDK box grid; live rendering must + use that same grid. Finger keeps one policy for its 2D and 3D views. """ + if self._draw_style() != "finger": + from orisys_app_shared.acquisition.arrows import sdk_box_policy + + return sdk_box_policy() from orisys_app_shared.scene3d.visual_policy import policy_for_settings return policy_for_settings(self.mesh3d_panel.display_settings()) diff --git a/apps/studio/tests/test_ui_smoke.py b/apps/studio/tests/test_ui_smoke.py index fd51bb5..bffad05 100644 --- a/apps/studio/tests/test_ui_smoke.py +++ b/apps/studio/tests/test_ui_smoke.py @@ -2424,6 +2424,15 @@ def test_the_two_figures_are_drawn_from_one_arrow_policy(make_window, monkeypatc window.close() +@pytest.mark.parametrize("sensor_type", ["box", "mini"]) +def test_box_family_live_uses_the_recorded_sample_grid(make_window, sensor_type): + from orisys_app_shared.acquisition.arrows import sdk_box_policy + + window = _show(make_window(sensor_type=sensor_type), (1440, 880)) + assert window._arrow_policy() == sdk_box_policy() + window.close() + + def test_a_bare_video_plays_even_though_it_is_not_a_recording( make_window, record_root, decline_video_prompt ): diff --git a/shared/src/orisys_app_shared/acquisition/arrows.py b/shared/src/orisys_app_shared/acquisition/arrows.py index 6e5821a..857fd4a 100644 --- a/shared/src/orisys_app_shared/acquisition/arrows.py +++ b/shared/src/orisys_app_shared/acquisition/arrows.py @@ -23,7 +23,7 @@ FLOW_IDLE_HEX = "#8A93A6" #: BGR grey used for depth samples that carry no value. NO_DEPTH_BGR = (0x8A, 0x8A, 0x8A) -#: The fixed flow-magnitude colour range, in **uv pixels of displacement**. +#: Finger's fixed flow-magnitude colour range, in **uv pixels of displacement**. #: #: The colour of an arrow is a statement about its magnitude, so it has to be the same #: statement from one frame to the next: a per-frame range makes the same magnitude a @@ -34,6 +34,8 @@ NO_DEPTH_BGR = (0x8A, 0x8A, 0x8A) #: where the arrow itself does. A pane that offers the operator a colour range owns this #: value and passes it as ``magnitude_range`` to :func:`draw_arrow_display`. FLOW_MAGNITUDE_RANGE: tuple[float, float] = (0.0, 2.0) +#: Box/Mini use a 2 px activation floor, so their fixed range extends to 8 px. +BOX_FLOW_MAGNITUDE_RANGE: tuple[float, float] = (0.0, 8.0) #: Whether an inactive sample is drawn as a quiet dot. **On**, which is the decided #: display behaviour: a sample below the display threshold is a statement about the @@ -498,8 +500,10 @@ def draw_arrow_display( 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 - settings model that owns the range passes its own here. ``per_frame_colour`` opts + (uv pixels of displacement). The default is :data:`FLOW_MAGNITUDE_RANGE` for + finger and :data:`BOX_FLOW_MAGNITUDE_RANGE` for box/mini; their different + sampling floors need different fixed ranges to retain intensity contrast. + A settings model that owns the range passes its own here. ``per_frame_colour`` opts back in to normalising every frame against its own maximum, which makes one magnitude a different colour from one frame to the next. @@ -570,7 +574,11 @@ def draw_arrow_display( policy=selected, colormap=colormap, below_threshold_color=tuple(draw_kw["below_threshold_color"]), - magnitude_range=magnitude_range, + magnitude_range=( + magnitude_range + if magnitude_range is not None + else FLOW_MAGNITUDE_RANGE if finger else BOX_FLOW_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) diff --git a/shared/tests/test_image_backend.py b/shared/tests/test_image_backend.py index 92bbb66..e63fbd3 100644 --- a/shared/tests/test_image_backend.py +++ b/shared/tests/test_image_backend.py @@ -1080,3 +1080,22 @@ def test_a_box_result_on_the_sdk_grid_matches_the_live_painter(): ) assert np.array_equal(np.asarray(drawn), np.asarray(live)) assert _painted(drawn) > 0 + + +@pytest.mark.parametrize("draw_style", ["box", "mini"]) +def test_box_family_keeps_sparse_idle_dots_and_distinct_motion_colours(draw_style): + from orisys_app_shared.acquisition.arrows import FLOW_IDLE_HEX, hex_to_bgr, sdk_box_policy + + policy = sdk_box_policy() + base = np.zeros((100, 120, 3), np.uint8) + flow = np.zeros((100, 120, 2), np.float32) + flow[50, 50, 0] = 1.25 # 2.5 px displacement, just over the box floor. + flow[50, 90, 0] = 3.0 # 6 px displacement on the same frame. + drawn = draw_arrow_display( + base, flow, draw_style=draw_style, policy=policy, + colormap=flow_colormap("dark"), + ) + tint = np.array(hex_to_bgr(FLOW_IDLE_HEX), np.uint8) + assert _marker_is_there(drawn, 30, 30, tint) + assert not _marker_is_there(drawn, 50, 50, tint) + assert not np.array_equal(drawn[50, 50], drawn[50, 90])