重构 Autocal 采集页:单一队列与控制器驱动的主按钮
选点、对准、加载与录像分散在两个面板,状态靠隐藏相位判断。现改由不依赖 Qt 的采集控制器驱动,并加入录像选项、汇总平铺副本、保持到时自动结束、会话结束回到空闲,以及无设备时的模拟采集。已运行 Autocal 非 Qt 测试并通过范围内门禁;界面测试待 Windows 复核,未做实机验收。
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user