重构 Autocal 采集页:单一队列与控制器驱动的主按钮

选点、对准、加载与录像分散在两个面板,状态靠隐藏相位判断。现改由不依赖 Qt 的采集控制器驱动,并加入录像选项、汇总平铺副本、保持到时自动结束、会话结束回到空闲,以及无设备时的模拟采集。已运行 Autocal 非 Qt 测试并通过范围内门禁;界面测试待 Windows 复核,未做实机验收。
This commit is contained in:
ykcheungab
2026-10-18 16:04:32 +08:00
parent 85d0b396e3
commit 06e06126ca
44 changed files with 7769 additions and 2218 deletions
+153 -104
View File
@@ -243,103 +243,112 @@ def test_input_end_aborts_active_hold(trial_setup, clock):
trial.finish_session()
def test_operator_export_reproduces_uv_sites_and_rejects_edited_xyz(qt_app, flat, tmp_path):
from orisys_autocal.sensor.export_bundle import SensorBundle
from orisys_autocal.ui.sensor_panel import FingerPanel
def test_a_trial_recorded_without_video_finishes_complete(trial_setup, clock):
"""Data-only recording: the force rows are the evidence and the verdict says so."""
from orisys_autocal.recording_options import RecordingOptions
panel = FingerPanel()
bundle = SensorBundle(
path=tmp_path, vertices_mm=np.array([[2, 1, 3], [4, 1, 3], [2, 4, 3]]),
faces=np.array([[0, 1, 2]]), uv_data=np.array([[0, 0], [1, 0], [0, 1]]),
loop_to_vertex=np.array([0, 1, 2]),
)
panel.set_bundle(bundle, flat)
panel.handle_pick(2, 4)
panel.handle_pick(4, 1)
panel.table.setCurrentCell(1, 1)
panel.move_selected(-1)
destination = panel.export_sites(tmp_path / "sites.csv")
from orisys_autocal.record_overview import read_plan, plan_points
from orisys_autocal.sensor.uv_flat import mapping_sha256
definition = {"origin_raster_xy_px": uv_origin(flat), "raster_size_hw": [flat.height, flat.width],
"mapping_sha256": mapping_sha256(flat), "u_positive": "right", "v_positive": "up"}
imported = plan_points(read_plan(destination, definition), definition)
assert [point["uv_xy_px"] for point in imported] == [[1, 2], [-1, -1]]
with destination.open(encoding="utf-8-sig", newline="") as handle:
rows = list(csv.DictReader(handle))
assert [(int(r["u_px"]), int(r["v_px"])) for r in rows] == [(1, 2), (-1, -1)]
for row in rows:
site = site_at(flat, int(row["u_px"]), int(row["v_px"]))
assert site.position_sensor_mm == tuple(float(row[f"{axis}_sensor_mm"]) for axis in "xyz")
assert row["mapping_sha256"] == site.mapping_sha256
panel.table.item(0, 1).setText("999")
with pytest.raises(ValueError):
panel.export_sites(tmp_path / "invalid.csv")
assert not (tmp_path / "invalid.csv").exists()
panel.deleteLater()
def test_gui_hold_controls_and_session_lifecycle(make_window, trial_setup, clock, flat, tmp_path, qt_app):
from orisys_app_shared.i18n import t
window = make_window()
panel = window.calibration_panel
worker = Worker()
panel.source_panel.worker = worker
panel.trial.worker = worker
panel.source_panel.mode = SimpleNamespace(fps=30)
panel.source_panel.config_ref.setText(trial_setup["config_ref"])
panel.set_flat(flat, trial_setup["bundle_path"])
panel._picked(2, 1)
panel.output_path.setText(str(trial_setup["root"]))
panel.sensor_sn.setText("F-001")
assert panel.sensor_model.currentText() == "finger"
panel.levels.setText("5, 10")
panel.start_button.click()
assert panel.trial.status == "recording"
manifest = json.loads((panel.trial.session.directory / "session.json").read_text(encoding="utf-8"))
assert manifest["uv_outline"]["surface"]
assert manifest["uv_outline"]["mapping_sha256"] == manifest["uv_definition"]["mapping_sha256"]
trial = ManualTrial(worker)
directory = trial.start(**{**trial_setup, "recording": RecordingOptions(video=False)})
for index, (_branch, target) in enumerate(trial.steps):
trial.confirm(target)
# Two frames inside the second half of the hold: the summary's own rule.
# Each frame carries its own id: a repeated id has no single acquisition
# time and the review would exclude it.
for delay, frame_id in ((15, index * 2 + 2), (1, index * 2 + 3)):
clock.advance(delay)
worker.packet = replace(
worker.packet, capture_mono_ns=clock.now,
frame_id=frame_id, source_frame_id=frame_id,
)
worker.emit()
clock.advance(4)
trial.poll()
assert trial.step_index == index + 1
trial.finish()
assert trial.recorder.stop(wait=True).ok
assert trial.poll() == "complete"
assert not list(directory.glob("raw_*.mp4"))
root = json.loads(
(directory.parent.parent / "session.json").read_text(encoding="utf-8")
)
assert root["recording"] == {
"video": False, "codec": "mp4v", "lossless_frames": False}
from orisys_autocal.summary import build_summary
summary = build_summary(directory.parent.parent)
assert all(row["eligible"] for row in summary["windows"]), [
(row["excluded_reason"], row["capture_status"], row["samples_valid"])
for row in summary["windows"]
]
assert all(row["capture_status"] == "complete" for row in summary["windows"])
trial.finish_session()
def test_recording_settings_are_locked_by_the_first_trial(trial_setup, clock):
"""One session, one recording choice: the manifest must not change mid-session."""
from orisys_autocal.recording_options import RecordingOptions
worker = Worker()
trial = ManualTrial(worker)
directory = trial.start(
**{**trial_setup, "recording": RecordingOptions(video=False)}
)
root = json.loads(
(directory.parent.parent / "session.json").read_text(encoding="utf-8")
)
assert root["recording"] == {
"video": False, "codec": "mp4v", "lossless_frames": False}
worker.emit()
assert panel.views.currentWidget() is panel.source_panel
assert not panel.sensor_sn.isEnabled()
assert not panel.close_session_button.isEnabled()
for index, force in enumerate((0, 5, 10, 5, 0)):
panel.reference.setText(str(force))
panel.confirm_button.click()
assert panel.trial.holding
assert not panel.confirm_button.isEnabled()
assert not panel.finish_button.isEnabled()
assert panel.trial.step_index == index
assert t("calibration.holding").split("{")[0] in panel.target_label.text()
if index == 0:
panel.resize(1120, 740)
panel.show()
qt_app.processEvents()
assert panel.grab().save(str(tmp_path / "operator-hold.png"))
clock.advance(20)
panel._poll()
assert panel.trial.step_index == index + 1
assert panel.finish_button.isEnabled()
panel.finish_button.click()
panel.trial.recorder.stop(wait=True)
panel._poll()
assert panel.close_session_button.isEnabled()
assert not panel.sensor_sn.isEnabled()
panel.close_session_button.click()
assert panel.trial.session is None
assert panel.sensor_sn.isEnabled()
trial.finish(aborted=True, reason="test ended")
trial.recorder.stop(wait=True)
trial.poll()
with pytest.raises(ValueError, match="locked"):
trial.start(**{**trial_setup, "recording": RecordingOptions(video=True)})
# The locked settings are what a later trial of this session uses.
second = trial.start(
**{**trial_setup, "recording": RecordingOptions(video=False)}
)
assert trial.recording.video is False
assert second.parent.parent == directory.parent.parent
trial.recorder.stop(wait=True)
trial.poll()
trial.finish_session()
def test_the_capture_page_no_longer_drives_a_manual_trial(make_window):
"""D1: a new capture is a one-point queue in the ``sites/`` format.
The ManualTrial flow stays only so old ``trials/`` sessions keep reading; the
capture page owns a Campaign plus the workflow controller. The two-page
single/multi-point split it used to switch between is gone.
"""
window = make_window()
page = window.calibration_panel
assert not hasattr(page, "trial")
assert not hasattr(page, "multipoint")
assert page.campaign is not None
assert page.controller is not None
window.close()
def test_calibration_page_keeps_precise_uv_site_when_switching_to_live_view(
make_window,
def test_the_capture_page_keeps_the_picked_uv_point_while_the_live_view_is_visible(
make_window, qt_app
):
from orisys_app_shared.i18n import t
"""Both images are on the page at once; picking on the map keeps the point."""
from orisys_autocal.sensor.uv_flat import FlatSurface
window = make_window()
window.resize(1440, 900)
window.show()
qt_app.processEvents()
window.workspace.set_current_key("sensor")
qt_app.processEvents()
mask = np.full((5, 7), -1, dtype=np.int32)
mask[1:4, 2:5] = 2
positions = np.zeros((5, 7, 3), dtype=np.float64)
@@ -353,21 +362,61 @@ def test_calibration_page_keeps_precise_uv_site_when_switching_to_live_view(
uv_min=(0, 0),
uv_max=(1, 1),
)
panel = window.calibration_panel
for key, button in (
("start", panel.start_button),
("confirm", panel.confirm_button),
("finish", panel.finish_button),
("abort", panel.abort_button),
):
assert button.text() == t(f"calibration.{key}")
assert button.toolTip() == t(f"calibration.{key}_tip")
assert panel.target_label.text() == t("calibration.before_start")
panel.set_flat(flat, "bundle")
panel._picked(2, 1)
assert (panel.u_spin.value(), panel.v_spin.value()) == (-1, 2)
assert panel._site.raster_xy_px == (2, 1)
assert "2.000" in panel.position_value.text()
panel.views.setCurrentWidget(panel.source_panel)
assert panel._site.raster_xy_px == (2, 1)
page = window.calibration_panel
page.set_flat(flat, "bundle")
page._picked(2, 1)
assert (page.u_spin.value(), page.v_spin.value()) == (-1, 2)
assert page._site.raster_xy_px == (2, 1)
assert "2.000" in page.position_value.text()
# The UV map and the live 2-D image are visible in the same row: selecting one
# used to hide the other behind a tab.
assert page.flat_view.isVisible() and page.live_view.isVisible()
window.close()
def test_the_session_row_opens_the_folder_the_summary_and_the_path(
make_window, tmp_path, monkeypatch
):
"""The operator can reach the recording without copying a path by hand."""
from PySide6.QtWidgets import QApplication
from orisys_app_shared.i18n import t
from evidence_helpers import multipoint_session
from orisys_autocal.summary import build_summary
from orisys_autocal.summary_export import publish_summary
from orisys_autocal.ui import calibration_panel as panel_module
window = make_window()
panel = window.calibration_panel
opened: list = []
monkeypatch.setattr(
panel_module, "reveal", lambda path, select=None: opened.append((path, select)) or True
)
# Nothing has been recorded yet: each action is disabled and says why.
panel._refresh()
assert not panel.open_session_button.isEnabled()
assert not panel.open_summary_button.isEnabled()
assert not panel.copy_path_button.isEnabled()
assert panel.open_session_button.toolTip() == t("cap.block.no_directory")
assert panel.open_summary_button.toolTip() == t("cap.block.no_summary")
root = multipoint_session(tmp_path / "session", sites=("P1",), repeats=1, levels=(5.0,))
publish_summary(root, build_summary(root))
# The page finds the session the campaign is recording into.
panel.campaign.directory = root
panel._refresh()
assert panel._session_directory() == root
assert panel.open_session_button.isEnabled()
assert panel.open_summary_button.isEnabled()
assert panel.copy_path_button.isEnabled()
assert panel.open_session_button.toolTip() == t("cap.action.open_session")
panel.open_session_button.click()
assert opened[-1][0] == root
panel.open_summary_button.click()
assert opened[-1][0] == root / "summary"
panel.copy_path_button.click()
assert QApplication.clipboard().text() == str(root)
window.close()