补齐 Autocal 实时反馈与稳定窗口复核

显示实时力值、曲线及 UV 图层,并保存人工确认窗口和零载检查基准。定向测试、真实视频预览与已录会话复核闭环验证通过。
This commit is contained in:
ykcheungab
2026-10-11 18:08:09 +08:00
parent 04adf17f60
commit 34a1d9b1f1
12 changed files with 731 additions and 39 deletions
@@ -0,0 +1,85 @@
"""Live operator feedback must preserve sample validity and UV orientation."""
from dataclasses import replace
import numpy as np
from orisys_app_shared.acquisition.models import SdkPacket
class Worker:
is_running = True
def __init__(self):
self.sinks = []
def add_sink(self, sink):
self.sinks.append(sink)
def remove_sink(self, sink):
self.sinks.remove(sink)
def packet(**values):
image = np.full((64, 80, 3), (10, 20, 30), dtype=np.uint8)
base = SdkPacket(raw_frame=image.copy(), processed_frame=image,
timestamp=100, mono_t=1, fps=30, process_ms=1,
session_id=1, frame_id=1, normal_force_raw=12,
shear_x_raw=2, shear_y_raw=-3)
return replace(base, **values)
def test_force_feedback_consumes_every_sample_and_breaks_invalid_gaps(qt_app):
from orisys_autocal.ui.live_force_view import LiveForceView
worker = Worker()
view = LiveForceView(worker)
for item in (packet(), packet(frame_id=2, mono_t=1.1, stale_forces=True),
packet(frame_id=3, mono_t=1.2, normal_force_raw=20)):
worker.sinks[0](item)
view.refresh()
assert view.history.sample_count == 3
x, y = view.curves[0].getData()
np.testing.assert_allclose(x, [0, .1, .2])
assert y[0] == 12 and np.isnan(y[1]) and y[2] == 20
assert "20 raw" in view.readings.text()
assert "20.000 N" not in view.readings.text()
view.on_packet(packet(session_id=2, normal_force=4, force_calibrated=True,
calibrated_axes=("fn",)))
view.refresh()
assert view.history.sample_count == 1
assert "4.000 N" in view.readings.text()
assert "2.000 N" not in view.readings.text()
worker.is_running = False
view.refresh()
assert view.state.text()
view.shutdown()
assert not worker.sinks
def test_display_toggles_redraw_same_frame_without_mutating_measurements(qt_app, monkeypatch):
from orisys_autocal.ui import finger_source_panel
panel = finger_source_panel.FingerSourcePanel()
item = packet(flow=np.full((64, 80, 2), 2., dtype=float),
depth=np.full((64, 80), .5), valid_mask=np.ones((64, 80), bool),
flow_valid=True, flow_frame_id=1, depth_valid=True, depth_frame_id=1)
original = item.processed_frame.copy()
panel._last_packet = item
frames = []
monkeypatch.setattr(panel, "_show_frame", lambda frame: frames.append(frame.copy()))
panel._redraw()
np.testing.assert_array_equal(frames[-1], original)
panel.background_check.setChecked(False)
assert not np.array_equal(frames[-1], original)
solid = frames[-1]
panel.depth_check.setChecked(True)
assert not np.array_equal(frames[-1], solid)
depth = frames[-1]
panel.arrows_check.setChecked(True)
assert not np.array_equal(frames[-1], depth)
assert frames[-1].shape == original.shape
np.testing.assert_array_equal(item.processed_frame, original)
assert np.all(item.flow == 2)
panel._last_packet = replace(item, flow_valid=False, depth_frame_id=0)
panel._redraw()
assert panel.display_note.text()
np.testing.assert_array_equal(frames[-1], solid)
panel.shutdown()
+88
View File
@@ -122,3 +122,91 @@ def test_review_dialog_shows_saved_windows_charts_and_new_export(qt_app, session
qt_app.processEvents()
assert dialog.grab().save(str(tmp_path / "review.png"))
dialog.close()
def test_reviewed_windows_roundtrip_and_reject_changed_inputs(session):
from orisys_autocal.session_review import load_review
selections = {f"trial-001:{index}": {
"t_start_host_mono_ns": (index * 20 + 1) * 1_000_000_000,
"t_end_host_mono_ns": (index * 20 + 3) * 1_000_000_000,
"stability_verified": True,
} for index in (0, 1, 2)}
before = (session / "trials/trial-001/reference.csv").read_bytes()
result = review_session(session, selections=selections)
first = result["sites"][0]
assert first["confirmed_fit"]["gain_n_per_raw"] == pytest.approx(.5)
assert all(w["duration_s"] == 2 for w in first["windows"][:3])
saved = save_review(result)
assert load_review(session, saved)["sites"] == result["sites"]
document = json.loads(saved.read_text(encoding="utf-8"))
assert document["baselines"][0]["status"] == "unavailable"
assert document["baselines"][0]["tracker_reference"] is False
assert document["analysis_versions"]["numpy"]
assert (session / "trials/trial-001/reference.csv").read_bytes() == before
with (session / "trials/trial-001/forces.csv").open("a", encoding="utf-8") as handle:
handle.write("\n")
with pytest.raises(ValueError, match="match"):
load_review(session, saved)
with pytest.raises(ValueError, match="changed"):
save_review(result)
def test_invalid_review_selection_does_not_become_verified(session):
for start, end in ((0, 2), (1, 1), (1, 12), (3, 4)):
with pytest.raises(ValueError):
review_session(session, selections={"trial-001:0": {
"t_start_host_mono_ns": start * 1_000_000_000,
"t_end_host_mono_ns": end * 1_000_000_000,
"stability_verified": True,
}})
def test_operator_confirms_windows_in_dialog_and_reloads_them(qt_app, session, monkeypatch):
from orisys_autocal.ui import session_review_dialog
dialog = session_review_dialog.SessionReviewDialog(session)
for row in (0, 1):
dialog.table.selectRow(row)
dialog.end_s.setValue(2)
dialog.verified.setChecked(True)
dialog.apply_button.click()
assert dialog.review["sites"][0]["windows"][row]["stability_verified"]
assert dialog.review["sites"][0]["confirmed_fit"] is not None
dialog.save_button.click()
saved = next(session.glob("derived/*/review.json"))
fresh = session_review_dialog.SessionReviewDialog(session)
monkeypatch.setattr(session_review_dialog.QFileDialog, "getOpenFileName",
lambda *args: (str(saved), ""))
fresh.load_button.click()
assert fresh.review["sites"] == dialog.review["sites"]
dialog.close()
fresh.close()
def test_baseline_image_matches_first_reviewed_zero_frame(session):
import cv2
import numpy as np
path = session / "trials/trial-001"
frames = list(csv.DictReader((path / "frames.csv").open(encoding="utf-8")))
writer = cv2.VideoWriter(str(path / "raw_0001.mp4"), cv2.VideoWriter_fourcc(*"mp4v"),
30, (32, 24))
assert writer.isOpened()
try:
for index, row in enumerate(frames):
writer.write(np.full((24, 32, 3), 20 if index == 0 else 220, np.uint8))
row.update(segment=1, segment_frame=index + 1, in_video=1)
finally:
writer.release()
_csv(path / "frames.csv", frames)
result = review_session(session, selections={"trial-001:0": {
"t_start_host_mono_ns": 1_000_000_000,
"t_end_host_mono_ns": 3_000_000_000, "stability_verified": True,
}})
saved = save_review(result)
document = json.loads(saved.read_text(encoding="utf-8"))
baseline = document["baselines"][0]
assert baseline["status"] == "saved" and baseline["segment_frame"] == 1
image = cv2.imread(str(saved.parent / baseline["image"]))
assert 10 < image.mean() < 30