This commit is contained in:
awe
2026-02-25 20:20:40 +03:00
parent 267ddedb19
commit f1652d072e
12 changed files with 1823 additions and 404 deletions

View File

@ -12,7 +12,7 @@ from rfg_adc_plotter.constants import FFT_LEN, FREQ_SPAN_GHZ, IFFT_LEN
_IFFT_T_MAX_NS = float((IFFT_LEN - 1) / (FREQ_SPAN_GHZ * 1e9) * 1e9)
from rfg_adc_plotter.io.sweep_reader import SweepReader
from rfg_adc_plotter.processing.normalizer import build_calib_envelopes
from rfg_adc_plotter.state.app_state import BACKGROUND_PATH, CALIB_ENVELOPE_PATH, AppState, format_status
from rfg_adc_plotter.state.app_state import AppState, format_status
from rfg_adc_plotter.state.ring_buffer import RingBuffer
from rfg_adc_plotter.types import SweepPacket
@ -82,7 +82,8 @@ def run_matplotlib(args):
import matplotlib
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from matplotlib.widgets import CheckButtons, Slider
from matplotlib.widgets import Button as MplButton
from matplotlib.widgets import CheckButtons, RadioButtons, Slider, TextBox
except Exception as e:
sys.stderr.write(f"[error] Нужны matplotlib и её зависимости: {e}\n")
sys.exit(1)
@ -111,6 +112,7 @@ def run_matplotlib(args):
logscale_enabled = bool(getattr(args, "logscale", False))
state = AppState(norm_type=norm_type)
state.configure_capture_import(fancy=bool(args.fancy), logscale=bool(getattr(args, "logscale", False)))
ring = RingBuffer(max_sweeps)
# --- Создание фигуры ---
@ -118,10 +120,12 @@ def run_matplotlib(args):
(ax_line, ax_img), (ax_fft, ax_spec) = axs
if hasattr(fig.canvas.manager, "set_window_title"):
fig.canvas.manager.set_window_title(args.title)
fig.subplots_adjust(wspace=0.25, hspace=0.35, left=0.07, right=0.90, top=0.92, bottom=0.08)
fig.subplots_adjust(wspace=0.25, hspace=0.35, left=0.07, right=0.90, top=0.92, bottom=0.22)
# Статусная строка
status_text = fig.text(0.01, 0.01, "", ha="left", va="bottom", fontsize=8, family="monospace")
pipeline_text = fig.text(0.01, 0.03, "", ha="left", va="bottom", fontsize=8, family="monospace")
ref_text = fig.text(0.01, 0.05, "", ha="left", va="bottom", fontsize=8, family="monospace")
# График последнего свипа
line_obj, = ax_line.plot([], [], lw=1, color="tab:blue")
@ -178,15 +182,144 @@ def run_matplotlib(args):
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_file = fig.add_axes([0.92, 0.36, 0.08, 0.08])
ax_line_mode = fig.add_axes([0.92, 0.10, 0.08, 0.08])
ymin_slider = Slider(ax_smin, "Y min", 0, max(1, fft_bins - 1), valinit=0, valstep=1, orientation="vertical")
ymax_slider = Slider(ax_smax, "Y max", 0, max(1, fft_bins - 1), valinit=max(1, fft_bins - 1), valstep=1, orientation="vertical")
contrast_slider = Slider(ax_sctr, "Int max", 0, 100, valinit=100, valstep=1, orientation="vertical")
calib_cb = CheckButtons(ax_cb, ["калибровка"], [False])
calib_file_cb = CheckButtons(ax_cb_file, ["из файла"], [False])
line_mode_rb = RadioButtons(ax_line_mode, ("raw", "processed"), active=0)
try:
ax_line_mode.set_title("Линия", fontsize=8, pad=2)
except Exception:
pass
line_mode_state = {"value": "raw"}
import os as _os
if not _os.path.isfile(CALIB_ENVELOPE_PATH):
ax_cb_file.set_visible(False)
try:
import tkinter as _tk
from tkinter import filedialog as _tk_filedialog
_tk_available = True
except Exception:
_tk = None
_tk_filedialog = None
_tk_available = False
# Нижняя панель путей и кнопок (работает без Qt; выбор файла через tkinter опционален).
ax_calib_path = fig.add_axes([0.07, 0.14, 0.40, 0.04])
ax_calib_load = fig.add_axes([0.48, 0.14, 0.07, 0.04])
ax_calib_pick = fig.add_axes([0.56, 0.14, 0.06, 0.04])
ax_calib_sample = fig.add_axes([0.63, 0.14, 0.09, 0.04])
ax_calib_save = fig.add_axes([0.73, 0.14, 0.10, 0.04])
ax_bg_path = fig.add_axes([0.07, 0.09, 0.40, 0.04])
ax_bg_load = fig.add_axes([0.48, 0.09, 0.07, 0.04])
ax_bg_pick = fig.add_axes([0.56, 0.09, 0.06, 0.04])
ax_bg_sample = fig.add_axes([0.63, 0.09, 0.09, 0.04])
ax_bg_save2 = fig.add_axes([0.73, 0.09, 0.10, 0.04])
calib_path_box = TextBox(ax_calib_path, "Калибр", initial=state.calib_envelope_path)
bg_path_box = TextBox(ax_bg_path, "Фон", initial=state.background_path)
calib_load_btn2 = MplButton(ax_calib_load, "Загруз.")
calib_pick_btn2 = MplButton(ax_calib_pick, "Файл")
calib_sample_btn2 = MplButton(ax_calib_sample, "sample")
calib_save_btn2 = MplButton(ax_calib_save, "Сохр env")
bg_load_btn2 = MplButton(ax_bg_load, "Загруз.")
bg_pick_btn2 = MplButton(ax_bg_pick, "Файл")
bg_sample_btn2 = MplButton(ax_bg_sample, "sample")
bg_save_btn2 = MplButton(ax_bg_save2, "Сохр фон")
if not _tk_available:
try:
calib_pick_btn2.label.set_text("Файл-")
bg_pick_btn2.label.set_text("Файл-")
except Exception:
pass
def _tb_text(tb):
try:
return str(tb.text).strip()
except Exception:
return ""
def _pick_file_dialog(initial_path: str) -> str:
if not _tk_available or _tk is None or _tk_filedialog is None:
return ""
root = None
try:
root = _tk.Tk()
root.withdraw()
root.attributes("-topmost", True)
except Exception:
root = None
try:
return str(
_tk_filedialog.askopenfilename(
initialdir=_os.path.dirname(initial_path) or ".",
initialfile=_os.path.basename(initial_path) or "",
title="Выбрать файл эталона (.npy или capture)",
)
)
finally:
try:
if root is not None:
root.destroy()
except Exception:
pass
def _sync_path_boxes():
try:
if _tb_text(calib_path_box) != state.calib_envelope_path:
calib_path_box.set_val(state.calib_envelope_path)
except Exception:
pass
try:
if _tb_text(bg_path_box) != state.background_path:
bg_path_box.set_val(state.background_path)
except Exception:
pass
def _refresh_status_texts():
pipeline_text.set_text(state.format_pipeline_status())
ref_text.set_text(state.format_reference_status())
try:
fig.canvas.draw_idle()
except Exception:
pass
def _line_mode() -> str:
return str(line_mode_state.get("value", "raw"))
def _refresh_checkboxes():
try:
# file-mode чекбокс показываем всегда; он активен при наличии пути/данных.
ax_cb_file.set_visible(True)
except Exception:
pass
def _load_calib_from_ui():
p = _tb_text(calib_path_box)
if p:
state.set_calib_envelope_path(p)
ok = state.load_calib_reference()
if ok and bool(calib_file_cb.get_status()[0]):
state.set_calib_mode("file")
state.set_calib_enabled(bool(calib_cb.get_status()[0]))
_sync_path_boxes()
_refresh_checkboxes()
_refresh_status_texts()
return ok
def _load_bg_from_ui():
p = _tb_text(bg_path_box)
if p:
state.set_background_path(p)
ok = state.load_background_reference()
_sync_path_boxes()
_refresh_status_texts()
return ok
def _on_ylim_change(_val):
try:
@ -200,7 +333,7 @@ def run_matplotlib(args):
def _on_calib_file_clicked(_v):
use_file = bool(calib_file_cb.get_status()[0])
if use_file:
ok = state.load_calib_envelope(CALIB_ENVELOPE_PATH)
ok = _load_calib_from_ui()
if ok:
state.set_calib_mode("file")
else:
@ -208,17 +341,15 @@ def run_matplotlib(args):
else:
state.set_calib_mode("live")
state.set_calib_enabled(bool(calib_cb.get_status()[0]))
_refresh_status_texts()
def _on_calib_clicked(_v):
import os as _os2
if _os2.path.isfile(CALIB_ENVELOPE_PATH):
ax_cb_file.set_visible(True)
state.set_calib_enabled(bool(calib_cb.get_status()[0]))
fig.canvas.draw_idle()
_refresh_checkboxes()
_refresh_status_texts()
ax_btn_bg = fig.add_axes([0.92, 0.27, 0.08, 0.05])
ax_cb_bg = fig.add_axes([0.92, 0.20, 0.08, 0.06])
from matplotlib.widgets import Button as MplButton
save_bg_btn = MplButton(ax_btn_bg, "Сохр. фон")
bg_cb = CheckButtons(ax_cb_bg, ["вычет фона"], [False])
@ -226,21 +357,88 @@ def run_matplotlib(args):
ok = state.save_background()
if ok:
state.load_background()
fig.canvas.draw_idle()
_sync_path_boxes()
_refresh_status_texts()
def _on_bg_clicked(_v):
state.set_background_enabled(bool(bg_cb.get_status()[0]))
_refresh_status_texts()
def _on_calib_load_btn(_event):
_load_calib_from_ui()
def _on_calib_pick_btn(_event):
path = _pick_file_dialog(_tb_text(calib_path_box) or state.calib_envelope_path)
if not path:
return
state.set_calib_envelope_path(path)
_sync_path_boxes()
_refresh_status_texts()
def _on_calib_sample_btn(_event):
state.set_calib_envelope_path(_os.path.join("sample_data", "no_antennas_35dB_attenuators"))
_sync_path_boxes()
if _load_calib_from_ui() and not bool(calib_file_cb.get_status()[0]):
calib_file_cb.set_active(0)
def _on_calib_save_btn(_event):
state.save_calib_envelope()
_sync_path_boxes()
_refresh_status_texts()
def _on_bg_load_btn(_event):
_load_bg_from_ui()
def _on_bg_pick_btn(_event):
path = _pick_file_dialog(_tb_text(bg_path_box) or state.background_path)
if not path:
return
state.set_background_path(path)
_sync_path_boxes()
_refresh_status_texts()
def _on_bg_sample_btn(_event):
state.set_background_path(_os.path.join("sample_data", "empty"))
_sync_path_boxes()
_load_bg_from_ui()
def _on_bg_save_btn2(_event):
ok = state.save_background()
if ok:
state.load_background()
_sync_path_boxes()
_refresh_status_texts()
def _on_line_mode_clicked(label):
line_mode_state["value"] = str(label)
try:
fig.canvas.draw_idle()
except Exception:
pass
save_bg_btn.on_clicked(_on_save_bg)
bg_cb.on_clicked(_on_bg_clicked)
calib_load_btn2.on_clicked(_on_calib_load_btn)
calib_pick_btn2.on_clicked(_on_calib_pick_btn)
calib_sample_btn2.on_clicked(_on_calib_sample_btn)
calib_save_btn2.on_clicked(_on_calib_save_btn)
bg_load_btn2.on_clicked(_on_bg_load_btn)
bg_pick_btn2.on_clicked(_on_bg_pick_btn)
bg_sample_btn2.on_clicked(_on_bg_sample_btn)
bg_save_btn2.on_clicked(_on_bg_save_btn2)
line_mode_rb.on_clicked(_on_line_mode_clicked)
ymin_slider.on_changed(_on_ylim_change)
ymax_slider.on_changed(_on_ylim_change)
contrast_slider.on_changed(lambda _v: fig.canvas.draw_idle())
calib_cb.on_clicked(_on_calib_clicked)
calib_file_cb.on_clicked(_on_calib_file_clicked)
_sync_path_boxes()
_refresh_checkboxes()
_refresh_status_texts()
except Exception:
calib_cb = None
line_mode_state = {"value": "raw"}
FREQ_MIN = 3.323
FREQ_MAX = 14.323
@ -276,23 +474,37 @@ def run_matplotlib(args):
xs = ring.x_shared[: raw.size]
else:
xs = np.arange(raw.size, dtype=np.int32)
line_obj.set_data(xs, raw)
if state.calib_mode == "file" and state.calib_file_envelope is not None:
upper = state.calib_file_envelope
lower = -upper
m_env = float(np.nanmax(np.abs(upper)))
if m_env <= 0.0:
m_env = 1.0
line_env_lo.set_data(xs[: upper.size], lower / m_env)
line_env_hi.set_data(xs[: upper.size], upper / m_env)
elif state.last_calib_sweep is not None:
calib = state.last_calib_sweep
m_calib = float(np.nanmax(np.abs(calib)))
if m_calib <= 0.0:
m_calib = 1.0
lower, upper = build_calib_envelopes(calib)
line_env_lo.set_data(xs[: calib.size], lower / m_calib)
line_env_hi.set_data(xs[: calib.size], upper / m_calib)
line_mode = str(line_mode_state.get("value", "raw"))
main = state.current_sweep_processed if line_mode == "processed" else raw
if main is not None:
line_obj.set_data(xs[: main.size], main)
else:
line_obj.set_data([], [])
if line_mode == "raw":
if state.calib_mode == "file" and state.calib_file_envelope is not None:
upper = np.asarray(state.calib_file_envelope, dtype=np.float32)
n_env = min(xs.size, upper.size)
if n_env > 0:
x_env = xs[:n_env]
y_env = upper[:n_env]
line_env_lo.set_data(x_env, -y_env)
line_env_hi.set_data(x_env, y_env)
else:
line_env_lo.set_data([], [])
line_env_hi.set_data([], [])
elif state.last_calib_sweep is not None:
calib = np.asarray(state.last_calib_sweep, dtype=np.float32)
lower, upper = build_calib_envelopes(calib)
n_env = min(xs.size, lower.size, upper.size)
if n_env > 0:
line_env_lo.set_data(xs[:n_env], lower[:n_env])
line_env_hi.set_data(xs[:n_env], upper[:n_env])
else:
line_env_lo.set_data([], [])
line_env_hi.set_data([], [])
else:
line_env_lo.set_data([], [])
line_env_hi.set_data([], [])
else:
line_env_lo.set_data([], [])
line_env_hi.set_data([], [])
@ -306,16 +518,19 @@ def run_matplotlib(args):
post = state.current_sweep_post_exp if state.current_sweep_post_exp is not None else raw
line_post_exp_obj.set_data(xs[: post.size], post)
if state.current_sweep_processed is not None:
proc = state.current_sweep_processed
line_obj.set_data(xs[: proc.size], proc)
if line_mode == "processed":
if state.current_sweep_processed is not None:
proc = state.current_sweep_processed
line_obj.set_data(xs[: proc.size], proc)
else:
line_obj.set_data([], [])
else:
line_obj.set_data([], [])
line_obj.set_data(xs[: raw.size], raw)
line_norm_obj.set_data([], [])
else:
line_pre_exp_obj.set_data([], [])
line_post_exp_obj.set_data([], [])
if state.current_sweep_norm is not None:
if line_mode == "raw" and state.current_sweep_norm is not None:
line_norm_obj.set_data(
xs[: state.current_sweep_norm.size], state.current_sweep_norm
)
@ -370,6 +585,11 @@ def run_matplotlib(args):
if changed and state.current_info:
status_text.set_text(format_status(state.current_info))
channel_text.set_text(state.format_channel_label())
pipeline_text.set_text(state.format_pipeline_status())
ref_text.set_text(state.format_reference_status())
elif changed:
pipeline_text.set_text(state.format_pipeline_status())
ref_text.set_text(state.format_reference_status())
return (
line_obj,
@ -382,6 +602,8 @@ def run_matplotlib(args):
fft_line_obj,
img_fft_obj,
status_text,
pipeline_text,
ref_text,
channel_text,
)

