feat(autocal): 添加标定记录总览与测点计划对照

集中显示采集覆盖、加载卸载响应与已确认法向灵敏度,并保存计划和复核快照。通过44项相关测试、已有视频会话检查及代码和文档检查。
This commit is contained in:
ykcheungab
2026-10-11 19:03:40 +08:00
parent 161e64278b
commit b200f3d55c
10 changed files with 663 additions and 11 deletions
+7
View File
@@ -238,6 +238,13 @@ def test_operator_export_reproduces_uv_sites_and_rejects_edited_xyz(qt_app, flat
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)]
+173
View File
@@ -0,0 +1,173 @@
"""Recorded coverage, plan identity and the normal-response operator journey."""
import csv
import json
from copy import deepcopy
import pytest
from PySide6.QtWidgets import QPushButton
from test_session_review import repeated_session as repeated_session, session as session
from orisys_autocal.calibration_session import write_json
from orisys_autocal.record_overview import load_overview, overview_rows, read_plan, save_overview
from orisys_autocal.session_review import review_session
@pytest.fixture
def overview_session(repeated_session):
root, selections = repeated_session
manifest = json.loads((root / "session.json").read_text(encoding="utf-8"))
definition = {"mapping_sha256": "test-map", "origin_raster_xy_px": [5, 8],
"raster_size_hw": [12, 12], "u_positive": "right", "v_positive": "up", "unit": "px"}
manifest["uv_definition"] = definition
for index, site in enumerate(manifest["sites"]):
site["position_sensor_mm"] = [index + 1., 2., 3.]
for trial in manifest["trials"]:
if trial["status"] == "complete":
path = root / trial["directory"] / "session.json"
metadata = json.loads(path.read_text(encoding="utf-8"))
metadata["uv_definition"] = definition
write_json(path, metadata)
trial["plan"] = {"steps": list(range(5))}
manifest["sites"].append({"site_id": "site-003", "uv_xy_px": [3, 2], "position_sensor_mm": [3., 2., 3.]})
manifest["trials"].append({"trial_id": "trial-005", "site_id": "site-003", "status": "incomplete"})
write_json(root / "session.json", manifest)
rows = []
for order, (u, v), position in ((1, (-3, 4), [2., 2., 3.]),
(2, (1, -2), [1., 2., 3.]),
(3, (-1, 1), [4., 2., 3.])):
rows.append({"order": order, "site_id": f"site-{order:03d}", "u_px": u, "v_px": v,
"x_sensor_mm": position[0], "y_sensor_mm": position[1], "z_sensor_mm": position[2],
"origin_raster_x_px": 5, "origin_raster_y_px": 8,
"raster_width_px": 12, "raster_height_px": 12, "mapping_sha256": "test-map",
"u_positive": "right", "v_positive": "up"})
plan_path = root / "operator-plan.csv"
with plan_path.open("w", newline="", encoding="utf-8-sig") as handle:
writer = csv.DictWriter(handle, fieldnames=list(rows[0]))
writer.writeheader()
writer.writerows(rows)
return root, selections, plan_path
def test_overview_distinguishes_coverage_without_matching_plan_ids(overview_session):
root, selections, plan_path = overview_session
review = review_session(root, selections=selections)
plan = read_plan(plan_path, review["uv_definition"])
rows = overview_rows(review, plan)
assert [r["site_id"] for r in rows] == ["site-002", "site-001", None, "site-003"]
assert [r["status"] for r in rows] == ["reviewed", "reviewed", "uncollected", "incomplete"]
assert rows[1]["complete_trials"] == 2 and rows[1]["fit_trials"] == 2
assert rows[1]["confirmed_windows"] == 10 and rows[1]["fit_range_n"] == [0, 10]
assert rows[2]["fit"] is None and rows[3]["windows"] == 0
assert overview_rows(review_session(root), plan)[0]["status"] == "pending"
saved = save_overview(review, plan)
restored, stored_plan = load_overview(root, saved)
assert overview_rows(restored, stored_plan) == rows
assert stored_plan["sha256"] == plan["sha256"]
@pytest.mark.parametrize("key,value", [("mapping_sha256", "other"), ("origin_raster_x_px", 6),
("v_positive", "down"), ("u_px", 999), ("x_sensor_mm", "nan")])
def test_plan_refuses_mapping_or_coordinate_mismatch(overview_session, key, value):
root, selections, plan_path = overview_session
review = review_session(root, selections=selections)
plan = read_plan(plan_path, review["uv_definition"])
plan["rows"][0][key] = value
with pytest.raises(ValueError):
overview_rows(review, plan)
def test_plan_merges_repeated_uv_points_and_refuses_conflicting_positions(overview_session):
root, selections, plan_path = overview_session
review = review_session(root, selections=selections)
plan = read_plan(plan_path, review["uv_definition"])
plan["rows"].append({**plan["rows"][0], "order": "4"})
assert overview_rows(review, plan)[0]["orders"] == [1, 4]
plan["rows"][0]["x_sensor_mm"] = "9"
with pytest.raises(ValueError):
overview_rows(review, plan)
plan["rows"].pop()
with pytest.raises(ValueError, match="3D"):
overview_rows(review, plan)
def test_overview_requires_all_planned_hold_windows_for_reviewed_status(overview_session):
root, selections, _ = overview_session
review = review_session(root, selections=selections)
altered = deepcopy(review)
altered["trials"][0]["plan"]["steps"].append(5)
assert overview_rows(altered)[0]["status"] == "pending"
assert overview_rows(review)[0]["status"] == "reviewed"
def test_overview_gui_plan_click_trial_filter_and_saved_roundtrip(qt_app, overview_session, monkeypatch, tmp_path):
from orisys_autocal.ui import record_overview_dialog
from orisys_autocal.ui.session_review_dialog import SessionReviewDialog
root, selections, plan_path = overview_session
dialog = record_overview_dialog.RecordOverviewDialog(root)
dialog.review = review_session(root, selections=selections)
dialog._refresh()
monkeypatch.setattr(record_overview_dialog.QFileDialog, "getOpenFileName", lambda *args: (str(plan_path), ""))
dialog.findChild(QPushButton, "overview_import_plan").click()
assert dialog.table.rowCount() == 4
assert dialog.table.item(0, 1).text() == "site-002"
dialog.uv_chart.series()[1].clicked.emit(dialog.uv_chart.series()[1].at(0))
assert dialog.table.currentRow() == 1
assert len(dialog.uv_chart.axes()) == 2
assert all(len(s.attachedAxes()) == 2 for s in dialog.uv_chart.series())
assert dialog.trials.count() == 3
dialog.trials.setCurrentIndex(1)
names = [s.name() for s in dialog.response_chart.series()]
assert any("trial-001" in name for name in names)
assert not any("trial-004" in name for name in names)
assert len(dialog.response_chart.axes()) == 2
assert all(len(s.attachedAxes()) == 2 for s in dialog.response_chart.series())
load_series = next(s for s in dialog.response_chart.series() if s.count() == 2 and "trial-001" in s.name())
assert [(load_series.at(i).x(), load_series.at(i).y()) for i in range(2)] == [(5, 11), (10, 21)]
opened = []
monkeypatch.setattr(SessionReviewDialog, "exec", lambda child: opened.append(child))
dialog.details_button.click()
assert opened[0].sites.currentIndex() == 0
assert opened[0].review["selections"] == selections
dialog.table.selectRow(2)
assert not dialog.details_button.isEnabled()
dialog.table.selectRow(3)
dialog.trials.setCurrentIndex(1)
assert not dialog.response_chart.series()
assert not dialog.details_button.isEnabled()
dialog.table.selectRow(2)
dialog.map_mode.setCurrentIndex(1)
assert len(dialog.uv_chart.series()) == 4
assert dialog.uv_chart.series()[0].color().name() != "#929aa7"
assert dialog.uv_chart.series()[2].color().name() == "#929aa7"
assert dialog.uv_chart.series()[3].color().name() == "#929aa7"
dialog.show()
qt_app.processEvents()
assert dialog.grab().save(str(tmp_path / "overview-sensitivity.png"))
dialog.table.selectRow(1)
dialog.map_mode.setCurrentIndex(0)
qt_app.processEvents()
assert dialog.grab().save(str(tmp_path / "overview-response.png"))
dialog.findChild(QPushButton, "overview_save").click()
saved = next(root.glob("derived/*/review.json"))
dialog.findChild(QPushButton, "overview_clear_plan").click()
assert dialog.table.rowCount() == 3
monkeypatch.setattr(record_overview_dialog.QFileDialog, "getOpenFileName", lambda *args: (str(saved), ""))
dialog.findChild(QPushButton, "overview_load").click()
assert dialog.table.rowCount() == 4
dialog.close()
def test_calibration_review_action_opens_record_overview(make_window, overview_session, monkeypatch):
from orisys_autocal.ui.record_overview_dialog import RecordOverviewDialog
from orisys_autocal.ui.calibration_panel import QFileDialog
root, _, _ = overview_session
opened = []
monkeypatch.setattr(QFileDialog, "getExistingDirectory", lambda *args: str(root))
monkeypatch.setattr(RecordOverviewDialog, "exec", lambda dialog: opened.append(dialog))
window = make_window()
window.calibration_panel.review_button.click()
assert len(opened) == 1 and len(opened[0].rows) == 3
window.close()