added dd06 test support, organized 2d viewer and 3d viewer code organization.
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
"""Tests for Qt viewer backend force history and aspect-aware display helpers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import types
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
_REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
_EXAMPLES_DIR = _REPO_ROOT / "examples"
|
||||
if str(_EXAMPLES_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(_EXAMPLES_DIR))
|
||||
|
||||
from gui.viewer2d.backend import ( # noqa: E402
|
||||
QtViewerBackend,
|
||||
image_shape_meta,
|
||||
panel_width_for_aspect,
|
||||
scale_bgr_to_fit,
|
||||
select_display_image,
|
||||
)
|
||||
from gui.sensors import ( # noqa: E402
|
||||
FrameSnapshot,
|
||||
_snapshot_from_sensor,
|
||||
calibrate_normal_force,
|
||||
)
|
||||
|
||||
|
||||
class FakeForceAdapter:
|
||||
"""Minimal adapter that emits changing FN/FX/FY without a real sensor."""
|
||||
|
||||
def __init__(self):
|
||||
self._i = 0
|
||||
|
||||
def read(self):
|
||||
i = self._i
|
||||
self._i += 1
|
||||
return FrameSnapshot(
|
||||
image=np.zeros((100, 200, 3), dtype=np.uint8),
|
||||
normal_force=1000.0 + 50.0 * i,
|
||||
shear_x=-200.0 + 10.0 * i,
|
||||
shear_y=300.0 - 5.0 * i,
|
||||
normal_force_calibrated=calibrate_normal_force(1000.0 + 50.0 * i),
|
||||
fps=30.0,
|
||||
timestamp=float(i) / 30.0,
|
||||
flow_overlay=np.zeros((100, 200, 3), dtype=np.uint8),
|
||||
record_frame=np.zeros((100, 200, 3), dtype=np.uint8),
|
||||
)
|
||||
|
||||
|
||||
def test_backend_force_history_tracks_all_channels():
|
||||
adapter = FakeForceAdapter()
|
||||
backend = QtViewerBackend(npoints=32, lowpass_cutoff_hz=0.0)
|
||||
|
||||
for _ in range(8):
|
||||
snap = adapter.read()
|
||||
backend.ingest(snap, now=snap.timestamp)
|
||||
|
||||
state = backend.plot_state()
|
||||
assert state is not None
|
||||
assert state.sample_count == 8
|
||||
assert state.values.shape == (8, 3)
|
||||
|
||||
# All three channels must change across frames (not stay flat).
|
||||
fn_span = float(state.values[:, 0].max() - state.values[:, 0].min())
|
||||
fx_span = float(state.values[:, 1].max() - state.values[:, 1].min())
|
||||
fy_span = float(state.values[:, 2].max() - state.values[:, 2].min())
|
||||
assert fn_span > 100.0
|
||||
assert fx_span > 20.0
|
||||
assert fy_span > 10.0
|
||||
|
||||
# Raw FN is used for curves, not the tiny calibrated Newton scale.
|
||||
assert float(state.values[-1, 0]) == pytest.approx(1000.0 + 50.0 * 7)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"shape,expected_aspect",
|
||||
[
|
||||
((100, 200, 3), 2.0), # wide
|
||||
((120, 120, 3), 1.0), # square
|
||||
((200, 100, 3), 0.5), # portrait
|
||||
],
|
||||
)
|
||||
def test_backend_image_aspect_metadata(shape, expected_aspect):
|
||||
img = np.zeros(shape, dtype=np.uint8)
|
||||
h, w, aspect = image_shape_meta(img)
|
||||
assert (h, w) == (shape[0], shape[1])
|
||||
assert aspect == pytest.approx(expected_aspect)
|
||||
|
||||
snap = FrameSnapshot(flow_overlay=img, normal_force=1.0, shear_x=2.0, shear_y=3.0)
|
||||
backend = QtViewerBackend(lowpass_cutoff_hz=0.0)
|
||||
frame = backend.ingest(snap, now=0.0)
|
||||
assert frame.image_height == shape[0]
|
||||
assert frame.image_width == shape[1]
|
||||
assert frame.aspect_ratio == pytest.approx(expected_aspect)
|
||||
|
||||
fitted = scale_bgr_to_fit(img, max_w=80, max_h=80)
|
||||
fh, fw = fitted.shape[:2]
|
||||
assert fw <= 80 and fh <= 80
|
||||
assert abs((fw / fh) - expected_aspect) < 0.05
|
||||
|
||||
panel_w = panel_width_for_aspect(200, expected_aspect, min_size=160)
|
||||
assert panel_w >= 160
|
||||
assert panel_w == pytest.approx(max(160, int(round(200 * expected_aspect))))
|
||||
|
||||
|
||||
def test_select_display_image_prefers_flow_overlay():
|
||||
overlay = np.full((10, 20, 3), 7, dtype=np.uint8)
|
||||
processed = np.full((10, 20, 3), 3, dtype=np.uint8)
|
||||
snap = FrameSnapshot(flow_overlay=overlay, processed_image=processed, image=processed)
|
||||
out = select_display_image(snap)
|
||||
assert np.array_equal(out, overlay)
|
||||
|
||||
|
||||
def test_snapshot_requests_hhd_every_frame_without_motion_gate():
|
||||
"""Regression: force refresh must not use check_motion=True gating."""
|
||||
sensor = MagicMock()
|
||||
sensor.img = np.zeros((32, 32), dtype=np.uint8)
|
||||
sensor.frame = np.zeros((32, 32), dtype=np.uint8)
|
||||
sensor.info = types.SimpleNamespace(
|
||||
FPS="FPS",
|
||||
VRAW="VRAW",
|
||||
VNORMAL="VNORMAL",
|
||||
IMG="IMG",
|
||||
CONTOUR="CONTOUR",
|
||||
CENTROID="CENTROID",
|
||||
DEPTH="DEPTH",
|
||||
FNORMAL="FNORMAL",
|
||||
FSHEARX="FSHEARX",
|
||||
FSHEARY="FSHEARY",
|
||||
)
|
||||
sensor.read_info.return_value = (
|
||||
30.0,
|
||||
np.zeros((32, 32, 2), dtype=np.float32),
|
||||
np.zeros((32, 32), dtype=np.float32),
|
||||
sensor.img,
|
||||
None,
|
||||
None,
|
||||
np.zeros((32, 32), dtype=np.float32),
|
||||
12345.0,
|
||||
-100.0,
|
||||
50.0,
|
||||
)
|
||||
|
||||
# Avoid depending on orisys.util.draw_arrows implementation details.
|
||||
import gui.sensors.adapter as sa
|
||||
|
||||
original_util = sa.orisys.util
|
||||
sa.orisys.util = types.SimpleNamespace(
|
||||
draw_arrows=lambda *args, **kwargs: np.zeros((32, 32, 3), dtype=np.uint8)
|
||||
)
|
||||
try:
|
||||
snap = _snapshot_from_sensor(sensor, raw_image=sensor.frame, processed_image=sensor.img)
|
||||
finally:
|
||||
sa.orisys.util = original_util
|
||||
|
||||
sensor.compute_deformation.assert_called_once_with(decompose=True, check_motion=False)
|
||||
assert snap is not None
|
||||
assert snap.normal_force == pytest.approx(12345.0)
|
||||
assert snap.shear_x == pytest.approx(-100.0)
|
||||
assert snap.shear_y == pytest.approx(50.0)
|
||||
assert snap.normal_force_calibrated == pytest.approx(calibrate_normal_force(12345.0))
|
||||
Reference in New Issue
Block a user