Unify 2D arrows on the shared painter.
Drop orisys.util.draw_arrows; box live and replay both use _paint_shared_flow_arrows with sdk_box_policy when no policy is passed. Recovered box flow uses the same inverse as finger. Verified with shared/tests/test_image_backend.py arrow/idle/recovered cases.
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user