Add object calibration

This commit is contained in:
Ayzen
2026-10-05 17:39:40 +03:00
parent 8f097dfa2e
commit 552cdc688a
33 changed files with 3141 additions and 517 deletions
@@ -0,0 +1,183 @@
"""GUI coordination for importing and estimating optional object calibration."""
from __future__ import annotations
import queue
import threading
from pathlib import Path
from PyQt6.QtCore import QObject, QTimer
from PyQt6.QtWidgets import QFileDialog
from python_app.gui.object_calibration_dialog import ObjectCalibrationDialog
def _calculate(groups, config, settings, cancelled, messages, result) -> None:
"""The worker handles only Python data; every Qt action stays on the GUI thread."""
try:
from python_app.workflows.mimo_calibration import Settings, compute_from_collections
calibration = compute_from_collections(
groups, config, settings=Settings(**settings),
progress=messages.put, cancelled=cancelled.is_set,
)
result.put(("cancelled", None) if cancelled.is_set() else ("ready", calibration))
except InterruptedError:
result.put(("cancelled", None))
except Exception as exc: # noqa: BLE001 - report worker failures on the GUI thread
result.put(("error", exc))
class ObjectCalibrationController(QObject):
"""Own one modeless editor and cancellable calculation for an AppWindow."""
def __init__(self, owner) -> None:
super().__init__(owner)
self._owner = owner
self._dialog: ObjectCalibrationDialog | None = None
self._capture = None
self._worker: threading.Thread | None = None
self._cancel = threading.Event()
self._messages: queue.Queue = queue.Queue()
self._result: queue.Queue = queue.Queue(maxsize=1)
self._poll = QTimer(self)
self._poll.setInterval(100)
self._poll.timeout.connect(self._poll_calculation)
self._save_name = ""
def import_calibration(self) -> None:
"""Validate and copy a legacy cal_*.npy folder into the app's own store."""
if getattr(self._owner, "_closing", False):
return
if getattr(self._owner, "_object_capture_job", None) is not None or self._worker is not None:
self._owner._show_error("Wait for the object-calibration operation to finish")
return
panel = self._owner._ensure_preprocess_dialog()
name = panel.set_name()
if not name:
self._owner._show_error("Set name is required")
return
directory = QFileDialog.getExistingDirectory(panel, "Import MIMO calibration")
if not directory:
return
try:
from python_app.workflows.mimo_calibration import load_calibration, validate_for_config
config = self._owner._build_config()
calibration = load_calibration(Path(directory))
validate_for_config(calibration, config.to_dict())
radar_key = self._owner._radar_key(config)
self._owner._store.save_object_calibration(radar_key, name, calibration)
self._select_saved(name)
panel.set_status(f"Object calibration imported: {name}. Applies on the next Start.")
except Exception as exc: # noqa: BLE001
self._owner._show_exception("Failed to import object calibration", exc)
def open_creation(self) -> None:
if getattr(self._owner, "_closing", False):
return
if self._dialog is None:
self._dialog = ObjectCalibrationDialog(self._owner._ensure_preprocess_dialog().set_name(), self._owner)
self._dialog.capture_requested.connect(self._capture_data)
self._dialog.calculate_requested.connect(self._start_calculation)
self._dialog.cancel_requested.connect(self.cancel)
self._dialog.show()
self._dialog.raise_()
self._dialog.activateWindow()
def _capture_data(self) -> None:
if (getattr(self._owner, "_closing", False) or self._capture is not None
or self._worker is not None or self._dialog is None):
return
from python_app.gui.controllers.object_calibration_capture import capture_object_sweeps
try:
self._capture = capture_object_sweeps(self._owner, self._dialog)
self._capture.finished.connect(self._capture_finished)
except Exception as exc: # noqa: BLE001
self._dialog.set_capture_pending(False)
self._dialog.set_status(str(exc))
self._owner._show_exception("Failed to capture object-calibration data", exc)
def _capture_finished(self) -> None:
self._capture = None
def _start_calculation(self) -> None:
if getattr(self._owner, "_closing", False) or self._worker is not None or self._capture is not None or self._dialog is None:
return
try:
name = self._dialog.set_name()
if not name:
raise ValueError("Set name is required")
groups, config, settings = self._dialog.calculation_inputs()
self._save_name = name
self._cancel = threading.Event()
self._messages = queue.Queue()
self._result = queue.Queue(maxsize=1)
self._worker = threading.Thread(
target=_calculate,
args=(groups, config, settings, self._cancel, self._messages, self._result),
name="object-calibration", daemon=True,
)
self._dialog.set_calculating(True)
self._dialog.set_status("Расчёт калибровки…")
self._worker.start()
self._poll.start()
except Exception as exc: # noqa: BLE001
self._worker = None
self._dialog.set_calculating(False)
self._dialog.set_status(f"Ошибка расчёта: {exc}")
self._owner._show_exception("Failed to calculate object calibration", exc)
def _poll_calculation(self) -> None:
if self._dialog is None or getattr(self._owner, "_closing", False):
self._poll.stop()
return
while not self._messages.empty():
self._dialog.set_status(self._messages.get_nowait())
try:
state, payload = self._result.get_nowait()
except queue.Empty:
return
self._poll.stop()
self._worker = None
self._dialog.set_calculating(False)
if self._cancel.is_set() or state == "cancelled":
self._dialog.set_status("Расчёт отменён. Снятые данные сохранены в этом окне.")
return
if state == "error":
self._dialog.set_status(f"Ошибка расчёта: {payload}")
self._owner._show_exception("Failed to calculate object calibration", payload)
return
try:
from python_app.workflows.mimo_calibration import validate_for_config
current_config = self._owner._build_config()
validate_for_config(payload, current_config.to_dict())
radar_key = self._owner._radar_key(current_config)
self._owner._store.save_object_calibration(radar_key, self._save_name, payload)
self._select_saved(self._save_name)
self._owner._ensure_preprocess_dialog().set_set_name(self._save_name)
self._dialog.show_calibration(payload)
self._dialog.set_status(f"Калибровка сохранена: {self._save_name}. Применится при следующем Start.")
except Exception as exc: # noqa: BLE001
self._dialog.set_status(f"Калибровка не сохранена: {exc}")
self._owner._show_exception("Failed to save object calibration", exc)
def _select_saved(self, name: str) -> None:
self._owner._selected_preprocess_sets["s21_object_calibration"] = name
self._owner._refresh_sets()
self._owner._log(f"Selected object calibration: {name}")
def cancel(self) -> None:
self._cancel.set()
if self._capture is not None:
self._capture.cancel()
if self._worker is not None and self._dialog is not None:
self._dialog.set_status("Отмена расчёта…")
def shutdown(self) -> None:
self.cancel()
self._poll.stop()
if self._dialog is not None:
self._dialog.close()