implemented background referencing and subtraction if from FFT window and B-scan. Continous ref calculation can be toggled

This commit is contained in:
2026-03-10 15:28:20 +03:00
parent 6260d10c4f
commit 8e9ffb3de7

View File

@ -1704,7 +1704,10 @@ def main():
ymax_slider = None ymax_slider = None
contrast_slider = None contrast_slider = None
calib_enabled = False calib_enabled = False
bg_compute_enabled = True
bg_subtract_enabled = False 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_calibrate_mode = bool(getattr(args, "calibrate", False))
peak_search_enabled = bool(getattr(args, "peak_search", False)) peak_search_enabled = bool(getattr(args, "peak_search", False))
peak_ref_window = float(getattr(args, "peak_ref_window", 1.0)) 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) return _normalize_by_calib(raw, calib, norm_type=norm_type)
def _sync_checkbox_states(): 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: try:
states = cb.get_status() if cb is not None else () states = cb.get_status() if cb is not None else ()
calib_enabled = bool(states[0]) if len(states) > 0 else False calib_enabled = bool(states[0]) if len(states) > 0 else False
bg_subtract_enabled = bool(states[1]) if len(states) > 1 else False bg_compute_enabled = bool(states[1]) if len(states) > 1 else True
peak_search_enabled = bool(states[2]) if len(states) > 2 else False 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: except Exception:
calib_enabled = False calib_enabled = False
bg_compute_enabled = True
bg_subtract_enabled = False bg_subtract_enabled = False
fft_bg_subtract_enabled = False
peak_search_enabled = False peak_search_enabled = False
if calib_enabled and current_sweep_raw is not None and last_calib_sweep is not None: 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) 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_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_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_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") 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") 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") contrast_slider = Slider(ax_sctr, "Int max", 0, 100, valinit=100, valstep=1, orientation="vertical")
cb = CheckButtons( cb = CheckButtons(
ax_cb, ax_cb,
["нормировка", "вычет фона", "поиск пиков"], ["нормировка", "расчет фона", "вычет фона", "FFT вычет фона", "поиск пиков"],
[False, False, peak_search_enabled], [False, True, False, False, peak_search_enabled],
) )
def _on_ylim_change(_val): def _on_ylim_change(_val):
@ -2155,13 +2162,19 @@ def main():
mean_spec = np.nan_to_num(mean_spec, nan=0.0) mean_spec = np.nan_to_num(mean_spec, nan=0.0)
return disp_fft - mean_spec[:, None] 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.""" """Оценка фона по медиане в текущем видимом по времени окне 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 return None
ny, nx = disp_fft.shape ny, nx = disp_fft.shape
if ny <= 0 or nx <= 0: 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: try:
x0, x1 = ax_spec.get_xlim() x0, x1 = ax_spec.get_xlim()
except Exception: except Exception:
@ -2173,14 +2186,15 @@ def main():
ix1 = ix0 ix1 = ix0
window = disp_fft[:, ix0 : ix1 + 1] window = disp_fft[:, ix0 : ix1 + 1]
if window.size == 0: if window.size == 0:
return None return bg_spec_cache
try: try:
bg_spec = np.nanmedian(window, axis=1) bg_spec = np.nanmedian(window, axis=1)
except Exception: except Exception:
return None return bg_spec_cache
if not np.any(np.isfinite(bg_spec)): if not np.any(np.isfinite(bg_spec)):
return None return bg_spec_cache
return np.nan_to_num(bg_spec, nan=0.0).astype(np.float32, copy=False) 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(): def make_display_ring_fft():
if ring_fft is None: if ring_fft is None:
@ -2211,6 +2225,14 @@ def main():
channel_text, channel_text,
) )
changed = drain_queue() > 0 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: if current_sweep_raw is not None:
@ -2258,6 +2280,19 @@ def main():
if fft_vals.size > xs_fft.size: if fft_vals.size > xs_fft.size:
fft_vals = fft_vals[: xs_fft.size] fft_vals = fft_vals[: xs_fft.size]
xs_fft = xs_fft[: fft_vals.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) fft_line_obj.set_data(xs_fft, fft_vals)
finite_fft = np.isfinite(xs_fft) & np.isfinite(fft_vals) finite_fft = np.isfinite(xs_fft) & np.isfinite(fft_vals)
@ -2291,7 +2326,12 @@ def main():
box_obj.set_visible(False) box_obj.set_visible(False)
# Авто-диапазон по Y для спектра # Авто-диапазон по 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] finite_x = xs_fft[finite_fft]
if finite_x.size > 0: if finite_x.size > 0:
ax_fft.set_xlim(float(np.min(finite_x)), float(np.max(finite_x))) 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("нормировка") calib_cb = QtWidgets.QCheckBox("нормировка")
bg_compute_cb = QtWidgets.QCheckBox("расчет фона")
bg_subtract_cb = QtWidgets.QCheckBox("вычет фона") bg_subtract_cb = QtWidgets.QCheckBox("вычет фона")
fft_bg_subtract_cb = QtWidgets.QCheckBox("FFT вычет фона")
peak_search_cb = QtWidgets.QCheckBox("поиск пиков") peak_search_cb = QtWidgets.QCheckBox("поиск пиков")
try: try:
settings_title = QtWidgets.QLabel("Настройки") settings_title = QtWidgets.QLabel("Настройки")
@ -2641,7 +2683,9 @@ def run_pyqtgraph(args):
except Exception: except Exception:
pass pass
settings_layout.addWidget(calib_cb) settings_layout.addWidget(calib_cb)
settings_layout.addWidget(bg_compute_cb)
settings_layout.addWidget(bg_subtract_cb) settings_layout.addWidget(bg_subtract_cb)
settings_layout.addWidget(fft_bg_subtract_cb)
settings_layout.addWidget(peak_search_cb) settings_layout.addWidget(peak_search_cb)
calib_window = None calib_window = None
@ -2673,7 +2717,10 @@ def run_pyqtgraph(args):
spec_clip = _parse_spec_clip(getattr(args, "spec_clip", None)) spec_clip = _parse_spec_clip(getattr(args, "spec_clip", None))
spec_mean_sec = float(getattr(args, "spec_mean_sec", 0.0)) spec_mean_sec = float(getattr(args, "spec_mean_sec", 0.0))
calib_enabled = False calib_enabled = False
bg_compute_enabled = True
bg_subtract_enabled = False bg_subtract_enabled = False
fft_bg_subtract_enabled = False
bg_spec_cache: Optional[np.ndarray] = None
current_peak_width: Optional[float] = None current_peak_width: Optional[float] = None
current_peak_amplitude: Optional[float] = None current_peak_amplitude: Optional[float] = None
peak_candidates: List[Dict[str, float]] = [] peak_candidates: List[Dict[str, float]] = []
@ -2721,14 +2768,44 @@ def run_pyqtgraph(args):
bg_subtract_enabled = False bg_subtract_enabled = False
plot_dirty = True 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: try:
calib_cb.stateChanged.connect(lambda _v: _set_calib_enabled()) calib_cb.stateChanged.connect(lambda _v: _set_calib_enabled())
except Exception: except Exception:
pass pass
try:
bg_compute_cb.stateChanged.connect(lambda _v: _set_bg_compute_enabled())
except Exception:
pass
try: try:
bg_subtract_cb.stateChanged.connect(lambda _v: _set_bg_subtract_enabled()) bg_subtract_cb.stateChanged.connect(lambda _v: _set_bg_subtract_enabled())
except Exception: except Exception:
pass 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]]): def _refresh_peak_params_label(peaks: List[Dict[str, float]]):
if peak_params_label is None: if peak_params_label is None:
@ -2959,13 +3036,19 @@ def run_pyqtgraph(args):
return None return None
return (vmin, vmax) 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.""" """Оценка фона по медиане в текущем видимом по времени окне 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 return None
ny, nx = disp_fft.shape ny, nx = disp_fft.shape
if ny <= 0 or nx <= 0: 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: try:
(x0, x1), _ = p_spec.viewRange() (x0, x1), _ = p_spec.viewRange()
except Exception: except Exception:
@ -2977,14 +3060,15 @@ def run_pyqtgraph(args):
ix1 = ix0 ix1 = ix0
window = disp_fft[:, ix0 : ix1 + 1] window = disp_fft[:, ix0 : ix1 + 1]
if window.size == 0: if window.size == 0:
return None return bg_spec_cache
try: try:
bg_spec = np.nanmedian(window, axis=1) bg_spec = np.nanmedian(window, axis=1)
except Exception: except Exception:
return None return bg_spec_cache
if not np.any(np.isfinite(bg_spec)): if not np.any(np.isfinite(bg_spec)):
return None return bg_spec_cache
return np.nan_to_num(bg_spec, nan=0.0).astype(np.float32, copy=False) 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): 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 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 return
changed = drain_queue() > 0 changed = drain_queue() > 0
redraw_needed = changed or plot_dirty 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 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: if current_freqs is not None and current_freqs.size == current_sweep_raw.size:
xs = current_freqs xs = current_freqs
@ -3124,6 +3215,19 @@ def run_pyqtgraph(args):
if fft_vals.size > xs_fft.size: if fft_vals.size > xs_fft.size:
fft_vals = fft_vals[: xs_fft.size] fft_vals = fft_vals[: xs_fft.size]
xs_fft = xs_fft[: fft_vals.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) curve_fft.setData(xs_fft, fft_vals)
finite_x = xs_fft[np.isfinite(xs_fft)] finite_x = xs_fft[np.isfinite(xs_fft)]
if finite_x.size > 0: if finite_x.size > 0:
@ -3159,13 +3263,16 @@ def run_pyqtgraph(args):
for box in fft_peak_boxes: for box in fft_peak_boxes:
box.setVisible(False) box.setVisible(False)
finite_y = y_for_range[np.isfinite(y_for_range)] if fft_bg_subtract_enabled and bg_fft_for_line is not None:
if finite_y.size > 0: p_fft.setYRange(-10.0, 30.0, padding=0)
y0 = float(np.min(finite_y)) else:
y1 = float(np.max(finite_y)) finite_y = y_for_range[np.isfinite(y_for_range)]
if y1 <= y0: if finite_y.size > 0:
y1 = y0 + 1e-3 y0 = float(np.min(finite_y))
p_fft.setYRange(y0, y1, padding=0) y1 = float(np.max(finite_y))
if y1 <= y0:
y1 = y0 + 1e-3
p_fft.setYRange(y0, y1, padding=0)
if peak_calibrate_mode: if peak_calibrate_mode:
markers = _find_peak_width_markers(xs_fft, fft_vals) markers = _find_peak_width_markers(xs_fft, fft_vals)
if markers is not None: if markers is not None: