Files
orisys-sdk/tests/test_qt_viewer_backend.py
T

168 lines
5.5 KiB
Python

"""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,
)
from orisys.signal_processing import calibrate_normal_force as sdk_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))
assert snap.normal_force_calibrated == pytest.approx(sdk_calibrate_normal_force(12345.0))