diff --git a/RFG_ADC_dataplotter.py b/RFG_ADC_dataplotter.py index d9c286d..75c3d55 100755 --- a/RFG_ADC_dataplotter.py +++ b/RFG_ADC_dataplotter.py @@ -1704,7 +1704,10 @@ def main(): ymax_slider = None contrast_slider = None calib_enabled = False + bg_compute_enabled = True bg_subtract_enabled = False + fft_bg_subtract_enabled = False + bg_spec_cache: Optional[np.ndarray] = None peak_calibrate_mode = bool(getattr(args, "calibrate", False)) peak_search_enabled = bool(getattr(args, "peak_search", False)) peak_ref_window = float(getattr(args, "peak_ref_window", 1.0)) @@ -1814,15 +1817,19 @@ def main(): return _normalize_by_calib(raw, calib, norm_type=norm_type) def _sync_checkbox_states(): - nonlocal calib_enabled, bg_subtract_enabled, peak_search_enabled, current_sweep_norm + nonlocal calib_enabled, bg_compute_enabled, bg_subtract_enabled, fft_bg_subtract_enabled, peak_search_enabled, current_sweep_norm try: 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 - peak_search_enabled = bool(states[2]) if len(states) > 2 else False + bg_compute_enabled = bool(states[1]) if len(states) > 1 else True + bg_subtract_enabled = bool(states[2]) if len(states) > 2 else False + fft_bg_subtract_enabled = bool(states[3]) if len(states) > 3 else False + peak_search_enabled = bool(states[4]) if len(states) > 4 else False except Exception: calib_enabled = False + bg_compute_enabled = True bg_subtract_enabled = False + fft_bg_subtract_enabled = False peak_search_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) @@ -1834,14 +1841,14 @@ 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.90, 0.37, 0.10, 0.17]) + ax_cb = fig.add_axes([0.90, 0.30, 0.10, 0.24]) 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, False, peak_search_enabled], + ["нормировка", "расчет фона", "вычет фона", "FFT вычет фона", "поиск пиков"], + [False, True, False, False, peak_search_enabled], ) def _on_ylim_change(_val): @@ -2155,13 +2162,19 @@ 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]: + def _visible_bg_fft(disp_fft: np.ndarray, force: bool = False) -> Optional[np.ndarray]: """Оценка фона по медиане в текущем видимом по времени окне B-scan.""" - if not bg_subtract_enabled or disp_fft.size == 0: + nonlocal bg_spec_cache + need_bg = bool(bg_subtract_enabled or force) + if (not need_bg) or disp_fft.size == 0: return None ny, nx = disp_fft.shape if ny <= 0 or nx <= 0: - return None + return bg_spec_cache + if bg_spec_cache is not None and bg_spec_cache.size != ny: + bg_spec_cache = None + if not bg_compute_enabled: + return bg_spec_cache try: x0, x1 = ax_spec.get_xlim() except Exception: @@ -2173,14 +2186,15 @@ def main(): ix1 = ix0 window = disp_fft[:, ix0 : ix1 + 1] if window.size == 0: - return None + return bg_spec_cache try: bg_spec = np.nanmedian(window, axis=1) except Exception: - return None + return bg_spec_cache 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) + return bg_spec_cache + bg_spec_cache = np.nan_to_num(bg_spec, nan=0.0).astype(np.float32, copy=False) + return bg_spec_cache def make_display_ring_fft(): if ring_fft is None: @@ -2211,6 +2225,14 @@ def main(): channel_text, ) changed = drain_queue() > 0 + bg_fft_for_line = None + if fft_bg_subtract_enabled and ring_fft is not None: + try: + disp_fft_for_bg = make_display_ring_fft() + disp_fft_for_bg = _subtract_recent_mean_fft(disp_fft_for_bg) + bg_fft_for_line = _visible_bg_fft(disp_fft_for_bg, force=True) + except Exception: + bg_fft_for_line = None # Обновление линии последнего свипа if current_sweep_raw is not None: @@ -2258,6 +2280,19 @@ def main(): if fft_vals.size > xs_fft.size: fft_vals = fft_vals[: xs_fft.size] xs_fft = xs_fft[: fft_vals.size] + if fft_bg_subtract_enabled and bg_fft_for_line is not None: + n_bg = int(min(fft_vals.size, bg_fft_for_line.size)) + if n_bg > 0: + num = np.maximum( + np.power(10.0, np.asarray(fft_vals[:n_bg], dtype=np.float64) / 20.0), + 0.0, + ) + den = np.maximum(np.asarray(bg_fft_for_line[:n_bg], dtype=np.float64), 0.0) + fft_vals = (20.0 * np.log10((num + 1e-9) / (den + 1e-9))).astype( + np.float32, + copy=False, + ) + xs_fft = xs_fft[:n_bg] fft_line_obj.set_data(xs_fft, fft_vals) finite_fft = np.isfinite(xs_fft) & np.isfinite(fft_vals) @@ -2291,7 +2326,12 @@ def main(): box_obj.set_visible(False) # Авто-диапазон по Y для спектра - if np.any(finite_fft): + if fft_bg_subtract_enabled and bg_fft_for_line is not None: + finite_x = xs_fft[np.isfinite(xs_fft)] + if finite_x.size > 0: + ax_fft.set_xlim(float(np.min(finite_x)), float(np.max(finite_x))) + ax_fft.set_ylim(-10.0, 30.0) + elif np.any(finite_fft): finite_x = xs_fft[finite_fft] if finite_x.size > 0: ax_fft.set_xlim(float(np.min(finite_x)), float(np.max(finite_x))) @@ -2633,7 +2673,9 @@ def run_pyqtgraph(args): # Правая панель настроек внутри основного окна. calib_cb = QtWidgets.QCheckBox("нормировка") + bg_compute_cb = QtWidgets.QCheckBox("расчет фона") bg_subtract_cb = QtWidgets.QCheckBox("вычет фона") + fft_bg_subtract_cb = QtWidgets.QCheckBox("FFT вычет фона") peak_search_cb = QtWidgets.QCheckBox("поиск пиков") try: settings_title = QtWidgets.QLabel("Настройки") @@ -2641,7 +2683,9 @@ def run_pyqtgraph(args): except Exception: pass settings_layout.addWidget(calib_cb) + settings_layout.addWidget(bg_compute_cb) settings_layout.addWidget(bg_subtract_cb) + settings_layout.addWidget(fft_bg_subtract_cb) settings_layout.addWidget(peak_search_cb) calib_window = None @@ -2673,7 +2717,10 @@ 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_compute_enabled = True bg_subtract_enabled = False + fft_bg_subtract_enabled = False + bg_spec_cache: Optional[np.ndarray] = None current_peak_width: Optional[float] = None current_peak_amplitude: Optional[float] = None peak_candidates: List[Dict[str, float]] = [] @@ -2721,14 +2768,44 @@ def run_pyqtgraph(args): bg_subtract_enabled = False plot_dirty = True + def _set_bg_compute_enabled(): + nonlocal bg_compute_enabled, plot_dirty + try: + bg_compute_enabled = bool(bg_compute_cb.isChecked()) + except Exception: + bg_compute_enabled = False + plot_dirty = True + + def _set_fft_bg_subtract_enabled(): + nonlocal fft_bg_subtract_enabled, plot_dirty + try: + fft_bg_subtract_enabled = bool(fft_bg_subtract_cb.isChecked()) + except Exception: + fft_bg_subtract_enabled = False + plot_dirty = True + + try: + bg_compute_cb.setChecked(True) + except Exception: + pass + _set_bg_compute_enabled() + try: calib_cb.stateChanged.connect(lambda _v: _set_calib_enabled()) except Exception: pass + try: + bg_compute_cb.stateChanged.connect(lambda _v: _set_bg_compute_enabled()) + except Exception: + pass try: bg_subtract_cb.stateChanged.connect(lambda _v: _set_bg_subtract_enabled()) except Exception: pass + try: + fft_bg_subtract_cb.stateChanged.connect(lambda _v: _set_fft_bg_subtract_enabled()) + except Exception: + pass def _refresh_peak_params_label(peaks: List[Dict[str, float]]): if peak_params_label is None: @@ -2959,13 +3036,19 @@ def run_pyqtgraph(args): return None return (vmin, vmax) - def _visible_bg_fft(disp_fft: np.ndarray) -> Optional[np.ndarray]: + def _visible_bg_fft(disp_fft: np.ndarray, force: bool = False) -> Optional[np.ndarray]: """Оценка фона по медиане в текущем видимом по времени окне B-scan.""" - if not bg_subtract_enabled or disp_fft.size == 0: + nonlocal bg_spec_cache + need_bg = bool(bg_subtract_enabled or force) + if (not need_bg) or disp_fft.size == 0: return None ny, nx = disp_fft.shape if ny <= 0 or nx <= 0: - return None + return bg_spec_cache + if bg_spec_cache is not None and bg_spec_cache.size != ny: + bg_spec_cache = None + if not bg_compute_enabled: + return bg_spec_cache try: (x0, x1), _ = p_spec.viewRange() except Exception: @@ -2977,14 +3060,15 @@ def run_pyqtgraph(args): ix1 = ix0 window = disp_fft[:, ix0 : ix1 + 1] if window.size == 0: - return None + return bg_spec_cache try: bg_spec = np.nanmedian(window, axis=1) except Exception: - return None + return bg_spec_cache 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) + return bg_spec_cache + bg_spec_cache = np.nan_to_num(bg_spec, nan=0.0).astype(np.float32, copy=False) + return bg_spec_cache def push_sweep(s: np.ndarray, freqs: Optional[np.ndarray] = None): nonlocal ring, ring_time, head, ring_fft, y_min_fft, y_max_fft, current_fft_db @@ -3075,6 +3159,13 @@ def run_pyqtgraph(args): return changed = drain_queue() > 0 redraw_needed = changed or plot_dirty + bg_fft_for_line = None + if redraw_needed and fft_bg_subtract_enabled and ring_fft is not None: + try: + disp_fft_for_bg = ring_fft if head == 0 else np.roll(ring_fft, -head, axis=0) + bg_fft_for_line = _visible_bg_fft(disp_fft_for_bg.T, force=True) + except Exception: + bg_fft_for_line = None if redraw_needed and current_sweep_raw is not None and x_shared is not None: if current_freqs is not None and current_freqs.size == current_sweep_raw.size: xs = current_freqs @@ -3124,6 +3215,19 @@ def run_pyqtgraph(args): if fft_vals.size > xs_fft.size: fft_vals = fft_vals[: xs_fft.size] xs_fft = xs_fft[: fft_vals.size] + if fft_bg_subtract_enabled and bg_fft_for_line is not None: + n_bg = int(min(fft_vals.size, bg_fft_for_line.size)) + if n_bg > 0: + num = np.maximum( + np.power(10.0, np.asarray(fft_vals[:n_bg], dtype=np.float64) / 20.0), + 0.0, + ) + den = np.maximum(np.asarray(bg_fft_for_line[:n_bg], dtype=np.float64), 0.0) + fft_vals = (20.0 * np.log10((num + 1e-9) / (den + 1e-9))).astype( + np.float32, + copy=False, + ) + xs_fft = xs_fft[:n_bg] curve_fft.setData(xs_fft, fft_vals) finite_x = xs_fft[np.isfinite(xs_fft)] if finite_x.size > 0: @@ -3159,13 +3263,16 @@ def run_pyqtgraph(args): for box in fft_peak_boxes: box.setVisible(False) - finite_y = y_for_range[np.isfinite(y_for_range)] - if finite_y.size > 0: - y0 = float(np.min(finite_y)) - y1 = float(np.max(finite_y)) - if y1 <= y0: - y1 = y0 + 1e-3 - p_fft.setYRange(y0, y1, padding=0) + if fft_bg_subtract_enabled and bg_fft_for_line is not None: + p_fft.setYRange(-10.0, 30.0, padding=0) + else: + finite_y = y_for_range[np.isfinite(y_for_range)] + if finite_y.size > 0: + y0 = float(np.min(finite_y)) + y1 = float(np.max(finite_y)) + if y1 <= y0: + y1 = y0 + 1e-3 + p_fft.setYRange(y0, y1, padding=0) if peak_calibrate_mode: markers = _find_peak_width_markers(xs_fft, fft_vals) if markers is not None: