From e66e7aef831ff3cd1707c150dca289a2b3f4cd7e Mon Sep 17 00:00:00 2001 From: Theodor Chikin Date: Wed, 4 Mar 2026 16:22:27 +0300 Subject: [PATCH] implemented reference subtraction from B_scan. Reference is average from all visible B-scan. --- RFG_ADC_dataplotter.py | 380 ++++++++++++++++++++++++++++------------- 1 file changed, 266 insertions(+), 114 deletions(-) diff --git a/RFG_ADC_dataplotter.py b/RFG_ADC_dataplotter.py index e626c23..1109aaf 100755 --- a/RFG_ADC_dataplotter.py +++ b/RFG_ADC_dataplotter.py @@ -198,12 +198,19 @@ def _prepare_fft_segment( return resampled, take_fft -def _compute_fft_row( +def _fft_mag_to_db(mag: np.ndarray) -> np.ndarray: + """Перевод модуля спектра в дБ с отсечкой отрицательных значений после вычитания фона.""" + mag_arr = np.asarray(mag, dtype=np.float32) + safe_mag = np.maximum(mag_arr, 0.0) + return (20.0 * np.log10(safe_mag + 1e-9)).astype(np.float32, copy=False) + + +def _compute_fft_mag_row( sweep: np.ndarray, freqs: Optional[np.ndarray], bins: int, ) -> np.ndarray: - """Посчитать FFT-строку, используя калиброванную частотную ось при подготовке входа.""" + """Посчитать линейный модуль FFT-строки, используя калиброванную частотную ось.""" if bins <= 0: return np.zeros((0,), dtype=np.float32) @@ -217,10 +224,18 @@ def _compute_fft_row( fft_in[:take_fft] = fft_seg * win spec = np.fft.ifft(fft_in) mag = np.abs(spec).astype(np.float32) - fft_row = 20.0 * np.log10(mag + 1e-9) - if fft_row.shape[0] != bins: - fft_row = fft_row[:bins] - return fft_row + if mag.shape[0] != bins: + mag = mag[:bins] + return mag + + +def _compute_fft_row( + sweep: np.ndarray, + freqs: Optional[np.ndarray], + bins: int, +) -> np.ndarray: + """Посчитать FFT-строку в дБ.""" + return _fft_mag_to_db(_compute_fft_mag_row(sweep, freqs, bins)) def _compute_distance_axis(freqs: Optional[np.ndarray], bins: int) -> np.ndarray: @@ -1080,8 +1095,8 @@ def main(): parser.add_argument( "--backend", choices=["auto", "pg", "mpl"], - default="auto", - help="Графический бэкенд: pyqtgraph (pg) — быстрее; matplotlib (mpl) — совместимый. По умолчанию auto", + default="pg", + help="Графический бэкенд: pyqtgraph (pg) — быстрее; matplotlib (mpl) — совместимый. По умолчанию pg", ) parser.add_argument( "--norm-type", @@ -1119,14 +1134,18 @@ def main(): args = parser.parse_args() # Попробуем быстрый бэкенд (pyqtgraph) при auto/pg - if args.backend in ("pg"): + if args.backend == "pg": try: return run_pyqtgraph(args) except Exception as e: - if args.backend == "pg": - sys.stderr.write(f"[error] PyQtGraph бэкенд недоступен: {e}\n") - sys.exit(1) - # При auto — тихо откатываемся на matplotlib + sys.stderr.write(f"[error] PyQtGraph бэкенд недоступен: {e}\n") + sys.exit(1) + + if args.backend == "auto": + try: + return run_pyqtgraph(args) + except Exception: + pass # При auto — тихо откатываемся на matplotlib try: import matplotlib @@ -1186,6 +1205,7 @@ def main(): ymax_slider = None contrast_slider = None calib_enabled = False + bg_subtract_enabled = False peak_calibrate_mode = bool(getattr(args, "calibrate", False)) current_peak_width: Optional[float] = None norm_type = str(getattr(args, "norm_type", "projector")).strip().lower() @@ -1283,12 +1303,15 @@ def main(): def _normalize_sweep(raw: np.ndarray, calib: np.ndarray) -> np.ndarray: return _normalize_by_calib(raw, calib, norm_type=norm_type) - def _set_calib_enabled(): - nonlocal calib_enabled, current_sweep_norm + def _sync_checkbox_states(): + nonlocal calib_enabled, bg_subtract_enabled, current_sweep_norm try: - calib_enabled = bool(cb.get_status()[0]) if cb is not None else False + states = cb.get_status() if cb is not None else () + calib_enabled = bool(states[0]) if len(states) > 0 else False + bg_subtract_enabled = bool(states[1]) if len(states) > 1 else False except Exception: calib_enabled = False + bg_subtract_enabled = False if calib_enabled and current_sweep_raw is not None and last_calib_sweep is not None: current_sweep_norm = _normalize_sweep(current_sweep_raw, last_calib_sweep) else: @@ -1299,11 +1322,11 @@ def main(): ax_smin = fig.add_axes([0.92, 0.55, 0.02, 0.35]) ax_smax = fig.add_axes([0.95, 0.55, 0.02, 0.35]) ax_sctr = fig.add_axes([0.98, 0.55, 0.02, 0.35]) - ax_cb = fig.add_axes([0.92, 0.45, 0.08, 0.08]) + ax_cb = fig.add_axes([0.90, 0.40, 0.10, 0.14]) ymin_slider = Slider(ax_smin, "R min", 0.0, 1.0, valinit=0.0, orientation="vertical") ymax_slider = Slider(ax_smax, "R max", 0.0, 1.0, valinit=1.0, orientation="vertical") contrast_slider = Slider(ax_sctr, "Int max", 0, 100, valinit=100, valstep=1, orientation="vertical") - cb = CheckButtons(ax_cb, ["калибровка"], [False]) + cb = CheckButtons(ax_cb, ["нормировка", "вычет фона"], [False, False]) def _on_ylim_change(_val): try: @@ -1318,7 +1341,7 @@ def main(): ymax_slider.on_changed(_on_ylim_change) # Контраст влияет на верхнюю границу цветовой шкалы (процент от авто-диапазона) contrast_slider.on_changed(lambda _v: fig.canvas.draw_idle()) - cb.on_clicked(lambda _v: _set_calib_enabled()) + cb.on_clicked(lambda _v: _sync_checkbox_states()) except Exception: pass @@ -1449,11 +1472,12 @@ def main(): if ring_time is not None: ring_time[head] = time.time() head = (head + 1) % ring.shape[0] - # FFT строка (дБ) + # FFT строка (линейный модуль; перевод в дБ делаем при отображении) if ring_fft is not None: bins = ring_fft.shape[1] - fft_row = _compute_fft_row(s, freqs, bins) - ring_fft[(head - 1) % ring_fft.shape[0], :] = fft_row + fft_mag = _compute_fft_mag_row(s, freqs, bins) + ring_fft[(head - 1) % ring_fft.shape[0], :] = fft_mag + fft_row = _fft_mag_to_db(fft_mag) # Экстремумы для цветовой шкалы fr_min = np.nanmin(fft_row) fr_max = np.nanmax(fft_row) @@ -1519,7 +1543,7 @@ def main(): return base_t def _subtract_recent_mean_fft(disp_fft: np.ndarray) -> np.ndarray: - """Вычесть среднее по каждой частоте за последние spec_mean_sec секунд.""" + """Вычесть среднее по каждой дальности за последние spec_mean_sec секунд в линейной области.""" if spec_mean_sec <= 0.0: return disp_fft disp_times = make_display_times() @@ -1536,6 +1560,33 @@ def main(): mean_spec = np.nan_to_num(mean_spec, nan=0.0) return disp_fft - mean_spec[:, None] + def _visible_bg_fft(disp_fft: np.ndarray) -> Optional[np.ndarray]: + """Оценка фона по медиане в текущем видимом по времени окне B-scan.""" + if not bg_subtract_enabled or disp_fft.size == 0: + return None + ny, nx = disp_fft.shape + if ny <= 0 or nx <= 0: + return None + try: + x0, x1 = ax_spec.get_xlim() + except Exception: + x0, x1 = 0.0, float(nx - 1) + xmin, xmax = sorted((float(x0), float(x1))) + ix0 = max(0, min(nx - 1, int(np.floor(xmin)))) + ix1 = max(0, min(nx - 1, int(np.ceil(xmax)))) + if ix1 < ix0: + ix1 = ix0 + window = disp_fft[:, ix0 : ix1 + 1] + if window.size == 0: + return None + try: + bg_spec = np.nanmedian(window, axis=1) + except Exception: + return None + if not np.any(np.isfinite(bg_spec)): + return None + return np.nan_to_num(bg_spec, nan=0.0).astype(np.float32, copy=False) + def make_display_ring_fft(): if ring_fft is None: return np.zeros((1, 1), dtype=np.float32) @@ -1658,39 +1709,61 @@ def main(): # Обновление водопада спектров if changed and ring_fft is not None: - disp_fft = make_display_ring_fft() - disp_fft = _subtract_recent_mean_fft(disp_fft) + disp_fft_lin = make_display_ring_fft() + disp_fft_lin = _subtract_recent_mean_fft(disp_fft_lin) + bg_spec = _visible_bg_fft(disp_fft_lin) + if bg_spec is not None: + num = np.maximum(disp_fft_lin, 0.0).astype(np.float32, copy=False) + 1e-9 + den = bg_spec[:, None] + 1e-9 + disp_fft = (20.0 * np.log10(num / den)).astype(np.float32, copy=False) + else: + disp_fft = _fft_mag_to_db(disp_fft_lin) # Новые данные справа: без реверса img_fft_obj.set_data(disp_fft) # Подписи времени не обновляем динамически (оставляем авто-тики) # Автодиапазон по среднему спектру за видимый интервал (как в хорошей версии) - try: - # disp_fft имеет форму (bins, time); берём среднее по времени - mean_spec = np.nanmean(disp_fft, axis=1) - vmin_v = float(np.nanmin(mean_spec)) - vmax_v = float(np.nanmax(mean_spec)) - except Exception: - vmin_v = vmax_v = None - # Если средние не дают валидный диапазон — используем процентильную обрезку (если задана) - if (vmin_v is None or not np.isfinite(vmin_v)) or (vmax_v is None or not np.isfinite(vmax_v)) or vmin_v == vmax_v: - if spec_clip is not None: - try: - vmin_v = float(np.nanpercentile(disp_fft, spec_clip[0])) - vmax_v = float(np.nanpercentile(disp_fft, spec_clip[1])) - except Exception: - vmin_v = vmax_v = None - # Фолбэк к отслеживаемым минимум/максимумам - if (vmin_v is None or not np.isfinite(vmin_v)) or (vmax_v is None or not np.isfinite(vmax_v)) or vmin_v == vmax_v: - if y_min_fft is not None and y_max_fft is not None and np.isfinite(y_min_fft) and np.isfinite(y_max_fft) and y_min_fft != y_max_fft: - vmin_v, vmax_v = y_min_fft, y_max_fft - if vmin_v is not None and vmax_v is not None and vmin_v != vmax_v: - # Применим скалирование контрастом (верхняя граница) + if bg_spec is not None: try: - c = float(contrast_slider.val) / 100.0 if contrast_slider is not None else 1.0 + p5 = float(np.nanpercentile(disp_fft, 5)) + p95 = float(np.nanpercentile(disp_fft, 95)) + span = max(abs(p5), abs(p95)) except Exception: - c = 1.0 - vmax_eff = vmin_v + c * (vmax_v - vmin_v) - img_fft_obj.set_clim(vmin=vmin_v, vmax=vmax_eff) + span = float("nan") + if np.isfinite(span) and span > 0.0: + try: + c = float(contrast_slider.val) / 100.0 if contrast_slider is not None else 1.0 + except Exception: + c = 1.0 + span_eff = max(span * c, 1e-6) + img_fft_obj.set_clim(vmin=-span_eff, vmax=span_eff) + else: + try: + # disp_fft имеет форму (bins, time); берём среднее по времени + mean_spec = np.nanmean(disp_fft, axis=1) + vmin_v = float(np.nanmin(mean_spec)) + vmax_v = float(np.nanmax(mean_spec)) + except Exception: + vmin_v = vmax_v = None + # Если средние не дают валидный диапазон — используем процентильную обрезку (если задана) + if (vmin_v is None or not np.isfinite(vmin_v)) or (vmax_v is None or not np.isfinite(vmax_v)) or vmin_v == vmax_v: + if spec_clip is not None: + try: + vmin_v = float(np.nanpercentile(disp_fft, spec_clip[0])) + vmax_v = float(np.nanpercentile(disp_fft, spec_clip[1])) + except Exception: + vmin_v = vmax_v = None + # Фолбэк к отслеживаемым минимум/максимумам + if (vmin_v is None or not np.isfinite(vmin_v)) or (vmax_v is None or not np.isfinite(vmax_v)) or vmin_v == vmax_v: + if y_min_fft is not None and y_max_fft is not None and np.isfinite(y_min_fft) and np.isfinite(y_max_fft) and y_min_fft != y_max_fft: + vmin_v, vmax_v = y_min_fft, y_max_fft + if vmin_v is not None and vmax_v is not None and vmin_v != vmax_v: + # Применим скалирование контрастом (верхняя граница) + try: + c = float(contrast_slider.val) / 100.0 if contrast_slider is not None else 1.0 + except Exception: + c = 1.0 + vmax_eff = vmin_v + c * (vmax_v - vmin_v) + img_fft_obj.set_clim(vmin=vmin_v, vmax=vmax_eff) if changed and current_info: status_payload = dict(current_info) @@ -1776,16 +1849,11 @@ def run_pyqtgraph(args): peak_calibrate_mode = bool(getattr(args, "calibrate", False)) try: import pyqtgraph as pg - from PyQt5 import QtCore, QtWidgets # noqa: F401 - except Exception: - # Возможно установлена PySide6 - try: - import pyqtgraph as pg - from PySide6 import QtCore, QtWidgets # noqa: F401 - except Exception as e: - raise RuntimeError( - "pyqtgraph/PyQt5(Pyside6) не найдены. Установите: pip install pyqtgraph PyQt5" - ) from e + from pyqtgraph.Qt import QtCore, QtWidgets # type: ignore + except Exception as e: + raise RuntimeError( + "pyqtgraph и совместимый Qt backend не найдены. Установите: pip install pyqtgraph PyQt5" + ) from e # Очередь завершённых свипов и поток чтения q: Queue[SweepPacket] = Queue(maxsize=1000) @@ -1823,8 +1891,25 @@ def run_pyqtgraph(args): app.setQuitOnLastWindowClosed(True) except Exception: pass - win = pg.GraphicsLayoutWidget(show=True, title=args.title) - win.resize(1200, 600) + main_window = QtWidgets.QWidget() + try: + main_window.setWindowTitle(str(args.title)) + except Exception: + pass + main_layout = QtWidgets.QHBoxLayout(main_window) + main_layout.setContentsMargins(6, 6, 6, 6) + main_layout.setSpacing(6) + win = pg.GraphicsLayoutWidget(show=False, title=args.title) + main_layout.addWidget(win) + settings_widget = QtWidgets.QWidget() + settings_layout = QtWidgets.QVBoxLayout(settings_widget) + settings_layout.setContentsMargins(6, 6, 6, 6) + settings_layout.setSpacing(8) + try: + settings_widget.setMinimumWidth(170) + except Exception: + pass + main_layout.addWidget(settings_widget) # Плот последнего свипа (слева-сверху) p_line = win.addPlot(row=0, col=0, title="Сырые данные") @@ -1889,20 +1974,17 @@ def run_pyqtgraph(args): spec_left_line.setVisible(False) spec_right_line.setVisible(False) - # Отдельное окно контролов: GraphicsLayoutWidget не принимает обычные QWidget через addItem. - calib_cb = QtWidgets.QCheckBox("калибровка") - control_window = None - control_layout = None - if peak_calibrate_mode: - control_window = QtWidgets.QWidget() - try: - control_window.setWindowTitle(f"{args.title} controls") - except Exception: - pass - control_layout = QtWidgets.QVBoxLayout(control_window) - control_layout.setContentsMargins(8, 8, 8, 8) - control_layout.setSpacing(6) - control_layout.addWidget(calib_cb) + # Правая панель настроек внутри основного окна. + calib_cb = QtWidgets.QCheckBox("нормировка") + bg_subtract_cb = QtWidgets.QCheckBox("вычет фона") + try: + settings_title = QtWidgets.QLabel("Настройки") + settings_layout.addWidget(settings_title) + except Exception: + pass + settings_layout.addWidget(calib_cb) + settings_layout.addWidget(bg_subtract_cb) + calib_window = None # Статусная строка (внизу окна) status = pg.LabelItem(justify="left") @@ -1931,6 +2013,7 @@ def run_pyqtgraph(args): spec_clip = _parse_spec_clip(getattr(args, "spec_clip", None)) spec_mean_sec = float(getattr(args, "spec_mean_sec", 0.0)) calib_enabled = False + bg_subtract_enabled = False current_peak_width: Optional[float] = None norm_type = str(getattr(args, "norm_type", "projector")).strip().lower() c_edits = [] @@ -1959,16 +2042,31 @@ def run_pyqtgraph(args): else: current_sweep_norm = None + def _set_bg_subtract_enabled(): + nonlocal bg_subtract_enabled + try: + bg_subtract_enabled = bool(bg_subtract_cb.isChecked()) + except Exception: + bg_subtract_enabled = False + try: calib_cb.stateChanged.connect(lambda _v: _set_calib_enabled()) except Exception: pass + try: + bg_subtract_cb.stateChanged.connect(lambda _v: _set_bg_subtract_enabled()) + except Exception: + pass if peak_calibrate_mode: try: - c_widget = QtWidgets.QWidget() - c_layout = QtWidgets.QFormLayout(c_widget) - c_layout.setContentsMargins(0, 0, 0, 0) + calib_window = QtWidgets.QWidget() + try: + calib_window.setWindowTitle(f"{args.title} freq calibration") + except Exception: + pass + calib_layout = QtWidgets.QFormLayout(calib_window) + calib_layout.setContentsMargins(8, 8, 8, 8) def _apply_c_value(idx: int, edit): global CALIBRATION_C @@ -1990,17 +2088,18 @@ def run_pyqtgraph(args): edit.editingFinished.connect(lambda i=idx, e=edit: _apply_c_value(i, e)) except Exception: pass - c_layout.addRow(f"C{idx}", edit) + calib_layout.addRow(f"C{idx}", edit) c_edits.append(edit) - if control_layout is not None: - control_layout.addWidget(c_widget) - except Exception: - pass - if control_window is not None: - try: - control_window.show() + try: + calib_window.show() + except Exception: + pass except Exception: pass + try: + settings_layout.addStretch(1) + except Exception: + pass def ensure_buffer(_w: int): nonlocal ring, ring_time, head, width, x_shared, ring_fft, distance_shared @@ -2076,6 +2175,33 @@ def run_pyqtgraph(args): return None return (vmin, vmax) + def _visible_bg_fft(disp_fft: np.ndarray) -> Optional[np.ndarray]: + """Оценка фона по медиане в текущем видимом по времени окне B-scan.""" + if not bg_subtract_enabled or disp_fft.size == 0: + return None + ny, nx = disp_fft.shape + if ny <= 0 or nx <= 0: + return None + try: + (x0, x1), _ = p_spec.viewRange() + except Exception: + x0, x1 = 0.0, float(nx - 1) + xmin, xmax = sorted((float(x0), float(x1))) + ix0 = max(0, min(nx - 1, int(np.floor(xmin)))) + ix1 = max(0, min(nx - 1, int(np.ceil(xmax)))) + if ix1 < ix0: + ix1 = ix0 + window = disp_fft[:, ix0 : ix1 + 1] + if window.size == 0: + return None + try: + bg_spec = np.nanmedian(window, axis=1) + except Exception: + return None + if not np.any(np.isfinite(bg_spec)): + return None + return np.nan_to_num(bg_spec, nan=0.0).astype(np.float32, copy=False) + def push_sweep(s: np.ndarray, freqs: Optional[np.ndarray] = None): nonlocal ring, ring_time, head, ring_fft, y_min_fft, y_max_fft if s is None or s.size == 0 or ring is None: @@ -2088,11 +2214,12 @@ def run_pyqtgraph(args): if ring_time is not None: ring_time[head] = time.time() head = (head + 1) % ring.shape[0] - # FFT строка (дБ) + # FFT строка (линейный модуль; перевод в дБ делаем при отображении) if ring_fft is not None: bins = ring_fft.shape[1] - fft_row = _compute_fft_row(s, freqs, bins) - ring_fft[(head - 1) % ring_fft.shape[0], :] = fft_row + fft_mag = _compute_fft_mag_row(s, freqs, bins) + ring_fft[(head - 1) % ring_fft.shape[0], :] = fft_mag + fft_row = _fft_mag_to_db(fft_mag) fr_min = np.nanmin(fft_row) fr_max = np.nanmax(fft_row) if y_min_fft is None or (not np.isnan(fr_min) and fr_min < y_min_fft): @@ -2272,41 +2399,58 @@ def run_pyqtgraph(args): pass if changed and ring_fft is not None: - disp_fft = ring_fft if head == 0 else np.roll(ring_fft, -head, axis=0) - disp_fft = disp_fft.T + disp_fft_lin = ring_fft if head == 0 else np.roll(ring_fft, -head, axis=0) + disp_fft_lin = disp_fft_lin.T if spec_mean_sec > 0.0 and ring_time is not None: disp_times = ring_time if head == 0 else np.roll(ring_time, -head) now_t = time.time() mask = np.isfinite(disp_times) & (disp_times >= (now_t - spec_mean_sec)) if np.any(mask): try: - mean_spec = np.nanmean(disp_fft[:, mask], axis=1) + mean_spec = np.nanmean(disp_fft_lin[:, mask], axis=1) mean_spec = np.nan_to_num(mean_spec, nan=0.0) - disp_fft = disp_fft - mean_spec[:, None] + disp_fft_lin = disp_fft_lin - mean_spec[:, None] except Exception: pass + bg_spec = _visible_bg_fft(disp_fft_lin) + if bg_spec is not None: + num = np.maximum(disp_fft_lin, 0.0).astype(np.float32, copy=False) + 1e-9 + den = bg_spec[:, None] + 1e-9 + disp_fft = (20.0 * np.log10(num / den)).astype(np.float32, copy=False) + else: + disp_fft = _fft_mag_to_db(disp_fft_lin) # Автодиапазон по среднему спектру за видимый интервал (как в хорошей версии) levels = None - try: - mean_spec = np.nanmean(disp_fft, axis=1) - vmin_v = float(np.nanmin(mean_spec)) - vmax_v = float(np.nanmax(mean_spec)) - if np.isfinite(vmin_v) and np.isfinite(vmax_v) and vmin_v != vmax_v: - levels = (vmin_v, vmax_v) - except Exception: - levels = None - # Процентильная обрезка как запасной вариант - if levels is None and spec_clip is not None: + if bg_spec is not None: try: - vmin_v = float(np.nanpercentile(disp_fft, spec_clip[0])) - vmax_v = float(np.nanpercentile(disp_fft, spec_clip[1])) + p5 = float(np.nanpercentile(disp_fft, 5)) + p95 = float(np.nanpercentile(disp_fft, 95)) + span = max(abs(p5), abs(p95)) + if np.isfinite(span) and span > 0.0: + levels = (-span, span) + except Exception: + levels = None + else: + try: + mean_spec = np.nanmean(disp_fft, axis=1) + vmin_v = float(np.nanmin(mean_spec)) + vmax_v = float(np.nanmax(mean_spec)) if np.isfinite(vmin_v) and np.isfinite(vmax_v) and vmin_v != vmax_v: levels = (vmin_v, vmax_v) except Exception: levels = None - # Ещё один фолбэк — глобальные накопленные мин/макс - if levels is None and y_min_fft is not None and y_max_fft is not None and np.isfinite(y_min_fft) and np.isfinite(y_max_fft) and y_min_fft != y_max_fft: - levels = (y_min_fft, y_max_fft) + # Процентильная обрезка как запасной вариант + if levels is None and spec_clip is not None: + try: + vmin_v = float(np.nanpercentile(disp_fft, spec_clip[0])) + vmax_v = float(np.nanpercentile(disp_fft, spec_clip[1])) + if np.isfinite(vmin_v) and np.isfinite(vmax_v) and vmin_v != vmax_v: + levels = (vmin_v, vmax_v) + except Exception: + levels = None + # Ещё один фолбэк — глобальные накопленные мин/макс + if levels is None and y_min_fft is not None and y_max_fft is not None and np.isfinite(y_min_fft) and np.isfinite(y_max_fft) and y_min_fft != y_max_fft: + levels = (y_min_fft, y_max_fft) if levels is not None: img_fft.setImage(disp_fft, autoLevels=False, levels=levels) else: @@ -2337,9 +2481,13 @@ def run_pyqtgraph(args): pass stop_event.set() reader.join(timeout=1.0) - if control_window is not None: + try: + main_window.close() + except Exception: + pass + if calib_window is not None: try: - control_window.close() + calib_window.close() except Exception: pass @@ -2352,7 +2500,7 @@ def run_pyqtgraph(args): except Exception: prev_sigint = None - orig_close_event = getattr(win, "closeEvent", None) + orig_close_event = getattr(main_window, "closeEvent", None) def _close_event(event): try: @@ -2371,12 +2519,16 @@ def run_pyqtgraph(args): pass try: - win.closeEvent = _close_event # type: ignore[method-assign] + main_window.closeEvent = _close_event # type: ignore[method-assign] except Exception: pass app.aboutToQuit.connect(on_quit) - win.show() + try: + main_window.resize(1200, 680) + except Exception: + pass + main_window.show() exec_fn = getattr(app, "exec_", None) or getattr(app, "exec", None) try: exec_fn()