View File

@ -1,5 +1,6 @@
"""PyQtGraph-бэкенд реалтайм-плоттера свипов."""
import os
import sys
import threading
from queue import Queue
@ -10,7 +11,7 @@ import numpy as np
from rfg_adc_plotter.constants import FREQ_SPAN_GHZ, IFFT_LEN
from rfg_adc_plotter.io.sweep_reader import SweepReader
from rfg_adc_plotter.processing.normalizer import build_calib_envelopes
from rfg_adc_plotter.state.app_state import BACKGROUND_PATH, CALIB_ENVELOPE_PATH, AppState, format_status
from rfg_adc_plotter.state.app_state import AppState, format_status
from rfg_adc_plotter.state.ring_buffer import RingBuffer
from rfg_adc_plotter.types import SweepPacket
@ -90,6 +91,18 @@ def _visible_levels(
return (vmin, vmax)
def _short_path(path: str, max_len: int = 48) -> str:
p = str(path or "").strip()
if not p:
return "(не задан)"
if len(p) <= max_len:
return p
base = os.path.basename(p)
if len(base) <= max_len:
return f".../{base}"
return "..." + p[-(max_len - 3) :]
def run_pyqtgraph(args):
"""Быстрый GUI на PyQtGraph. Требует pyqtgraph и PyQt5/PySide6."""
try:
@ -128,13 +141,22 @@ def run_pyqtgraph(args):
logscale_enabled = bool(getattr(args, "logscale", False))
state = AppState(norm_type=norm_type)
state.configure_capture_import(fancy=bool(args.fancy), logscale=bool(getattr(args, "logscale", False)))
ring = RingBuffer(max_sweeps)
try:
_qt_text_selectable = QtCore.Qt.TextSelectableByMouse
except Exception:
try:
_qt_text_selectable = QtCore.Qt.TextInteractionFlag.TextSelectableByMouse
except Exception:
_qt_text_selectable = None
# --- Создание окна ---
pg.setConfigOptions(useOpenGL=True, antialias=False)
app = pg.mkQApp(args.title)
win = pg.GraphicsLayoutWidget(show=True, title=args.title)
win.resize(1200, 600)
win.resize(1280, 760)
# График последнего свипа (слева-сверху)
p_line = win.addPlot(row=0, col=0, title="Сырые данные")
@ -196,32 +218,58 @@ def run_pyqtgraph(args):
img_fft = pg.ImageItem()
p_spec.addItem(img_fft)
# Чекбоксы калибровки — в одном контейнере
# Блок управления калибровкой
calib_widget = QtWidgets.QWidget()
calib_layout = QtWidgets.QHBoxLayout(calib_widget)
calib_layout = QtWidgets.QVBoxLayout(calib_widget)
calib_layout.setContentsMargins(2, 2, 2, 2)
calib_layout.setSpacing(8)
calib_layout.setSpacing(4)
calib_row_1 = QtWidgets.QHBoxLayout()
calib_row_1.setSpacing(8)
calib_row_2 = QtWidgets.QHBoxLayout()
calib_row_2.setSpacing(6)
calib_cb = QtWidgets.QCheckBox("калибровка")
calib_file_cb = QtWidgets.QCheckBox("из файла")
calib_file_cb.setEnabled(False) # активируется только если файл существует
calib_file_cb.setEnabled(False)
calib_path_label = QtWidgets.QLabel()
calib_path_label.setMinimumWidth(260)
if _qt_text_selectable is not None:
calib_path_label.setTextInteractionFlags(_qt_text_selectable)
calib_pick_btn = QtWidgets.QPushButton("Файл…")
calib_load_btn = QtWidgets.QPushButton("Загрузить")
calib_save_btn = QtWidgets.QPushButton("Сохранить env")
calib_sample_btn = QtWidgets.QPushButton("sample calib")
calib_layout.addWidget(calib_cb)
calib_layout.addWidget(calib_file_cb)
calib_row_1.addWidget(calib_cb)
calib_row_1.addWidget(calib_file_cb)
calib_row_1.addStretch(1)
calib_row_2.addWidget(QtWidgets.QLabel("Калибр:"))
calib_row_2.addWidget(calib_path_label, 1)
calib_row_2.addWidget(calib_pick_btn)
calib_row_2.addWidget(calib_load_btn)
calib_row_2.addWidget(calib_save_btn)
calib_row_2.addWidget(calib_sample_btn)
calib_layout.addLayout(calib_row_1)
calib_layout.addLayout(calib_row_2)
cb_container_proxy = QtWidgets.QGraphicsProxyWidget()
cb_container_proxy.setWidget(calib_widget)
win.addItem(cb_container_proxy, row=2, col=1)
def _check_file_cb_available():
import os
calib_file_cb.setEnabled(os.path.isfile(CALIB_ENVELOPE_PATH))
_check_file_cb_available()
def _refresh_calib_controls():
calib_path_label.setText(_short_path(state.calib_envelope_path))
calib_path_label.setToolTip(state.calib_envelope_path)
calib_load_btn.setEnabled(bool(state.calib_envelope_path) and os.path.isfile(state.calib_envelope_path))
calib_save_btn.setEnabled(state.last_calib_sweep is not None)
# Переключатель file-mode доступен, если файл существует или уже загружен в память.
calib_file_cb.setEnabled(state.has_calib_envelope_file() or state.calib_file_envelope is not None)
def _on_calib_file_toggled(checked):
if checked:
ok = state.load_calib_envelope(CALIB_ENVELOPE_PATH)
ok = state.load_calib_reference()
if ok:
state.set_calib_mode("file")
else:
@ -229,43 +277,196 @@ def run_pyqtgraph(args):
else:
state.set_calib_mode("live")
state.set_calib_enabled(calib_cb.isChecked())
_refresh_calib_controls()
_refresh_pipeline_label()
def _on_calib_toggled(_v):
_check_file_cb_available()
state.set_calib_enabled(calib_cb.isChecked())
_refresh_calib_controls()
_refresh_pipeline_label()
def _on_pick_calib_path():
path, _ = QtWidgets.QFileDialog.getOpenFileName(
win,
"Выбрать источник калибровки (.npy или capture)",
state.calib_envelope_path,
"Все файлы (*);;NumPy (*.npy)",
)
if not path:
return
state.set_calib_envelope_path(path)
if calib_file_cb.isChecked():
if state.load_calib_reference():
state.set_calib_mode("file")
state.set_calib_enabled(calib_cb.isChecked())
else:
calib_file_cb.setChecked(False)
_refresh_calib_controls()
_refresh_pipeline_label()
def _on_load_calib():
if state.load_calib_reference():
if calib_file_cb.isChecked():
state.set_calib_mode("file")
state.set_calib_enabled(calib_cb.isChecked())
_refresh_calib_controls()
_refresh_pipeline_label()
def _on_save_calib():
if state.save_calib_envelope():
if calib_file_cb.isChecked():
state.load_calib_envelope()
state.set_calib_mode("file")
state.set_calib_enabled(calib_cb.isChecked())
_refresh_calib_controls()
_refresh_pipeline_label()
def _on_sample_calib():
sample_path = os.path.join("sample_data", "no_antennas_35dB_attenuators")
state.set_calib_envelope_path(sample_path)
if state.load_calib_reference():
calib_file_cb.setChecked(True)
state.set_calib_mode("file")
state.set_calib_enabled(calib_cb.isChecked())
_refresh_calib_controls()
_refresh_pipeline_label()
calib_cb.stateChanged.connect(_on_calib_toggled)
calib_file_cb.stateChanged.connect(lambda _v: _on_calib_file_toggled(calib_file_cb.isChecked()))
calib_pick_btn.clicked.connect(_on_pick_calib_path)
calib_load_btn.clicked.connect(_on_load_calib)
calib_save_btn.clicked.connect(_on_save_calib)
calib_sample_btn.clicked.connect(_on_sample_calib)
# Кнопка сохранения фона + чекбокс вычета фона
# Блок управления фоном
bg_widget = QtWidgets.QWidget()
bg_layout = QtWidgets.QHBoxLayout(bg_widget)
bg_layout = QtWidgets.QVBoxLayout(bg_widget)
bg_layout.setContentsMargins(2, 2, 2, 2)
bg_layout.setSpacing(8)
bg_layout.setSpacing(4)
save_bg_btn = QtWidgets.QPushButton("Сохр. фон")
bg_row_1 = QtWidgets.QHBoxLayout()
bg_row_1.setSpacing(8)
bg_row_2 = QtWidgets.QHBoxLayout()
bg_row_2.setSpacing(6)
save_bg_btn = QtWidgets.QPushButton("Сохранить фон")
load_bg_btn = QtWidgets.QPushButton("Загрузить")
bg_pick_btn = QtWidgets.QPushButton("Файл…")
bg_sample_btn = QtWidgets.QPushButton("sample bg")
bg_cb = QtWidgets.QCheckBox("вычет фона")
bg_cb.setEnabled(False)
bg_cb.setEnabled(False) # активируется при успешной загрузке/сохранении
bg_path_label = QtWidgets.QLabel()
bg_path_label.setMinimumWidth(260)
if _qt_text_selectable is not None:
bg_path_label.setTextInteractionFlags(_qt_text_selectable)
bg_layout.addWidget(save_bg_btn)
bg_layout.addWidget(bg_cb)
bg_row_1.addWidget(bg_cb)
bg_row_1.addStretch(1)
bg_row_2.addWidget(QtWidgets.QLabel("Фон:"))
bg_row_2.addWidget(bg_path_label, 1)
bg_row_2.addWidget(bg_pick_btn)
bg_row_2.addWidget(load_bg_btn)
bg_row_2.addWidget(save_bg_btn)
bg_row_2.addWidget(bg_sample_btn)
bg_layout.addLayout(bg_row_1)
bg_layout.addLayout(bg_row_2)
bg_container_proxy = QtWidgets.QGraphicsProxyWidget()
bg_container_proxy.setWidget(bg_widget)
win.addItem(bg_container_proxy, row=2, col=0)
def _refresh_bg_controls():
bg_path_label.setText(_short_path(state.background_path))
bg_path_label.setToolTip(state.background_path)
load_bg_btn.setEnabled(bool(state.background_path) and os.path.isfile(state.background_path))
bg_cb.setEnabled(state.background is not None or state.background_source_type == "capture_raw")
def _on_pick_bg_path():
path, _ = QtWidgets.QFileDialog.getOpenFileName(
win,
"Выбрать источник фона (.npy или capture)",
state.background_path,
"Все файлы (*);;NumPy (*.npy)",
)
if not path:
return
state.set_background_path(path)
if bg_cb.isChecked():
if not state.load_background_reference():
bg_cb.setChecked(False)
_refresh_bg_controls()
_refresh_pipeline_label()
def _on_load_bg():
state.load_background_reference()
_refresh_bg_controls()
_refresh_pipeline_label()
def _on_save_bg():
ok = state.save_background()
if ok:
state.load_background()
bg_cb.setEnabled(True)
_refresh_bg_controls()
_refresh_pipeline_label()
def _on_bg_toggled(_v):
state.set_background_enabled(bg_cb.isChecked())
_refresh_pipeline_label()
def _on_sample_bg():
sample_path = os.path.join("sample_data", "empty")
state.set_background_path(sample_path)
if state.load_background_reference():
bg_cb.setEnabled(True)
_refresh_bg_controls()
_refresh_pipeline_label()
bg_pick_btn.clicked.connect(_on_pick_bg_path)
load_bg_btn.clicked.connect(_on_load_bg)
save_bg_btn.clicked.connect(_on_save_bg)
bg_cb.stateChanged.connect(lambda _v: state.set_background_enabled(bg_cb.isChecked()))
bg_cb.stateChanged.connect(_on_bg_toggled)
bg_sample_btn.clicked.connect(_on_sample_bg)
# Переключатель отображения верхнего линейного графика
line_mode_widget = QtWidgets.QWidget()
line_mode_layout = QtWidgets.QHBoxLayout(line_mode_widget)
line_mode_layout.setContentsMargins(2, 2, 2, 2)
line_mode_layout.setSpacing(8)
line_mode_layout.addWidget(QtWidgets.QLabel("Линия:"))
line_mode_raw_rb = QtWidgets.QRadioButton("raw")
line_mode_proc_rb = QtWidgets.QRadioButton("processed")
line_mode_raw_rb.setChecked(True)
line_mode_layout.addWidget(line_mode_raw_rb)
line_mode_layout.addWidget(line_mode_proc_rb)
line_mode_layout.addStretch(1)
line_mode_proxy = QtWidgets.QGraphicsProxyWidget()
line_mode_proxy.setWidget(line_mode_widget)
win.addItem(line_mode_proxy, row=6, col=0, colspan=2)
def _line_mode() -> str:
return "processed" if line_mode_proc_rb.isChecked() else "raw"
# Статусная строка
status = pg.LabelItem(justify="left")
win.addItem(status, row=3, col=0, colspan=2)
pipeline_status = pg.LabelItem(justify="left")
win.addItem(pipeline_status, row=4, col=0, colspan=2)
ref_status = pg.LabelItem(justify="left")
win.addItem(ref_status, row=5, col=0, colspan=2)
def _refresh_pipeline_label():
txt = state.format_pipeline_status()
trace = state.format_stage_trace()
if trace:
txt = f"{txt} | trace: {trace}"
pipeline_status.setText(txt)
ref_status.setText(state.format_reference_status())
_refresh_calib_controls()
_refresh_bg_controls()
_refresh_pipeline_label()
_imshow_initialized = [False]
@ -293,28 +494,46 @@ def run_pyqtgraph(args):
if changed and not _imshow_initialized[0] and ring.is_ready:
_init_imshow_extents()
_imshow_initialized[0] = True
if changed:
_refresh_calib_controls()
_refresh_bg_controls()
_refresh_pipeline_label()
# Линейный график свипа
if state.current_sweep_raw is not None and ring.x_shared is not None:
raw = state.current_sweep_raw
xs = ring.x_shared[: raw.size] if raw.size <= ring.x_shared.size else np.arange(raw.size)
curve.setData(xs, raw, autoDownsample=True)
if state.calib_mode == "file" and state.calib_file_envelope is not None:
upper = state.calib_file_envelope
lower = -upper
m_env = float(np.nanmax(np.abs(upper)))
if m_env <= 0.0:
m_env = 1.0
curve_env_lo.setData(xs[: upper.size], lower / m_env, autoDownsample=True)
curve_env_hi.setData(xs[: upper.size], upper / m_env, autoDownsample=True)
elif state.last_calib_sweep is not None:
calib = state.last_calib_sweep
m_calib = float(np.nanmax(np.abs(calib)))
if m_calib <= 0.0:
m_calib = 1.0
lower, upper = build_calib_envelopes(calib)
curve_env_lo.setData(xs[: calib.size], lower / m_calib, autoDownsample=True)
curve_env_hi.setData(xs[: calib.size], upper / m_calib, autoDownsample=True)
line_mode = _line_mode()
main = state.current_sweep_processed if line_mode == "processed" else raw
if main is not None:
curve.setData(xs[: main.size], main, autoDownsample=True)
else:
curve.setData([], [])
if line_mode == "raw":
if state.calib_mode == "file" and state.calib_file_envelope is not None:
upper = np.asarray(state.calib_file_envelope, dtype=np.float32)
n_env = min(xs.size, upper.size)
if n_env > 0:
x_env = xs[:n_env]
y_env = upper[:n_env]
curve_env_lo.setData(x_env, -y_env, autoDownsample=True)
curve_env_hi.setData(x_env, y_env, autoDownsample=True)
else:
curve_env_lo.setData([], [])
curve_env_hi.setData([], [])
elif state.last_calib_sweep is not None:
calib = np.asarray(state.last_calib_sweep, dtype=np.float32)
lower, upper = build_calib_envelopes(calib)
n_env = min(xs.size, upper.size, lower.size)
if n_env > 0:
curve_env_lo.setData(xs[:n_env], lower[:n_env], autoDownsample=True)
curve_env_hi.setData(xs[:n_env], upper[:n_env], autoDownsample=True)
else:
curve_env_lo.setData([], [])
curve_env_hi.setData([], [])
else:
curve_env_lo.setData([], [])
curve_env_hi.setData([], [])
else:
curve_env_lo.setData([], [])
curve_env_hi.setData([], [])
@ -328,16 +547,19 @@ def run_pyqtgraph(args):
post = state.current_sweep_post_exp if state.current_sweep_post_exp is not None else raw
curve_post_exp.setData(xs[: post.size], post, autoDownsample=True)
if state.current_sweep_processed is not None:
proc = state.current_sweep_processed
curve.setData(xs[: proc.size], proc, autoDownsample=True)
if line_mode == "processed":
if state.current_sweep_processed is not None:
proc = state.current_sweep_processed
curve.setData(xs[: proc.size], proc, autoDownsample=True)
else:
curve.setData([], [])
else:
curve.setData([], [])
curve.setData(xs[: raw.size], raw, autoDownsample=True)
curve_norm.setData([], [])
else:
curve_pre_exp.setData([], [])
curve_post_exp.setData([], [])
if state.current_sweep_norm is not None:
if line_mode == "raw" and state.current_sweep_norm is not None:
curve_norm.setData(
xs[: state.current_sweep_norm.size],
state.current_sweep_norm,
@ -385,6 +607,8 @@ def run_pyqtgraph(args):
except Exception:
pass
ch_text.setText(state.format_channel_label())
elif changed:
_refresh_pipeline_label()
# Водопад спектров — новые данные справа (без реверса)
if changed and ring.is_ready: