diff --git a/apps/studio/src/orisys_studio/replay_analysis.py b/apps/studio/src/orisys_studio/replay_analysis.py index 4b4786d..badd464 100644 --- a/apps/studio/src/orisys_studio/replay_analysis.py +++ b/apps/studio/src/orisys_studio/replay_analysis.py @@ -294,11 +294,10 @@ class AnalysisSession: the pass's own answer — the field is finite only at those cells, and :meth:`serve_policy` refuses a display setting that moves the grid — so the grid travels from the result's provenance. *Which* candidates are active is a display - question for the shared finger painter, and ``magnitude`` is stored precisely so it - can be asked again later. A box result on the live painter's grid is different: the - 2D canvas calls ``orisys.util.draw_arrows``, which does not read the display - threshold, so the on/off floors stay at that painter's values (2 / 1.5 px). The 3D - overlay uses this same policy so the two figures zero at the same strength. + question for the shared painter, which re-selects the recovered field. A box + result keeps the box grid's on/off floors (2 / 1.5 px) rather than the operator's + finger thresholds, so live and replay zero at the same strength. The 3D overlay + uses this same policy. Without this, a display setting that moves the grid would be refused by ``serve_policy`` and then applied to the drawing anyway, re-selecting the recovered @@ -541,12 +540,7 @@ class AnalysisSession: The shared painter re-selects this field, so the inverse divides by that product and the cap stays idempotent: the stored offsets come back exactly. At ``flow_arrow_scale == 1`` that divisor is ``DISPLACEMENT_PX_PER_FLOW`` times - the margin. - - The box painter does not re-select. It colours by the displacement *before* - the cap, which ``magnitude`` still holds (``|DISPLACEMENT_PX_PER_FLOW * flow|``). - Direction comes from ``dx``/``dy``. Float32 magnitude is not exact enough to - reproduce a capped offset, which is why only the box grid uses it. + the margin. Box and finger use the same inverse; only the policy numbers differ. """ rows = np.asarray(samples.rows, np.int64) cols = np.asarray(samples.cols, np.int64) @@ -563,25 +557,16 @@ class AnalysisSession: # ``NaN`` is the point of the mask, and the selector's tip arithmetic on a # discarded cell is defined to be discarded: say so here rather than let # numpy warn about an invalid cast on every frame. - if uses_sdk_box_grid(self.policy): - magnitude = np.asarray(samples.magnitude, np.float64) - length = np.sqrt(dx * dx + dy * dy) - flow_mag = magnitude / DISPLACEMENT_PX_PER_FLOW - safe = np.maximum(length, 1e-12) - moving = length > 0.0 - flow[rows, cols, 0] = np.where(moving, dx / safe * flow_mag, 0.0) - flow[rows, cols, 1] = np.where(moving, dy / safe * flow_mag, 0.0) - else: - scale = float(self.policy.flow_arrow_scale) - if scale <= 0.0: - scale = 1.0 - divisor = ( - DISPLACEMENT_PX_PER_FLOW - * float(self.policy.flow_length_margin) - / scale - ) - flow[rows, cols, 0] = dx / divisor - flow[rows, cols, 1] = dy / divisor + scale = float(self.policy.flow_arrow_scale) + if scale <= 0.0: + scale = 1.0 + divisor = ( + DISPLACEMENT_PX_PER_FLOW + * float(self.policy.flow_length_margin) + / scale + ) + flow[rows, cols, 0] = dx / divisor + flow[rows, cols, 1] = dy / divisor return flow diff --git a/apps/studio/tests/test_bare_video_analysis.py b/apps/studio/tests/test_bare_video_analysis.py index 084274c..ae1b1bd 100644 --- a/apps/studio/tests/test_bare_video_analysis.py +++ b/apps/studio/tests/test_bare_video_analysis.py @@ -17,8 +17,8 @@ Where a failure would be invisible: reference frame are read back from the dialog's own widgets; * a recomputed force trace and a recorded one are different claims, so the charts' source kind and the provenance's ``sources``/``synthetic`` are asserted, not just the pixels; -* a box result is recorded on the live painter's grid, so the canvas is compared with - ``draw_arrows`` on the recovered field once its gaps are filled with zero. +* a box result is recorded on the box sample grid, so the canvas is compared with + ``draw_arrow_display`` on the recovered field. The fixture clip is the project's recorded box footage under ``dist/``, copied into the test's own directory. If it is absent the test **fails** (``_require_clip``) rather than skipping: a @@ -433,10 +433,10 @@ def test_a_bare_video_is_analysed_and_scrubbed_in_both_directions( def test_the_box_arrow_canvas_is_painted_by_the_live_painter( bare_clip, window, monkeypatch ): - """A box pass stores the live painter's grid, so the window draws ``draw_arrows``. + """A box pass stores the box sample grid; the panel redraws with that policy. - The recovered field is still NaN off the recorded cells. Filling those gaps with zero - is what the live painter reads as "no motion", and that picture is the panel. + The recovered field is NaN off the recorded cells. The shared selector only samples + finite cells, so the panel matches ``draw_arrow_display`` on that sparse field. """ open_bare_video(window, bare_clip, monkeypatch) settle(window) @@ -451,7 +451,6 @@ def test_the_box_arrow_canvas_is_painted_by_the_live_painter( "draw_style": window._draw_style(), "colormap": flow_colormap(window._theme), "policy": policy, - "shared_painter": False, } set_mode(window, ARROW_PURE) @@ -460,15 +459,14 @@ def test_the_box_arrow_canvas_is_painted_by_the_live_painter( QApplication.processEvents() shown = panel_bgr(window) assert painted(shown, canvas) > 0, "the operator's arrow canvas is blank" - filled = np.nan_to_num(np.asarray(first.flow, dtype=np.float64), nan=0.0) expected = draw_arrow_display( first.base_bgr, - filled, + first.flow, valid_mask=first.valid_mask, **common, ) assert np.array_equal(shown, np.asarray(expected)), ( - "the panel's canvas is not the live painter's output for this frame" + "the panel's canvas is not the shared painter's output for this frame" ) diff --git a/apps/studio/tests/test_replay_analysis.py b/apps/studio/tests/test_replay_analysis.py index b6fc594..cfb8ed8 100644 --- a/apps/studio/tests/test_replay_analysis.py +++ b/apps/studio/tests/test_replay_analysis.py @@ -738,11 +738,11 @@ def test_the_recovered_field_reselects_to_the_stored_samples(session): def test_box_live_and_replay_arrow_canvases_do_not_diverge(box_pass, box_session, monkeypatch): - """Live ``draw_arrows`` on the dense field equals the replay canvas, frame for frame. + """Live dense field equals the replay canvas, frame for frame. - This is the discrepancy the box arrow work exists to close: the operator's live view - paints the chain's dense flow, and the replay paints a field recovered from the - stored samples. Both go through :func:`render_image` with ``draw_style="box"``. + The operator's live view paints the chain's dense flow, and the replay paints a + field recovered from the stored samples. Both go through :func:`render_image` with + ``draw_style="box"`` and the shared painter. """ service, outcome = box_pass assert outcome.complete, outcome.reason diff --git a/shared/src/orisys_app_shared/acquisition/arrows.py b/shared/src/orisys_app_shared/acquisition/arrows.py index 1b03063..152e033 100644 --- a/shared/src/orisys_app_shared/acquisition/arrows.py +++ b/shared/src/orisys_app_shared/acquisition/arrows.py @@ -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 diff --git a/shared/src/orisys_app_shared/acquisition/image_backend.py b/shared/src/orisys_app_shared/acquisition/image_backend.py index 04051db..14ad1ec 100644 --- a/shared/src/orisys_app_shared/acquisition/image_backend.py +++ b/shared/src/orisys_app_shared/acquisition/image_backend.py @@ -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), diff --git a/shared/src/orisys_app_shared/analysis/service.py b/shared/src/orisys_app_shared/analysis/service.py index 98bdadd..d18b34e 100644 --- a/shared/src/orisys_app_shared/analysis/service.py +++ b/shared/src/orisys_app_shared/analysis/service.py @@ -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 diff --git a/shared/tests/test_image_backend.py b/shared/tests/test_image_backend.py index 713f85a..49c40df 100644 --- a/shared/tests/test_image_backend.py +++ b/shared/tests/test_image_backend.py @@ -582,28 +582,25 @@ def test_the_idle_dot_switch_hides_the_dots_and_leaves_the_arrows_alone(): def test_the_idle_marker_is_the_marker_a_box_sample_gets(): - """One marker shape and one tint across sensor types. + """One marker shape and one tint across sensor styles. - Box and Mini draw through the SDK's ``orisys.util.draw_arrows``, which paints a - below-threshold sample with ``cv2.circle(radius=1, thickness=-1)`` in the same - ``below_threshold_color`` this module passes it. The finger canvas has to put the same - blob on the same kind of location, or a quiet sample would look like two different - things depending on the sensor; this compares the two paths' pixels for one - below-threshold sample. + Box and finger both use the shared per-sample painter. With the same policy they + must put the same plus-shaped idle blob on the same quiet sample. """ - pytest.importorskip("orisys", reason="the SDK path needs the SDK") from orisys_app_shared.acquisition.arrows import FLOW_IDLE_HEX, hex_to_bgr from orisys_app_shared.scene3d.visual_policy import VisualPolicy - # One candidate location, shared by both paths: the box draw's own grid is 20 px with - # a 15 px edge margin, so a 60x60 frame has exactly the (30, 30) sample. + # One candidate location on a 20 px grid with a 15 px edge: a 60x60 frame has + # exactly the (30, 30) sample inside the margin. size = 60 flow = np.zeros((size, size, 2), dtype=np.float64) - flow[30, 30] = (0.1, 0.0) # 0.2 px: under both paths' thresholds + flow[30, 30] = (0.1, 0.0) # 0.2 px: under the threshold canvas = _flat_canvas(size, size) policy = VisualPolicy(flow_on_px=0.4, flow_grid_spacing=20, flow_edge_margin=15) - box = draw_arrow_display(canvas, flow, background="captured", draw_style="box") + box = draw_arrow_display( + canvas, flow, background="captured", draw_style="box", policy=policy + ) finger = draw_arrow_display( canvas, flow, background="captured", draw_style="finger", policy=policy ) @@ -970,19 +967,17 @@ def _painted(image, canvas=(0x16, 0x13, 0x12)) -> int: def test_a_recovered_field_is_drawn_by_the_shared_painter_whatever_the_style_is(): - """Where a frame came from decides the painter, not which sensor's style is selected. + """Live and analysis sources use the same painter on a sparse recovered field. - The SDK painter walks the dense sampling grid, so on a field that is finite only at a - stored result's candidate cells it finds no sample at all and paints nothing: measured - on a real box recording, **0 px** against the shared painter's **1,486 px** on the same - frame. So the routing is by ``ImageFrame.source`` — the frame came from a result — and - the box *style* still owns the magnification and the idle-dot tint. + The field is finite only at the stored candidate cells. The shared selector walks + those cells, so both sources paint the same picture under the same policy. """ from orisys_app_shared.scene3d.visual_policy import select_flow_samples sparse, samples = _recovered_flow() base = np.zeros((60, 80, 3), dtype=np.uint8) base[:, :] = (0x16, 0x13, 0x12) + policy = _policy() result_frame = backend.ImageFrame( base, flow=sparse, source=backend.SOURCE_ANALYSIS, flow_valid=True @@ -991,7 +986,7 @@ def test_a_recovered_field_is_drawn_by_the_shared_painter_whatever_the_style_is( base, flow=sparse, source=backend.SOURCE_LIVE, flow_valid=True ) drawn = backend.render_image( - result_frame, mode=backend.ARROW_PURE, draw_style="box", arrow_policy=_policy() + result_frame, mode=backend.ARROW_PURE, draw_style="box", arrow_policy=policy ) expected = draw_arrow_display( base, @@ -1001,30 +996,27 @@ def test_a_recovered_field_is_drawn_by_the_shared_painter_whatever_the_style_is( # The backend hands the theme's ramp to the painter, so the reference call has to # as well; the default is ``cv2.COLORMAP_JET``, which is a different picture. colormap=flow_colormap("dark"), - policy=_policy(), - shared_painter=True, + policy=policy, ) assert np.array_equal(np.asarray(drawn), np.asarray(expected)), ( "a result's canvas is not the shared painter's output" ) assert _painted(drawn) > 0, "the recovered field painted nothing" - # The same array handed over as a *live* field keeps the SDK path — which is what the - # sensor family's draw style has always meant, and which paints nothing here. - sdk = backend.render_image( - live_frame, mode=backend.ARROW_PURE, draw_style="box", arrow_policy=_policy() + live = backend.render_image( + live_frame, mode=backend.ARROW_PURE, draw_style="box", arrow_policy=policy + ) + assert np.array_equal(np.asarray(drawn), np.asarray(live)), ( + "live and analysis disagree on the same sparse field" ) - assert _painted(sdk) == 0, "the SDK painter drew a recovered field" - assert not np.array_equal(np.asarray(drawn), np.asarray(sdk)) # One marker per recorded cell at most: the recovered field carries no other value, so # nothing outside those cells can become a sample. - assert select_flow_samples(sparse, None, _policy()).rows.size == samples.rows.size + assert select_flow_samples(sparse, None, policy).rows.size == samples.rows.size def test_a_box_result_on_the_sdk_grid_matches_the_live_painter(): - """A box pass stores the live painter's grid, so filling the gaps and calling it - back draws the same picture as the dense field.""" + """A box pass stores the box grid; the idempotent inverse redraws the dense field.""" from orisys_app_shared.acquisition.arrows import sdk_box_policy from orisys_app_shared.scene3d.visual_policy import ( DISPLACEMENT_PX_PER_FLOW, @@ -1039,13 +1031,13 @@ def test_a_box_result_on_the_sdk_grid_matches_the_live_painter(): assert samples.rows.size > 0 dx = np.asarray(samples.dx, np.float64) dy = np.asarray(samples.dy, np.float64) - length = np.sqrt(dx * dx + dy * dy) - flow_mag = np.asarray(samples.magnitude, np.float64) / DISPLACEMENT_PX_PER_FLOW - safe = np.maximum(length, 1e-12) - moving = length > 0.0 + scale = float(policy.flow_arrow_scale) or 1.0 + divisor = ( + DISPLACEMENT_PX_PER_FLOW * float(policy.flow_length_margin) / scale + ) sparse = np.full(dense.shape, np.nan, dtype=np.float64) - sparse[samples.rows, samples.cols, 0] = np.where(moving, dx / safe * flow_mag, 0.0) - sparse[samples.rows, samples.cols, 1] = np.where(moving, dy / safe * flow_mag, 0.0) + sparse[samples.rows, samples.cols, 0] = dx / divisor + sparse[samples.rows, samples.cols, 1] = dy / divisor base = np.zeros(dense.shape[:2] + (3,), np.uint8) base[:] = (0x16, 0x13, 0x12) cmap = flow_colormap("dark") @@ -1056,6 +1048,7 @@ def test_a_box_result_on_the_sdk_grid_matches_the_live_painter(): background="pure_color", canvas_bgr=(0x16, 0x13, 0x12), colormap=cmap, + policy=policy, ) frame = backend.ImageFrame( base,