diff --git a/background.npy b/background.npy new file mode 100644 index 0000000..c3d18da Binary files /dev/null and b/background.npy differ diff --git a/rfg_adc_plotter/gui/matplotlib_backend.py b/rfg_adc_plotter/gui/matplotlib_backend.py index e7f737f..b936a22 100644 --- a/rfg_adc_plotter/gui/matplotlib_backend.py +++ b/rfg_adc_plotter/gui/matplotlib_backend.py @@ -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 CALIB_ENVELOPE_PATH, AppState, format_status +from rfg_adc_plotter.state.app_state import BACKGROUND_PATH, CALIB_ENVELOPE_PATH, AppState, format_status from rfg_adc_plotter.state.ring_buffer import RingBuffer from rfg_adc_plotter.types import SweepPacket @@ -204,6 +204,24 @@ def run_matplotlib(args): state.set_calib_enabled(bool(calib_cb.get_status()[0])) fig.canvas.draw_idle() + 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]) + + def _on_save_bg(_event): + ok = state.save_background() + if ok: + state.load_background() + fig.canvas.draw_idle() + + def _on_bg_clicked(_v): + state.set_background_enabled(bool(bg_cb.get_status()[0])) + + save_bg_btn.on_clicked(_on_save_bg) + bg_cb.on_clicked(_on_bg_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()) diff --git a/rfg_adc_plotter/gui/pyqtgraph_backend.py b/rfg_adc_plotter/gui/pyqtgraph_backend.py index ed76229..97aa872 100644 --- a/rfg_adc_plotter/gui/pyqtgraph_backend.py +++ b/rfg_adc_plotter/gui/pyqtgraph_backend.py @@ -10,7 +10,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 CALIB_ENVELOPE_PATH, AppState, format_status +from rfg_adc_plotter.state.app_state import BACKGROUND_PATH, CALIB_ENVELOPE_PATH, AppState, format_status from rfg_adc_plotter.state.ring_buffer import RingBuffer from rfg_adc_plotter.types import SweepPacket @@ -225,6 +225,32 @@ def run_pyqtgraph(args): calib_cb.stateChanged.connect(_on_calib_toggled) calib_file_cb.stateChanged.connect(lambda _v: _on_calib_file_toggled(calib_file_cb.isChecked())) + # Кнопка сохранения фона + чекбокс вычета фона + bg_widget = QtWidgets.QWidget() + bg_layout = QtWidgets.QHBoxLayout(bg_widget) + bg_layout.setContentsMargins(2, 2, 2, 2) + bg_layout.setSpacing(8) + + save_bg_btn = QtWidgets.QPushButton("Сохр. фон") + bg_cb = QtWidgets.QCheckBox("вычет фона") + bg_cb.setEnabled(False) + + bg_layout.addWidget(save_bg_btn) + bg_layout.addWidget(bg_cb) + + bg_container_proxy = QtWidgets.QGraphicsProxyWidget() + bg_container_proxy.setWidget(bg_widget) + win.addItem(bg_container_proxy, row=2, col=0) + + def _on_save_bg(): + ok = state.save_background() + if ok: + state.load_background() + bg_cb.setEnabled(True) + + save_bg_btn.clicked.connect(_on_save_bg) + bg_cb.stateChanged.connect(lambda _v: state.set_background_enabled(bg_cb.isChecked())) + # Статусная строка status = pg.LabelItem(justify="left") win.addItem(status, row=3, col=0, colspan=2) diff --git a/rfg_adc_plotter/state/app_state.py b/rfg_adc_plotter/state/app_state.py index 229b273..0530fb4 100644 --- a/rfg_adc_plotter/state/app_state.py +++ b/rfg_adc_plotter/state/app_state.py @@ -15,6 +15,7 @@ from rfg_adc_plotter.state.ring_buffer import RingBuffer from rfg_adc_plotter.types import SweepInfo, SweepPacket CALIB_ENVELOPE_PATH = "calib_envelope.npy" +BACKGROUND_PATH = "background.npy" def format_status(data: Mapping[str, Any]) -> str: @@ -54,6 +55,10 @@ class AppState: # "live" — нормировка по текущему ch0-свипу; "file" — по огибающей из файла self.calib_mode: str = "live" self.calib_file_envelope: Optional[np.ndarray] = None + # Вычет фона + self.background: Optional[np.ndarray] = None + self.background_enabled: bool = False + self._last_sweep_for_ring: Optional[np.ndarray] = None def _normalize(self, raw: np.ndarray, calib: np.ndarray) -> np.ndarray: if self.calib_mode == "file" and self.calib_file_envelope is not None: @@ -96,6 +101,43 @@ class AppState: """Переключить режим калибровки: 'live' или 'file'.""" self.calib_mode = mode + def save_background(self, path: str = BACKGROUND_PATH) -> bool: + """Сохранить текущий sweep_for_ring как фоновый спектр. + + Сохраняет последний свип, который был записан в ринг-буфер + (нормированный, если калибровка включена, иначе сырой). + Возвращает True при успехе. + """ + if self._last_sweep_for_ring is None: + return False + try: + np.save(path, self._last_sweep_for_ring) + return True + except Exception as exc: + import sys + sys.stderr.write(f"[warn] Не удалось сохранить фон: {exc}\n") + return False + + def load_background(self, path: str = BACKGROUND_PATH) -> bool: + """Загрузить фоновый спектр из файла. + + Возвращает True при успехе. + """ + if not os.path.isfile(path): + return False + try: + bg = np.load(path) + self.background = np.asarray(bg, dtype=np.float32) + return True + except Exception as exc: + import sys + sys.stderr.write(f"[warn] Не удалось загрузить фон: {exc}\n") + return False + + def set_background_enabled(self, enabled: bool): + """Включить/выключить вычет фона.""" + self.background_enabled = enabled + def set_calib_enabled(self, enabled: bool): """Включить/выключить режим калибровки, пересчитать norm-свип.""" self.calib_enabled = enabled @@ -140,6 +182,7 @@ class AppState: self.save_calib_envelope() self.current_sweep_norm = None sweep_for_ring = s + self._last_sweep_for_ring = sweep_for_ring else: can_normalize = self.calib_enabled and ( (self.calib_mode == "file" and self.calib_file_envelope is not None) @@ -153,6 +196,14 @@ class AppState: self.current_sweep_norm = None sweep_for_ring = s + # Вычет фона (в том же домене что и sweep_for_ring) + if self.background_enabled and self.background is not None and ch != 0: + w = min(sweep_for_ring.size, self.background.size) + sweep_for_ring = sweep_for_ring.copy() + sweep_for_ring[:w] -= self.background[:w] + self.current_sweep_norm = sweep_for_ring + + self._last_sweep_for_ring = sweep_for_ring ring.ensure_init(s.size) ring.push(sweep_for_ring) return drained