Rework radar system
This commit is contained in:
@@ -7,11 +7,11 @@ import math
|
||||
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
from PyQt6.QtCore import Qt, pyqtSignal
|
||||
from PyQt6.QtCore import QSignalBlocker, QRectF, Qt, pyqtSignal
|
||||
from PyQt6.QtWidgets import (
|
||||
QCheckBox, QComboBox, QDialog, QDoubleSpinBox, QFormLayout, QGroupBox,
|
||||
QHBoxLayout, QHeaderView, QLabel, QLineEdit, QPushButton, QTableWidget, QTableWidgetItem,
|
||||
QVBoxLayout, QWidget,
|
||||
QToolButton, QVBoxLayout, QWidget,
|
||||
)
|
||||
|
||||
from python_app.models.dataset_model import SweepCollection
|
||||
@@ -30,6 +30,7 @@ class ObjectCalibrationDialog(QDialog):
|
||||
self.resize(1000, 850)
|
||||
self._captures: list[dict] = []
|
||||
self._config: dict | None = None
|
||||
self._provenance: dict | None = None
|
||||
self._capture_pending = self._calculating = False
|
||||
self._calibration: dict | None = None
|
||||
layout = QVBoxLayout(self)
|
||||
@@ -39,26 +40,26 @@ class ObjectCalibrationDialog(QDialog):
|
||||
name_row.addWidget(self._set_name_input)
|
||||
layout.addLayout(name_row)
|
||||
explanation = QLabel(
|
||||
"Снимите данные, затем введите физически измеренные координаты центра отражателя: "
|
||||
"X — поперёк, Y — высота над землёй, Z — вдоль земли. "
|
||||
"Обычные S21-калибровка и выбранный референс уже применены; "
|
||||
"существующая MIMO-поправка при съёмке отключена.", self,
|
||||
"Capture data, then enter the measured reflector centre: "
|
||||
"X across the array, Y above the ground, Z along the ground. "
|
||||
"S21 through calibration and the selected reference are already applied; "
|
||||
"object calibration is disabled during capture. Only antenna pairs with configured "
|
||||
"Tx/Rx geometry are calibrated; other acquired pairs remain unchanged.", self,
|
||||
)
|
||||
explanation.setWordWrap(True)
|
||||
layout.addWidget(explanation)
|
||||
self._table = QTableWidget(0, 5, self)
|
||||
self._table.setHorizontalHeaderLabels(["Набор", "Свипов", "X (м)", "Y (м)", "Z (м)"])
|
||||
self._table.setHorizontalHeaderLabels(["Target", "Sweeps", "X (m)", "Y (m)", "Z (m)"])
|
||||
horizontal, vertical = self._table.horizontalHeader(), self._table.verticalHeader()
|
||||
assert horizontal is not None and vertical is not None
|
||||
horizontal.setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
|
||||
vertical.hide()
|
||||
self._table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
|
||||
self._table.itemChanged.connect(self._measurements_changed)
|
||||
layout.addWidget(self._table)
|
||||
actions = QHBoxLayout()
|
||||
self._capture_button = QPushButton("Снять данные", self)
|
||||
self._capture_button = QPushButton("Capture data", self)
|
||||
self._capture_button.clicked.connect(self.capture_requested.emit)
|
||||
self._remove_button = QPushButton("Удалить выбранный набор", self)
|
||||
self._remove_button = QPushButton("Remove selected target", self)
|
||||
self._remove_button.clicked.connect(self._remove_capture)
|
||||
actions.addWidget(self._capture_button)
|
||||
actions.addWidget(self._remove_button)
|
||||
@@ -67,81 +68,109 @@ class ObjectCalibrationDialog(QDialog):
|
||||
self._settings: dict[str, QDoubleSpinBox | QCheckBox | QComboBox] = {}
|
||||
form = QFormLayout()
|
||||
for key, title, default, low, high in (
|
||||
("sphere_radius_m", "Радиус шара, м (0 — точка)", .16, 0, 100),
|
||||
("antenna_height_m", "Высота начала координат антенн над землёй, м", .92, 0, 100),
|
||||
("pitch_deg", "Наклон радара вниз, °", 10, -90, 90),
|
||||
("sphere_radius_m", "Sphere radius, m (0 = point)", .16, 0, 100),
|
||||
("antenna_height_m", "Antenna origin height above ground, m", .92, 0, 100),
|
||||
("pitch_deg", "Radar downward tilt, °", 10, -90, 90),
|
||||
):
|
||||
form.addRow(title, self._number(key, default, low, high))
|
||||
layout.addLayout(form)
|
||||
advanced = QGroupBox("Параметры расчёта", self)
|
||||
advanced.setFlat(True)
|
||||
advanced.setCheckable(True)
|
||||
advanced.setChecked(False)
|
||||
advanced_layout = QVBoxLayout(advanced)
|
||||
advanced_content = QWidget(advanced)
|
||||
advanced_form = QFormLayout(advanced_content)
|
||||
# A disclosure header: only the header shows until the operator opens it.
|
||||
advanced_toggle = QToolButton(self)
|
||||
advanced_toggle.setText("Calculation settings")
|
||||
advanced_toggle.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
|
||||
advanced_toggle.setArrowType(Qt.ArrowType.RightArrow)
|
||||
advanced_toggle.setAutoRaise(True)
|
||||
advanced = QGroupBox(self)
|
||||
advanced_form = QFormLayout(advanced)
|
||||
for key, title, default, low, high in (
|
||||
("f_min", "Нижняя частота оценки, МГц", 2000, 0, 100000),
|
||||
("f_max", "Верхняя частота оценки, МГц", 6000, 0, 100000),
|
||||
("gate_half_m", "Полуширина окна эха по пути, м", .15, .000001, 100),
|
||||
("gate_taper_m", "Плавный край окна, м", .03, .000001, 100),
|
||||
("coarse_range_search_m", "Грубый поиск по пути, ±м (0 — выкл.)", .6, 0, 100),
|
||||
("refine_range_m", "Уточнение координат, ±м", .1, 0, 100),
|
||||
("f_min", "Estimation band start, MHz", 2000, 0, 100000),
|
||||
("f_max", "Estimation band stop, MHz", 6000, 0, 100000),
|
||||
("gate_half_m", "Echo path gate half-width, m", .15, .000001, 100),
|
||||
("gate_taper_m", "Gate taper width, m", .03, .000001, 100),
|
||||
("coarse_range_search_m", "Coarse path search, ±m (0 = off)", .6, 0, 100),
|
||||
("refine_range_m", "Coordinate refinement, ±m", .1, 0, 100),
|
||||
):
|
||||
advanced_form.addRow(title, self._number(key, default, low, high))
|
||||
axes = QWidget(advanced_content)
|
||||
axes = QWidget(advanced)
|
||||
axes_layout = QHBoxLayout(axes)
|
||||
axes_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self._refine_axes = {}
|
||||
for axis in "xyz":
|
||||
checkbox = QCheckBox(axis.upper(), axes)
|
||||
checkbox.setChecked(True)
|
||||
checkbox.toggled.connect(self._measurements_changed)
|
||||
self._refine_axes[axis] = checkbox
|
||||
axes_layout.addWidget(checkbox)
|
||||
advanced_form.addRow("Уточнять координаты", axes)
|
||||
phase_model = QComboBox(advanced_content)
|
||||
axes_layout.addStretch(1)
|
||||
advanced_form.addRow("Refine coordinates", axes)
|
||||
phase_model = QComboBox(advanced)
|
||||
phase_model.addItems(["const", "linear", "poly3", "raw"])
|
||||
phase_model.setCurrentText("linear")
|
||||
phase_model.currentTextChanged.connect(self._measurements_changed)
|
||||
self._settings["phase_model"] = phase_model
|
||||
advanced_form.addRow("Модель фазы антенны", phase_model)
|
||||
amplitude = QCheckBox("Выравнивать также амплитуды", advanced_content)
|
||||
amplitude.toggled.connect(self._measurements_changed)
|
||||
advanced_form.addRow("Antenna phase model", phase_model)
|
||||
amplitude = QCheckBox("Equalize amplitudes", advanced)
|
||||
self._settings["apply_amplitude"] = amplitude
|
||||
advanced_form.addRow(amplitude)
|
||||
advanced_content.hide()
|
||||
advanced.toggled.connect(advanced_content.setVisible)
|
||||
advanced_layout.addWidget(advanced_content)
|
||||
advanced.hide()
|
||||
|
||||
def toggle_advanced() -> None:
|
||||
shown = advanced.isHidden()
|
||||
advanced_toggle.setArrowType(Qt.ArrowType.DownArrow if shown else Qt.ArrowType.RightArrow)
|
||||
advanced.setVisible(shown)
|
||||
|
||||
advanced_toggle.clicked.connect(toggle_advanced)
|
||||
layout.addWidget(advanced_toggle)
|
||||
layout.addWidget(advanced)
|
||||
self._target_combo = QComboBox(self)
|
||||
self._target_combo.currentIndexChanged.connect(self._draw_phase_effect)
|
||||
layout.addWidget(self._target_combo)
|
||||
self._phase_plot = pg.PlotWidget(background="#0f141c", enableMenu=False)
|
||||
self._phase_plot.setLabel("left", "Target phase relative to first pair", units="deg")
|
||||
self._phase_plot.setLabel("bottom", "Tx / Rx pair")
|
||||
self._phase_plot.showGrid(x=True, y=True, alpha=.2)
|
||||
self._phase_plot.setMinimumHeight(220)
|
||||
self._phase_plot.addLegend()
|
||||
layout.addWidget(self._phase_plot)
|
||||
self._quality_label = QLabel("Фазы вкладов пар появятся после расчёта.", self)
|
||||
self._phase_view = pg.GraphicsLayoutWidget(parent=self)
|
||||
self._phase_view.setBackground("#0f141c")
|
||||
self._phase_view.setMinimumHeight(270)
|
||||
self._phase_plots = []
|
||||
self._phase_images = []
|
||||
self._phase_text = []
|
||||
for column, title in enumerate(("Before", "After")):
|
||||
plot = self._phase_view.ci.addPlot(row=0, col=column, title=title, enableMenu=False)
|
||||
plot.setLabel("bottom", "Rx")
|
||||
plot.setLabel("left", "Tx")
|
||||
plot.getViewBox().setAspectLocked(True)
|
||||
plot.getViewBox().invertY(True)
|
||||
plot.getViewBox().setMouseEnabled(x=False, y=False)
|
||||
image = pg.ImageItem(axisOrder="row-major")
|
||||
plot.addItem(image)
|
||||
self._phase_plots.append(plot)
|
||||
self._phase_images.append(image)
|
||||
self._phase_colorbar = pg.ColorBarItem(
|
||||
values=(-180, 180), colorMap=pg.colormap.get("CET-C2"),
|
||||
label="Phase (deg)", interactive=False, colorMapMenu=False,
|
||||
)
|
||||
self._phase_colorbar.setImageItem(self._phase_images)
|
||||
self._phase_colorbar.axis.setTicks([[(value, str(value)) for value in (-180, -90, 0, 90, 180)]])
|
||||
self._phase_view.ci.addItem(self._phase_colorbar, row=0, col=2)
|
||||
layout.addWidget(self._phase_view)
|
||||
self._quality_label = QLabel("Pair phases at the target will appear after calculation.", self)
|
||||
self._quality_label.setWordWrap(True)
|
||||
layout.addWidget(self._quality_label)
|
||||
self._status_label = QLabel("Готово к съёмке", self)
|
||||
self._status_label = QLabel("Ready to capture", self)
|
||||
self._status_label.setWordWrap(True)
|
||||
layout.addWidget(self._status_label)
|
||||
footer = QHBoxLayout()
|
||||
self._calculate_button = QPushButton("Рассчитать и сохранить", self)
|
||||
self._calculate_button = QPushButton("Calculate and save", self)
|
||||
self._calculate_button.clicked.connect(self.calculate_requested.emit)
|
||||
self._cancel_button = QPushButton("Отменить операцию", self)
|
||||
self._cancel_button = QPushButton("Cancel operation", self)
|
||||
self._cancel_button.clicked.connect(self.cancel_requested.emit)
|
||||
close_button = QPushButton("Закрыть", self)
|
||||
close_button = QPushButton("Close", self)
|
||||
close_button.clicked.connect(self.reject)
|
||||
footer.addWidget(self._calculate_button)
|
||||
footer.addWidget(self._cancel_button)
|
||||
footer.addStretch(1)
|
||||
footer.addWidget(close_button)
|
||||
layout.addLayout(footer)
|
||||
self._table.itemChanged.connect(self._measurements_changed)
|
||||
self._target_combo.currentIndexChanged.connect(self._draw_phase_effect)
|
||||
for field in [*self._settings.values(), *self._refine_axes.values()]:
|
||||
signal = (field.valueChanged if isinstance(field, QDoubleSpinBox) else
|
||||
field.toggled if isinstance(field, QCheckBox) else field.currentTextChanged)
|
||||
signal.connect(self._measurements_changed)
|
||||
self._update_actions()
|
||||
|
||||
def _number(self, key: str, value: float, low: float, high: float) -> QDoubleSpinBox:
|
||||
@@ -150,35 +179,40 @@ class ObjectCalibrationDialog(QDialog):
|
||||
field.setRange(low, high)
|
||||
field.setSingleStep(100 if key in {"f_min", "f_max"} else .1 if key == "pitch_deg" else .01)
|
||||
field.setValue(value)
|
||||
field.valueChanged.connect(self._measurements_changed)
|
||||
self._settings[key] = field
|
||||
return field
|
||||
|
||||
def add_capture(self, collections: list[SweepCollection], config: dict) -> None:
|
||||
def add_capture(self, collections: list[SweepCollection], config: dict, provenance: dict) -> None:
|
||||
"""Freeze a captured group before the operator enters its target coordinates."""
|
||||
if not collections:
|
||||
raise ValueError("Не получены свипы отражателя")
|
||||
if self._config is not None and any(
|
||||
config.get(key) != self._config.get(key) for key in ("radar", "gpr", "preprocess")
|
||||
raise ValueError("No reflector sweeps were captured")
|
||||
if self._config is not None and (
|
||||
any(config.get(key) != self._config.get(key) for key in ("radar", "gpr", "preprocess"))
|
||||
or config.get("run", {}).get("combos") != self._config.get("run", {}).get("combos")
|
||||
or provenance != self._provenance
|
||||
):
|
||||
raise ValueError("Параметры радара, геометрия или препроцессинг изменились между наборами")
|
||||
self._config = copy.deepcopy(config)
|
||||
raise ValueError("Radar settings, geometry, channels or preprocessing changed between captures")
|
||||
if self._config is None:
|
||||
self._config = copy.deepcopy(config)
|
||||
self._provenance = copy.deepcopy(provenance)
|
||||
self._captures.append({"collections": copy.deepcopy(collections)})
|
||||
row = self._table.rowCount()
|
||||
self._table.insertRow(row)
|
||||
for column, value in enumerate((str(row + 1), str(len(collections)), "", "", "")):
|
||||
item = QTableWidgetItem(value)
|
||||
if column < 2:
|
||||
item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsEditable)
|
||||
self._table.setItem(row, column, item)
|
||||
with QSignalBlocker(self._table):
|
||||
self._table.insertRow(row)
|
||||
for column, value in enumerate((str(row + 1), str(len(collections)), "", "", "")):
|
||||
item = QTableWidgetItem(value)
|
||||
if column < 2:
|
||||
item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsEditable)
|
||||
self._table.setItem(row, column, item)
|
||||
self._table.setCurrentCell(row, 2)
|
||||
self.set_status("Данные сняты. Введите координаты центра отражателя для нового набора.")
|
||||
self._measurements_changed()
|
||||
self.set_status("Data captured. Enter the reflector centre coordinates for the new target.")
|
||||
self._update_actions()
|
||||
|
||||
def calculation_inputs(self) -> tuple[list[dict], dict, dict]:
|
||||
def calculation_inputs(self) -> tuple[list[dict], dict, dict, dict]:
|
||||
"""Validate XYZ after capture; expose a frozen recipe to the worker."""
|
||||
if not self._captures or self._config is None:
|
||||
raise ValueError("Сначала снимите данные отражателя")
|
||||
if not self._captures or self._config is None or self._provenance is None:
|
||||
raise ValueError("Capture reflector data first")
|
||||
groups = []
|
||||
for row, capture in enumerate(self._captures):
|
||||
try:
|
||||
@@ -189,9 +223,9 @@ class ObjectCalibrationDialog(QDialog):
|
||||
coordinates.append(float(item.text().strip()))
|
||||
target = tuple(coordinates)
|
||||
except ValueError as exc:
|
||||
raise ValueError(f"Введите X, Y и Z центра отражателя для набора {row + 1}") from exc
|
||||
raise ValueError(f"Enter X, Y and Z of the reflector centre for target {row + 1}") from exc
|
||||
if not all(math.isfinite(value) for value in target):
|
||||
raise ValueError(f"Координаты набора {row + 1} должны быть конечными числами")
|
||||
raise ValueError(f"Target {row + 1} coordinates must be finite numbers")
|
||||
groups.append({**capture, "target_scene_m": target})
|
||||
settings: dict[str, float | bool | str | tuple[str, ...] | tuple[int, ...]] = {
|
||||
key: field.value() * (1e6 if key in {"f_min", "f_max"} else 1) if isinstance(field, QDoubleSpinBox) else
|
||||
@@ -199,10 +233,10 @@ class ObjectCalibrationDialog(QDialog):
|
||||
for key, field in self._settings.items()
|
||||
}
|
||||
settings["refine_axes"] = tuple(axis for axis, field in self._refine_axes.items() if field.isChecked())
|
||||
traces = self._captures[0]["collections"][0].traces
|
||||
settings["input_idx"] = tuple(sorted({trace.combo.input for trace in traces}))
|
||||
settings["output_idx"] = tuple(sorted({trace.combo.output for trace in traces}))
|
||||
return groups, copy.deepcopy(self._config), settings
|
||||
geometry = self._config["gpr"]
|
||||
settings["input_idx"] = tuple(sorted(entry["input_pos"] for entry in geometry["rx_geometry"]))
|
||||
settings["output_idx"] = tuple(sorted(entry["output_pos"] for entry in geometry["tx_geometry"]))
|
||||
return groups, copy.deepcopy(self._config), settings, copy.deepcopy(self._provenance)
|
||||
|
||||
def set_name(self) -> str:
|
||||
return self._set_name_input.text().strip()
|
||||
@@ -218,60 +252,74 @@ class ObjectCalibrationDialog(QDialog):
|
||||
def set_status(self, message: str) -> None:
|
||||
self._status_label.setText(message)
|
||||
|
||||
def show_calibration(self, calibration: dict) -> None:
|
||||
self._calibration = calibration
|
||||
self._target_combo.clear()
|
||||
for index, target in enumerate(calibration.get("meta", {}).get("targets", [])):
|
||||
xyz = target.get("nominal_scene_m", [])
|
||||
self._target_combo.addItem(f"Набор {index + 1}: {xyz}")
|
||||
def show_calibration(self, metadata: dict) -> None:
|
||||
"""Display phase diagnostics stored with the calculated calibration set."""
|
||||
self._calibration = metadata
|
||||
with QSignalBlocker(self._target_combo):
|
||||
self._target_combo.clear()
|
||||
for index, target in enumerate(metadata["targets"]):
|
||||
xyz = target["nominal_scene_m"]
|
||||
self._target_combo.addItem(f"Target {index + 1}: {xyz}")
|
||||
self._draw_phase_effect()
|
||||
|
||||
def _clear_phase_effect(self) -> None:
|
||||
for image in self._phase_images:
|
||||
image.clear()
|
||||
for plot, text in self._phase_text:
|
||||
plot.removeItem(text)
|
||||
self._phase_text.clear()
|
||||
for plot, title in zip(self._phase_plots, ("Before", "After")):
|
||||
plot.setTitle(title)
|
||||
|
||||
def _draw_phase_effect(self, *_args) -> None:
|
||||
self._phase_plot.clear()
|
||||
"""Render notebook cell 5: Tx-by-Rx phase maps relative to each matrix sum."""
|
||||
self._clear_phase_effect()
|
||||
if self._calibration is None:
|
||||
return
|
||||
diagnostics = self._calibration.get("meta", {}).get("diagnostics", [])
|
||||
diagnostics = self._calibration["diagnostics"]
|
||||
index = self._target_combo.currentIndex()
|
||||
if not 0 <= index < len(diagnostics):
|
||||
self._quality_label.setText("У этого набора нет диагностики фаз в точке цели.")
|
||||
self._quality_label.setText("This target has no phase diagnostics.")
|
||||
return
|
||||
diagnostic = diagnostics[index]
|
||||
phases = np.array([diagnostic["phase_before_deg"], diagnostic["phase_after_deg"]]).reshape(2, -1)
|
||||
before, after = (phases - phases[0, 0] + 180) % 360 - 180
|
||||
pairs = [(output, input_pos) for output in self._calibration["tx_output_pos"]
|
||||
for input_pos in self._calibration["rx_input_pos"]]
|
||||
xx = np.arange(len(pairs))
|
||||
for values, color, label in ((before, "#ed7d31", "До"), (after, "#42c7a5", "После")):
|
||||
self._phase_plot.plot(xx, values, pen=pg.mkPen(color, width=1.5), symbol="o",
|
||||
symbolBrush=color, symbolPen=color, name=label)
|
||||
step = max(1, len(pairs) // 16)
|
||||
self._phase_plot.getAxis("bottom").setTicks([[
|
||||
(k, f"o{pairs[k][0]}/i{pairs[k][1]}") for k in range(0, len(pairs), step)
|
||||
]])
|
||||
self._quality_label.setText(
|
||||
f"Согласованность фаз в точке цели: {diagnostic['coherence_before']:.3f} "
|
||||
f"→ {diagnostic['coherence_after']:.3f} (1 — все вклады в фазе)."
|
||||
)
|
||||
tx, rx = self._calibration["tx_output_pos"], self._calibration["rx_input_pos"]
|
||||
for key, plot, image in zip(("before", "after"), self._phase_plots, self._phase_images):
|
||||
matrix = np.asarray(diagnostic[f"phase_{key}_deg"], dtype=float)
|
||||
image.setImage(matrix, autoLevels=False, levels=(-180, 180))
|
||||
image.setRect(QRectF(-.5, -.5, len(rx), len(tx)))
|
||||
plot.getAxis("bottom").setTicks([[(i, f"Rx{port}") for i, port in enumerate(rx)]])
|
||||
plot.getAxis("left").setTicks([[(i, f"Tx{port}") for i, port in enumerate(tx)]])
|
||||
plot.setRange(xRange=(-.5, len(rx) - .5), yRange=(-.5, len(tx) - .5), padding=0)
|
||||
plot.setTitle(f"{key.title()}: pair phase at target (STD {diagnostic[f'phase_std_{key}_deg']:.0f}°)")
|
||||
for (row, column), value in np.ndenumerate(matrix):
|
||||
text = pg.TextItem(f"{value:+.0f}", color="w", anchor=(.5, .5), fill=pg.mkBrush(0, 0, 0, 90))
|
||||
text.setPos(column, row)
|
||||
plot.addItem(text)
|
||||
self._phase_text.append((plot, text))
|
||||
self._quality_label.setText("Each matrix shows phase relative to its own coherent sum, in degrees.")
|
||||
|
||||
def _remove_capture(self) -> None:
|
||||
row = self._table.currentRow()
|
||||
if row >= 0:
|
||||
self._captures.pop(row)
|
||||
self._table.removeRow(row)
|
||||
for index in range(self._table.rowCount()):
|
||||
item = self._table.item(index, 0)
|
||||
assert item is not None
|
||||
item.setText(str(index + 1))
|
||||
with QSignalBlocker(self._table):
|
||||
self._table.removeRow(row)
|
||||
for index in range(self._table.rowCount()):
|
||||
item = self._table.item(index, 0)
|
||||
assert item is not None
|
||||
item.setText(str(index + 1))
|
||||
if not self._captures:
|
||||
self._config = None
|
||||
self._provenance = None
|
||||
self._measurements_changed()
|
||||
self._update_actions()
|
||||
|
||||
def _measurements_changed(self, *_args) -> None:
|
||||
self._calibration = None
|
||||
if hasattr(self, "_phase_plot"):
|
||||
self._phase_plot.clear()
|
||||
with QSignalBlocker(self._target_combo):
|
||||
self._target_combo.clear()
|
||||
self._quality_label.setText("Параметры изменены — требуется новый расчёт.")
|
||||
self._clear_phase_effect()
|
||||
self._quality_label.setText("Measurements or settings changed. Recalculate to update the phase maps.")
|
||||
|
||||
def _update_actions(self) -> None:
|
||||
busy = self._capture_pending or self._calculating
|
||||
@@ -287,7 +335,3 @@ class ObjectCalibrationDialog(QDialog):
|
||||
def reject(self) -> None:
|
||||
self.cancel_requested.emit()
|
||||
super().reject()
|
||||
|
||||
def closeEvent(self, event) -> None: # noqa: N802
|
||||
self.cancel_requested.emit()
|
||||
super().closeEvent(event)
|
||||
|
||||
Reference in New Issue
Block a user