Record the live box arrow grid so replay matches live paint
Box analysis used the finger sample grid, so replay packed arrows into the contact zone. The pass now stores draw_arrows cells; stale box caches miss and re-run. Verified with shared and Studio tests, including a live-vs-replay canvas check.
This commit is contained in:
@@ -919,3 +919,144 @@ def test_resetting_the_latch_clears_the_session_boundary():
|
||||
assert np.array_equal(latch.update(other), other), (
|
||||
"a new session kept the previous session's island"
|
||||
)
|
||||
|
||||
|
||||
# --------------------------------------------- a field recovered from a stored result
|
||||
def _policy():
|
||||
from orisys_app_shared.scene3d.visual_policy import VisualPolicy
|
||||
|
||||
return VisualPolicy(flow_grid_spacing=10, max_arrows=400).normalised()
|
||||
|
||||
|
||||
def _recovered_flow(height: int = 60, width: int = 80):
|
||||
"""A flow field as an analysis result hands one over: finite only at its own cells.
|
||||
|
||||
This is what the replay's reconstruction produces — the pass recorded candidate
|
||||
*locations* and their displacements, and the field is rebuilt with those values at
|
||||
those cells and ``NaN`` everywhere else. A dense field is the opposite case, and the
|
||||
difference between them is what the painter has to be chosen by.
|
||||
"""
|
||||
from orisys_app_shared.scene3d.visual_policy import select_flow_samples
|
||||
|
||||
dense = np.zeros((height, width, 2), dtype=np.float64)
|
||||
dense[..., 0] = 1.5
|
||||
dense[..., 1] = -0.75
|
||||
samples = select_flow_samples(dense, None, _policy())
|
||||
sparse = np.full((height, width, 2), np.nan, dtype=np.float64)
|
||||
sparse[samples.rows, samples.cols, 0] = samples.dx
|
||||
sparse[samples.rows, samples.cols, 1] = samples.dy
|
||||
return sparse, samples
|
||||
|
||||
|
||||
def _painted(image, canvas=(0x16, 0x13, 0x12)) -> int:
|
||||
"""Pixels the painter changed from the canvas colour."""
|
||||
array = np.asarray(image)
|
||||
return int(
|
||||
np.count_nonzero(np.any(array != np.asarray(canvas, np.uint8), axis=-1))
|
||||
)
|
||||
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
"""
|
||||
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)
|
||||
|
||||
result_frame = backend.ImageFrame(
|
||||
base, flow=sparse, source=backend.SOURCE_ANALYSIS, flow_valid=True
|
||||
)
|
||||
live_frame = backend.ImageFrame(
|
||||
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()
|
||||
)
|
||||
expected = draw_arrow_display(
|
||||
base,
|
||||
sparse,
|
||||
background="pure_color",
|
||||
draw_style="box",
|
||||
# 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,
|
||||
)
|
||||
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()
|
||||
)
|
||||
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
|
||||
|
||||
|
||||
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."""
|
||||
from orisys_app_shared.acquisition.arrows import sdk_box_policy
|
||||
from orisys_app_shared.scene3d.visual_policy import (
|
||||
DISPLACEMENT_PX_PER_FLOW,
|
||||
select_flow_samples,
|
||||
)
|
||||
|
||||
policy = sdk_box_policy()
|
||||
dense = np.zeros((80, 100, 2), dtype=np.float64)
|
||||
dense[20:60, 20:80, 0] = 1.5
|
||||
dense[30:40, 30:50, 0] = 8.0
|
||||
samples = select_flow_samples(dense, None, policy)
|
||||
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
|
||||
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)
|
||||
base = np.zeros(dense.shape[:2] + (3,), np.uint8)
|
||||
base[:] = (0x16, 0x13, 0x12)
|
||||
cmap = flow_colormap("dark")
|
||||
live = draw_arrow_display(
|
||||
base,
|
||||
dense,
|
||||
draw_style="box",
|
||||
background="pure_color",
|
||||
canvas_bgr=(0x16, 0x13, 0x12),
|
||||
colormap=cmap,
|
||||
)
|
||||
frame = backend.ImageFrame(
|
||||
base,
|
||||
flow=sparse,
|
||||
source=backend.SOURCE_ANALYSIS,
|
||||
flow_valid=True,
|
||||
result_available=True,
|
||||
)
|
||||
drawn = backend.render_image(
|
||||
frame,
|
||||
mode=backend.ARROW_PURE,
|
||||
draw_style="box",
|
||||
arrow_policy=policy,
|
||||
canvas_bgr=(0x16, 0x13, 0x12),
|
||||
)
|
||||
assert np.array_equal(np.asarray(drawn), np.asarray(live))
|
||||
assert _painted(drawn) > 0
|
||||
|
||||
Reference in New Issue
Block a user