修复回放标定选择与曲线显示数据保留
让标定选择进入处理和缓存,避免显示设置清空曲线。102 项针对性检查通过,全量门禁仍有停止时序、模型尺寸与原生崩溃问题。
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
"""Display controls retain measurements and redraw the same source timeline."""
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
from orisys_app_shared.force.session import SessionSample, SessionStore
|
||||
from orisys_app_shared.force.source import StoreCurveSource
|
||||
|
||||
from orisys_studio.ui.force_plots import ForcePlotsPanel
|
||||
|
||||
|
||||
def _store(*, partial=False):
|
||||
store = SessionStore()
|
||||
for index in range(21):
|
||||
raw = float(index + 100)
|
||||
shear = float("nan") if partial else raw / 100
|
||||
store.append(SessionSample(
|
||||
frame_id=index, t_rel_s=float(index),
|
||||
fn_raw=raw, fx_raw=-raw, fy_raw=raw,
|
||||
fn_n=raw / 100, fx_n=shear, fy_n=shear,
|
||||
fn_raw_processed=raw, fx_raw_processed=-raw, fy_raw_processed=raw,
|
||||
fn_n_processed=raw / 100, fx_n_processed=shear, fy_n_processed=shear,
|
||||
))
|
||||
return store
|
||||
|
||||
|
||||
def _curve(panel, axis=0):
|
||||
x, y = panel.curves[axis].getData()
|
||||
if x is None:
|
||||
return np.empty(0), np.empty(0)
|
||||
return np.asarray(x, dtype=float), np.asarray(y, dtype=float)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("browse", [False, True])
|
||||
def test_y_limit_settings_keep_trace_window_cursor_and_store(make_window, browse):
|
||||
window = make_window(units="raw", plot_window_sec=4.0)
|
||||
store = _store()
|
||||
window.force_plots.set_store(store)
|
||||
if browse:
|
||||
window.force_plots.set_window(5.0, 12.0)
|
||||
window.force_plots.set_cursor(8.0)
|
||||
before = tuple(array.copy() for array in _curve(window.force_plots))
|
||||
x_range = window.force_plots.plots[0].viewRange()[0]
|
||||
# No acquisition tick follows: an idle or paused view must redraw immediately.
|
||||
window.display_ctl.apply_plot_settings({"fn_raw_max": 2000.0})
|
||||
for actual, expected in zip(_curve(window.force_plots), before):
|
||||
np.testing.assert_array_equal(actual, expected)
|
||||
assert window.force_plots.plots[0].viewRange()[0] == pytest.approx(x_range)
|
||||
assert window.force_plots.cursor_time() == 8.0
|
||||
assert len(store.visible(0.0, 20.0).times) == 21
|
||||
|
||||
|
||||
@pytest.mark.parametrize("kind", ["memory", "disk", "analysis"])
|
||||
def test_window_and_unit_changes_preserve_partial_axes_and_session_time(qt_app, kind):
|
||||
store = _store(partial=True)
|
||||
panel = ForcePlotsPanel()
|
||||
try:
|
||||
source = StoreCurveSource(store)
|
||||
source.kind = kind
|
||||
panel.set_curve_source(source)
|
||||
for unit in ("N", "raw", "N"):
|
||||
panel.set_units(unit)
|
||||
for seconds in (4.0, 8.0, 8.0):
|
||||
panel.set_window_sec(seconds)
|
||||
x, y = _curve(panel)
|
||||
assert len(x) > 1
|
||||
assert x[-1] == 20.0
|
||||
assert x[0] == 20.0 - seconds
|
||||
assert y[-1] == pytest.approx(1.2 if unit == "N" else 120.0)
|
||||
assert panel.plots[0].viewRange()[0][1] == pytest.approx(20.0)
|
||||
if unit == "N":
|
||||
assert np.isnan(_curve(panel, 1)[1]).all()
|
||||
assert np.isnan(_curve(panel, 2)[1]).all()
|
||||
assert len(store.visible(0.0, 20.0).times) == 21
|
||||
finally:
|
||||
panel.close()
|
||||
panel.deleteLater()
|
||||
|
||||
|
||||
def test_back_to_latest_reloads_the_live_tail_without_duplicate_samples(qt_app):
|
||||
panel = ForcePlotsPanel()
|
||||
try:
|
||||
panel.set_store(_store())
|
||||
for _ in range(2):
|
||||
panel.set_window(2.0, 7.0)
|
||||
panel.follow_latest()
|
||||
x, y = _curve(panel)
|
||||
np.testing.assert_array_equal(x, np.arange(16.0, 21.0))
|
||||
np.testing.assert_array_equal(y, np.arange(116.0, 121.0))
|
||||
panel.reset()
|
||||
assert _curve(panel)[0].size == 0
|
||||
finally:
|
||||
panel.close()
|
||||
panel.deleteLater()
|
||||
|
||||
|
||||
def test_auto_y_range_keeps_replayed_data_visible_after_unit_switch(qt_app):
|
||||
panel = ForcePlotsPanel()
|
||||
try:
|
||||
panel.set_store(_store())
|
||||
panel.show_all()
|
||||
panel.set_axis_limits(
|
||||
fn_min=0, fn_max=20, shear_max=20,
|
||||
fn_raw_min=0, fn_raw_max=0, shear_raw_max=0,
|
||||
)
|
||||
panel.set_units("N")
|
||||
panel.set_units("raw")
|
||||
qt_app.processEvents()
|
||||
x, y = _curve(panel)
|
||||
assert x.size == 21
|
||||
low, high = panel.plots[0].viewRange()[1]
|
||||
assert low <= y.min() <= y.max() <= high
|
||||
finally:
|
||||
panel.close()
|
||||
panel.deleteLater()
|
||||
@@ -0,0 +1,173 @@
|
||||
"""A recording's analysis choices reach processing, cache identity and curves."""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
from orisys_app_shared.analysis.bare_video import BareVideoSetup
|
||||
from orisys_app_shared.analysis.service import AnalysisService, ChainSpec
|
||||
from orisys_app_shared.i18n import t
|
||||
from PySide6.QtWidgets import QDialog
|
||||
|
||||
from orisys_studio import app_window, replay_controller
|
||||
|
||||
|
||||
def _calibration(tmp_path):
|
||||
from orisys.calibration import load
|
||||
|
||||
path = tmp_path / "chosen.json"
|
||||
path.write_text(json.dumps(load(sensor_type="box").to_dict()), encoding="utf-8")
|
||||
return path
|
||||
|
||||
|
||||
@pytest.mark.parametrize("calibrated", [True, False])
|
||||
def test_recording_dialog_keeps_all_choices_for_its_own_recording(
|
||||
make_window, replay_recording, tmp_path, monkeypatch, calibrated,
|
||||
):
|
||||
calibration = _calibration(tmp_path) if calibrated else None
|
||||
setup = BareVideoSetup(
|
||||
profile_ref="shipped/box", calibration=calibration, reference_frame=12,
|
||||
device_id="confirmed" if calibrated else None,
|
||||
unloaded_reference_frames=30 if calibrated else 0,
|
||||
)
|
||||
monkeypatch.setattr(app_window.VideoAnalysisDialog, "exec", lambda self: QDialog.Accepted)
|
||||
monkeypatch.setattr(app_window.VideoAnalysisDialog, "choice", lambda self: setup)
|
||||
window = make_window()
|
||||
before = (Path(replay_recording) / "session.json").read_bytes()
|
||||
assert window._recording_analysis_overrides(replay_recording)
|
||||
service = window._analysis_service(replay_recording)
|
||||
assert service.chain.calibration == calibration
|
||||
assert service.chain.device_id == setup.device_id
|
||||
assert service.chain.calibration_disabled is (not calibrated)
|
||||
assert service.reference_frame == 12
|
||||
assert service.unloaded_reference_frames == setup.unloaded_reference_frames
|
||||
assert service.store_force
|
||||
# Another recording must not inherit this one's reference or model.
|
||||
other = tmp_path / "other"
|
||||
other.mkdir()
|
||||
(other / "session.json").write_bytes(before)
|
||||
untouched = window._analysis_service(other)
|
||||
assert untouched.chain.calibration is None
|
||||
assert untouched.reference_frame is None
|
||||
assert not untouched.store_force
|
||||
assert (Path(replay_recording) / "session.json").read_bytes() == before
|
||||
|
||||
|
||||
def test_chosen_calibration_content_and_force_channel_change_cache_key(replay_recording, tmp_path):
|
||||
calibration = _calibration(tmp_path)
|
||||
chain = ChainSpec(sensor_type="box", calibration=calibration)
|
||||
service = AnalysisService(replay_recording, chain=chain, store_force=True)
|
||||
original = service.dependency_report()["key"].digest
|
||||
data = json.loads(calibration.read_text(encoding="utf-8"))
|
||||
data["meta"] = {**data.get("meta", {}), "note": "changed coefficients file"}
|
||||
calibration.write_text(json.dumps(data), encoding="utf-8")
|
||||
assert service.dependency_report()["key"].digest != original
|
||||
recorded = AnalysisService(replay_recording, chain=chain, store_force=False)
|
||||
assert recorded.dependency_report()["key"].digest != service.dependency_report()["key"].digest
|
||||
|
||||
|
||||
def test_reference_selector_uses_recorded_ids_and_times(qt_app, tmp_path):
|
||||
from orisys_app_shared.analysis.bare_video import VideoFacts
|
||||
from orisys_app_shared.sensors import scan_sensor_catalog
|
||||
|
||||
dialog = app_window.VideoAnalysisDialog(
|
||||
video=tmp_path / "clip.mp4",
|
||||
facts=VideoFacts(tmp_path / "clip.mp4", 33, 30.0, 640, 480),
|
||||
catalog=scan_sensor_catalog(),
|
||||
)
|
||||
try:
|
||||
assert dialog.select_profile("shipped/box")
|
||||
dialog.select_calibration(None)
|
||||
dialog.set_reference_times({20: 0.0, 36: .9, 52: 1.9})
|
||||
assert dialog.reference_spin.minimum() == 20
|
||||
assert dialog.reference_spin.maximum() == 52
|
||||
assert dialog.choice().reference_frame is None
|
||||
dialog.reference_spin.setValue(36)
|
||||
assert dialog.choice().reference_frame == 36
|
||||
assert dialog.reference_time_label.text() == t("video_analysis.reference_time", seconds="0.900")
|
||||
dialog.reference_spin.setValue(35)
|
||||
assert dialog.choice() is None
|
||||
assert dialog.note_label.text() == t("video_analysis.reference_missing", frame=35)
|
||||
finally:
|
||||
dialog.close()
|
||||
dialog.deleteLater()
|
||||
|
||||
|
||||
def test_recalibrated_recording_uses_recomputed_trace_through_display_controls(
|
||||
make_window, replay_recording, monkeypatch,
|
||||
):
|
||||
from orisys_app_shared.force.session import SessionSample, SessionStore
|
||||
from orisys_app_shared.force.source import StoreCurveSource
|
||||
|
||||
window = make_window(units="raw")
|
||||
assert window.replay.open_recording(replay_recording)
|
||||
session = SimpleNamespace(reader=SimpleNamespace(
|
||||
provenance=SimpleNamespace(sources={"forces": "recomputed"}), meta={},
|
||||
))
|
||||
store = SessionStore()
|
||||
for index in range(3):
|
||||
store.append(SessionSample(
|
||||
frame_id=index, t_rel_s=float(index), fn_raw=100.0 + index,
|
||||
fn_raw_processed=100.0 + index,
|
||||
))
|
||||
source = StoreCurveSource(store)
|
||||
source.kind = "analysis"
|
||||
source.reader = session.reader
|
||||
source.unit_available = lambda unit: True
|
||||
monkeypatch.setattr(replay_controller, "AnalysisCurveSource", lambda reader: source)
|
||||
monkeypatch.setattr(window.replay_ctl, "available_result", lambda: session)
|
||||
files = [Path(replay_recording) / name for name in ("forces.csv", "frames.csv", "session.json")]
|
||||
before = [hashlib.sha256(path.read_bytes()).hexdigest() for path in files]
|
||||
assert window.replay_ctl._serve_curves(session) == "recomputed"
|
||||
assert window.force_plots.curve_kind() == "analysis"
|
||||
assert window.force_plots.mode_label.text() == t("chart.mode_recomputed")
|
||||
window.display_ctl.apply_plot_settings({"fn_raw_max": 2000.0})
|
||||
window.replay_ctl.apply_source(True)
|
||||
assert window.force_plots.curve_kind() == "analysis"
|
||||
window._reset_display()
|
||||
assert window.force_plots.curve_kind() == "analysis"
|
||||
assert [hashlib.sha256(path.read_bytes()).hexdigest() for path in files] == before
|
||||
|
||||
|
||||
def test_accepting_setup_then_cancelling_keeps_the_last_complete_result(
|
||||
make_window, replay_recording, monkeypatch,
|
||||
):
|
||||
window = make_window()
|
||||
assert window.replay.open_recording(replay_recording)
|
||||
previous = SimpleNamespace(recording=Path(replay_recording), close=lambda: None)
|
||||
window.replay_ctl.results.open_session(previous)
|
||||
monkeypatch.setattr(window, "_recording_analysis_overrides", lambda recording: True)
|
||||
monkeypatch.setattr(window.replay_ctl, "refresh_state", lambda: None)
|
||||
monkeypatch.setattr(window.replay_ctl, "render_frame", lambda: None)
|
||||
requested = []
|
||||
monkeypatch.setattr(window.replay_ctl, "on_analysis_requested", lambda: requested.append(True))
|
||||
window.replay_ctl.on_analysis_setup()
|
||||
assert requested == [True]
|
||||
assert window.replay_ctl.available_result() is previous
|
||||
window.replay_ctl.on_analysis_finished(SimpleNamespace(cancelled=True, frames_written=0))
|
||||
assert window.replay_ctl.available_result() is previous
|
||||
|
||||
|
||||
@pytest.mark.parametrize("family", ["box", "finger"])
|
||||
def test_explicit_raw_choice_disables_default_calibration_before_processing(monkeypatch, tmp_path, family):
|
||||
from orisys_app_shared.analysis import service as service_module
|
||||
import orisys.finger
|
||||
|
||||
applied = []
|
||||
sensor = SimpleNamespace(
|
||||
cap=None, _read_frame=lambda: (False, None), get_img=lambda **kwargs: None,
|
||||
set_calibration=applied.append,
|
||||
)
|
||||
runtime = SimpleNamespace(
|
||||
tactile=sensor, capture=None, read_source=lambda: (False, None),
|
||||
process_frame=lambda frame: None, set_calibration=applied.append,
|
||||
)
|
||||
monkeypatch.setattr(service_module, "_import_sdk", lambda: SimpleNamespace(Sensor=lambda *args, **kwargs: sensor))
|
||||
monkeypatch.setattr(orisys.finger, "FingerRuntime", SimpleNamespace(open=lambda **kwargs: runtime))
|
||||
video = tmp_path / "clip.mp4"
|
||||
video.touch()
|
||||
chain = ChainSpec(sensor_type=family, calibration_disabled=True)
|
||||
service_module._OpenChain(chain, video, tmp_path).open()
|
||||
assert applied == [None]
|
||||
Reference in New Issue
Block a user