new background remove algoritm
This commit is contained in:
@ -2,6 +2,7 @@
|
||||
|
||||
WF_WIDTH = 1000
|
||||
FFT_LEN = 1024
|
||||
BACKGROUND_MEDIAN_SWEEPS = 64
|
||||
|
||||
SWEEP_FREQ_MIN_GHZ = 3.3
|
||||
SWEEP_FREQ_MAX_GHZ = 14.3
|
||||
|
||||
@ -12,6 +12,11 @@ import numpy as np
|
||||
|
||||
from rfg_adc_plotter.constants import FFT_LEN, SWEEP_FREQ_MAX_GHZ, SWEEP_FREQ_MIN_GHZ
|
||||
from rfg_adc_plotter.io.sweep_reader import SweepReader
|
||||
from rfg_adc_plotter.processing.background import (
|
||||
load_fft_background,
|
||||
save_fft_background,
|
||||
subtract_fft_background,
|
||||
)
|
||||
from rfg_adc_plotter.processing.calibration import (
|
||||
build_calib_envelope,
|
||||
calibrate_freqs,
|
||||
@ -21,7 +26,7 @@ from rfg_adc_plotter.processing.calibration import (
|
||||
save_calib_envelope,
|
||||
set_calibration_base_value,
|
||||
)
|
||||
from rfg_adc_plotter.processing.fft import compute_fft_row, fft_mag_to_db
|
||||
from rfg_adc_plotter.processing.fft import compute_fft_mag_row, fft_mag_to_db
|
||||
from rfg_adc_plotter.processing.formatting import compute_auto_ylim, format_status_kv, parse_spec_clip
|
||||
from rfg_adc_plotter.processing.normalization import normalize_by_envelope, resample_envelope
|
||||
from rfg_adc_plotter.processing.peaks import (
|
||||
@ -248,9 +253,6 @@ def run_pyqtgraph(args) -> None:
|
||||
spec_right_line.setVisible(False)
|
||||
|
||||
calib_cb = QtWidgets.QCheckBox("калибровка по огибающей")
|
||||
bg_compute_cb = QtWidgets.QCheckBox("расчет фона")
|
||||
bg_subtract_cb = QtWidgets.QCheckBox("вычет фона")
|
||||
fft_bg_subtract_cb = QtWidgets.QCheckBox("FFT вычет фона")
|
||||
range_group = QtWidgets.QGroupBox("Рабочий диапазон")
|
||||
range_group_layout = QtWidgets.QFormLayout(range_group)
|
||||
range_group_layout.setContentsMargins(6, 6, 6, 6)
|
||||
@ -300,15 +302,39 @@ def run_pyqtgraph(args) -> None:
|
||||
calib_buttons_row.addWidget(calib_save_btn)
|
||||
calib_buttons_row.addWidget(calib_load_btn)
|
||||
calib_group_layout.addLayout(calib_buttons_row)
|
||||
background_group = QtWidgets.QGroupBox("Фон")
|
||||
background_group_layout = QtWidgets.QVBoxLayout(background_group)
|
||||
background_group_layout.setContentsMargins(6, 6, 6, 6)
|
||||
background_group_layout.setSpacing(6)
|
||||
background_cb = QtWidgets.QCheckBox("вычитание фона")
|
||||
background_group_layout.addWidget(background_cb)
|
||||
background_path_edit = QtWidgets.QLineEdit("fft_background.npy")
|
||||
try:
|
||||
background_path_edit.setPlaceholderText("fft_background.npy")
|
||||
except Exception:
|
||||
pass
|
||||
background_path_row = QtWidgets.QHBoxLayout()
|
||||
background_path_row.setContentsMargins(0, 0, 0, 0)
|
||||
background_path_row.setSpacing(4)
|
||||
background_path_row.addWidget(background_path_edit)
|
||||
background_pick_btn = QtWidgets.QPushButton("Файл...")
|
||||
background_path_row.addWidget(background_pick_btn)
|
||||
background_group_layout.addLayout(background_path_row)
|
||||
background_buttons_row = QtWidgets.QHBoxLayout()
|
||||
background_buttons_row.setContentsMargins(0, 0, 0, 0)
|
||||
background_buttons_row.setSpacing(4)
|
||||
background_save_btn = QtWidgets.QPushButton("Сохранить медиану")
|
||||
background_load_btn = QtWidgets.QPushButton("Загрузить")
|
||||
background_buttons_row.addWidget(background_save_btn)
|
||||
background_buttons_row.addWidget(background_load_btn)
|
||||
background_group_layout.addLayout(background_buttons_row)
|
||||
try:
|
||||
settings_layout.addWidget(QtWidgets.QLabel("Настройки"))
|
||||
except Exception:
|
||||
pass
|
||||
settings_layout.addWidget(range_group)
|
||||
settings_layout.addWidget(calib_group)
|
||||
settings_layout.addWidget(bg_compute_cb)
|
||||
settings_layout.addWidget(bg_subtract_cb)
|
||||
settings_layout.addWidget(fft_bg_subtract_cb)
|
||||
settings_layout.addWidget(background_group)
|
||||
settings_layout.addWidget(fft_mode_label)
|
||||
settings_layout.addWidget(fft_mode_combo)
|
||||
settings_layout.addWidget(peak_search_cb)
|
||||
@ -317,9 +343,7 @@ def run_pyqtgraph(args) -> None:
|
||||
win.addItem(status, row=3, col=0, colspan=2)
|
||||
|
||||
calib_enabled = False
|
||||
bg_compute_enabled = True
|
||||
bg_subtract_enabled = False
|
||||
fft_bg_subtract_enabled = False
|
||||
background_enabled = False
|
||||
fft_mode = "symmetric"
|
||||
status_note = ""
|
||||
status_dirty = True
|
||||
@ -422,11 +446,35 @@ def run_pyqtgraph(args) -> None:
|
||||
path = ""
|
||||
return path or "calibration_envelope.npy"
|
||||
|
||||
def get_background_file_path() -> str:
|
||||
try:
|
||||
path = background_path_edit.text().strip()
|
||||
except Exception:
|
||||
path = ""
|
||||
return path or "fft_background.npy"
|
||||
|
||||
def reset_background_state(*, clear_profile: bool = True) -> None:
|
||||
runtime.background_buffer.reset()
|
||||
if clear_profile:
|
||||
runtime.background_profile = None
|
||||
runtime.current_fft_mag = None
|
||||
runtime.current_fft_db = None
|
||||
|
||||
def resolve_active_background(expected_size: int) -> Optional[np.ndarray]:
|
||||
if not background_enabled:
|
||||
return None
|
||||
if runtime.background_profile is None:
|
||||
return None
|
||||
if runtime.background_profile.size != int(expected_size):
|
||||
set_status_note("фон: размер профиля не совпадает с FFT")
|
||||
return None
|
||||
return runtime.background_profile
|
||||
|
||||
def reset_ring_buffers() -> None:
|
||||
runtime.ring.reset()
|
||||
runtime.current_distances = None
|
||||
runtime.current_fft_mag = None
|
||||
runtime.current_fft_db = None
|
||||
runtime.bg_spec_cache = None
|
||||
runtime.current_peak_width = None
|
||||
runtime.current_peak_amplitude = None
|
||||
runtime.peak_candidates = []
|
||||
@ -442,6 +490,7 @@ def run_pyqtgraph(args) -> None:
|
||||
runtime.current_freqs = None
|
||||
runtime.current_sweep_raw = None
|
||||
runtime.current_sweep_norm = None
|
||||
runtime.current_fft_mag = None
|
||||
runtime.current_fft_db = None
|
||||
runtime.current_distances = runtime.ring.distance_axis
|
||||
return
|
||||
@ -461,6 +510,7 @@ def run_pyqtgraph(args) -> None:
|
||||
runtime.current_freqs = None
|
||||
runtime.current_sweep_raw = None
|
||||
runtime.current_sweep_norm = None
|
||||
runtime.current_fft_mag = None
|
||||
runtime.current_fft_db = None
|
||||
runtime.current_distances = None
|
||||
set_status_note("диапазон: нет точек в выбранном окне")
|
||||
@ -479,6 +529,7 @@ def run_pyqtgraph(args) -> None:
|
||||
else:
|
||||
runtime.current_sweep_norm = None
|
||||
|
||||
runtime.current_fft_mag = None
|
||||
runtime.current_fft_db = None
|
||||
if (
|
||||
not push_to_ring
|
||||
@ -491,7 +542,10 @@ def run_pyqtgraph(args) -> None:
|
||||
ensure_buffer(runtime.current_sweep_raw.size)
|
||||
runtime.ring.push(sweep_for_processing, runtime.current_freqs)
|
||||
runtime.current_distances = runtime.ring.distance_axis
|
||||
runtime.current_fft_mag = runtime.ring.get_last_fft_linear()
|
||||
runtime.current_fft_db = runtime.ring.last_fft_db
|
||||
if runtime.current_fft_mag is not None:
|
||||
runtime.background_buffer.push(runtime.current_fft_mag)
|
||||
|
||||
def set_calib_enabled() -> None:
|
||||
nonlocal calib_enabled
|
||||
@ -501,6 +555,7 @@ def run_pyqtgraph(args) -> None:
|
||||
calib_enabled = False
|
||||
if calib_enabled and runtime.calib_envelope is None:
|
||||
set_status_note("калибровка: огибающая не загружена")
|
||||
reset_background_state(clear_profile=True)
|
||||
recompute_current_processed_sweep(push_to_ring=False)
|
||||
runtime.mark_dirty()
|
||||
|
||||
@ -530,6 +585,7 @@ def run_pyqtgraph(args) -> None:
|
||||
return
|
||||
runtime.range_min_ghz = new_min
|
||||
runtime.range_max_ghz = new_max
|
||||
reset_background_state(clear_profile=True)
|
||||
refresh_current_window(push_to_ring=True, reset_ring=True)
|
||||
set_status_note(f"диапазон: {new_min:.6g}..{new_max:.6g} GHz")
|
||||
runtime.mark_dirty()
|
||||
@ -554,6 +610,26 @@ def run_pyqtgraph(args) -> None:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def pick_background_file() -> None:
|
||||
start_path = get_background_file_path()
|
||||
try:
|
||||
selected, _ = QtWidgets.QFileDialog.getSaveFileName(
|
||||
main_window,
|
||||
"Файл фона FFT",
|
||||
start_path,
|
||||
"NumPy (*.npy);;All files (*)",
|
||||
)
|
||||
except Exception as exc:
|
||||
set_status_note(f"фон: выбор файла недоступен ({exc})")
|
||||
runtime.mark_dirty()
|
||||
return
|
||||
if not selected:
|
||||
return
|
||||
try:
|
||||
background_path_edit.setText(selected)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def save_current_calibration() -> None:
|
||||
if runtime.current_sweep_raw is None or runtime.current_sweep_raw.size == 0:
|
||||
set_status_note("калибровка: нет текущего raw-свипа")
|
||||
@ -574,6 +650,7 @@ def run_pyqtgraph(args) -> None:
|
||||
calib_path_edit.setText(saved_path)
|
||||
except Exception:
|
||||
pass
|
||||
reset_background_state(clear_profile=True)
|
||||
recompute_current_processed_sweep(push_to_ring=False)
|
||||
set_status_note(f"калибровка сохранена: {saved_path}")
|
||||
runtime.mark_dirty()
|
||||
@ -595,32 +672,67 @@ def run_pyqtgraph(args) -> None:
|
||||
calib_path_edit.setText(normalized_path)
|
||||
except Exception:
|
||||
pass
|
||||
reset_background_state(clear_profile=True)
|
||||
recompute_current_processed_sweep(push_to_ring=False)
|
||||
set_status_note(f"калибровка загружена: {normalized_path}")
|
||||
runtime.mark_dirty()
|
||||
|
||||
def set_bg_compute_enabled() -> None:
|
||||
nonlocal bg_compute_enabled
|
||||
def save_current_background() -> None:
|
||||
background = runtime.background_buffer.median()
|
||||
if background is None or background.size == 0:
|
||||
set_status_note("фон: буфер пуст, нечего сохранять")
|
||||
runtime.mark_dirty()
|
||||
return
|
||||
try:
|
||||
bg_compute_enabled = bool(bg_compute_cb.isChecked())
|
||||
saved_path = save_fft_background(get_background_file_path(), background)
|
||||
except Exception as exc:
|
||||
set_status_note(f"фон: не удалось сохранить ({exc})")
|
||||
runtime.mark_dirty()
|
||||
return
|
||||
|
||||
runtime.background_profile = np.asarray(background, dtype=np.float32).copy()
|
||||
runtime.background_file_path = saved_path
|
||||
try:
|
||||
background_path_edit.setText(saved_path)
|
||||
except Exception:
|
||||
bg_compute_enabled = False
|
||||
pass
|
||||
runtime.current_fft_db = None
|
||||
set_status_note(f"фон сохранен: {saved_path}")
|
||||
runtime.mark_dirty()
|
||||
|
||||
def set_bg_subtract_enabled() -> None:
|
||||
nonlocal bg_subtract_enabled
|
||||
def load_background_file() -> None:
|
||||
path = get_background_file_path()
|
||||
try:
|
||||
bg_subtract_enabled = bool(bg_subtract_cb.isChecked())
|
||||
background = load_fft_background(path)
|
||||
except Exception as exc:
|
||||
set_status_note(f"фон: не удалось загрузить ({exc})")
|
||||
runtime.mark_dirty()
|
||||
return
|
||||
|
||||
if background.size != fft_bins:
|
||||
set_status_note(f"фон: ожидалось {fft_bins} FFT-бинов, получено {background.size}")
|
||||
runtime.mark_dirty()
|
||||
return
|
||||
|
||||
normalized_path = path if path.lower().endswith(".npy") else f"{path}.npy"
|
||||
runtime.background_profile = np.asarray(background, dtype=np.float32).copy()
|
||||
runtime.background_file_path = normalized_path
|
||||
try:
|
||||
background_path_edit.setText(normalized_path)
|
||||
except Exception:
|
||||
bg_subtract_enabled = False
|
||||
pass
|
||||
runtime.current_fft_db = None
|
||||
set_status_note(f"фон загружен: {normalized_path}")
|
||||
runtime.mark_dirty()
|
||||
|
||||
def set_fft_bg_subtract_enabled() -> None:
|
||||
nonlocal fft_bg_subtract_enabled
|
||||
def set_background_enabled() -> None:
|
||||
nonlocal background_enabled
|
||||
try:
|
||||
fft_bg_subtract_enabled = bool(fft_bg_subtract_cb.isChecked())
|
||||
background_enabled = bool(background_cb.isChecked())
|
||||
except Exception:
|
||||
fft_bg_subtract_enabled = False
|
||||
background_enabled = False
|
||||
if background_enabled and runtime.background_profile is None:
|
||||
set_status_note("фон: профиль не загружен")
|
||||
runtime.mark_dirty()
|
||||
|
||||
def set_fft_mode() -> None:
|
||||
@ -630,7 +742,9 @@ def run_pyqtgraph(args) -> None:
|
||||
except Exception:
|
||||
fft_mode = "symmetric"
|
||||
runtime.ring.set_fft_mode(fft_mode)
|
||||
reset_background_state(clear_profile=True)
|
||||
runtime.current_distances = runtime.ring.distance_axis
|
||||
runtime.current_fft_mag = None
|
||||
runtime.current_fft_db = None
|
||||
mode_label = {
|
||||
"direct": "IFFT: обычный",
|
||||
@ -642,12 +756,11 @@ def run_pyqtgraph(args) -> None:
|
||||
runtime.mark_dirty()
|
||||
|
||||
try:
|
||||
bg_compute_cb.setChecked(True)
|
||||
fft_mode_combo.setCurrentIndex(1)
|
||||
except Exception:
|
||||
pass
|
||||
restore_range_controls()
|
||||
set_bg_compute_enabled()
|
||||
set_background_enabled()
|
||||
set_fft_mode()
|
||||
|
||||
try:
|
||||
@ -657,9 +770,10 @@ def run_pyqtgraph(args) -> None:
|
||||
calib_pick_btn.clicked.connect(lambda _checked=False: pick_calib_file())
|
||||
calib_save_btn.clicked.connect(lambda _checked=False: save_current_calibration())
|
||||
calib_load_btn.clicked.connect(lambda _checked=False: load_calibration_file())
|
||||
bg_compute_cb.stateChanged.connect(lambda _v: set_bg_compute_enabled())
|
||||
bg_subtract_cb.stateChanged.connect(lambda _v: set_bg_subtract_enabled())
|
||||
fft_bg_subtract_cb.stateChanged.connect(lambda _v: set_fft_bg_subtract_enabled())
|
||||
background_cb.stateChanged.connect(lambda _v: set_background_enabled())
|
||||
background_pick_btn.clicked.connect(lambda _checked=False: pick_background_file())
|
||||
background_save_btn.clicked.connect(lambda _checked=False: save_current_background())
|
||||
background_load_btn.clicked.connect(lambda _checked=False: load_background_file())
|
||||
fft_mode_combo.currentIndexChanged.connect(lambda _v: set_fft_mode())
|
||||
except Exception:
|
||||
pass
|
||||
@ -776,6 +890,7 @@ def run_pyqtgraph(args) -> None:
|
||||
def apply_c_value(idx: int, edit) -> None:
|
||||
try:
|
||||
set_calibration_base_value(idx, float(edit.text().strip()))
|
||||
reset_background_state(clear_profile=True)
|
||||
runtime.mark_dirty()
|
||||
except Exception:
|
||||
try:
|
||||
@ -821,38 +936,14 @@ def run_pyqtgraph(args) -> None:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def visible_bg_fft(disp_fft: np.ndarray, force: bool = False) -> Optional[np.ndarray]:
|
||||
nonlocal bg_compute_enabled, bg_subtract_enabled
|
||||
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 runtime.bg_spec_cache
|
||||
if runtime.bg_spec_cache is not None and runtime.bg_spec_cache.size != ny:
|
||||
runtime.bg_spec_cache = None
|
||||
if not bg_compute_enabled:
|
||||
return runtime.bg_spec_cache
|
||||
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 runtime.bg_spec_cache
|
||||
try:
|
||||
bg_spec = np.nanmedian(window, axis=1)
|
||||
except Exception:
|
||||
return runtime.bg_spec_cache
|
||||
if not np.any(np.isfinite(bg_spec)):
|
||||
return runtime.bg_spec_cache
|
||||
runtime.bg_spec_cache = np.nan_to_num(bg_spec, nan=0.0).astype(np.float32, copy=False)
|
||||
return runtime.bg_spec_cache
|
||||
def refresh_current_fft_cache(sweep_for_fft: np.ndarray, bins: int) -> None:
|
||||
runtime.current_fft_mag = compute_fft_mag_row(
|
||||
sweep_for_fft,
|
||||
runtime.current_freqs,
|
||||
bins,
|
||||
mode=fft_mode,
|
||||
)
|
||||
runtime.current_fft_db = fft_mag_to_db(runtime.current_fft_mag)
|
||||
|
||||
def drain_queue() -> int:
|
||||
drained = 0
|
||||
@ -897,12 +988,6 @@ def run_pyqtgraph(args) -> None:
|
||||
|
||||
changed = drain_queue() > 0
|
||||
redraw_needed = changed or runtime.plot_dirty
|
||||
bg_fft_for_line = None
|
||||
if redraw_needed and fft_bg_subtract_enabled and runtime.ring.ring_fft is not None:
|
||||
try:
|
||||
bg_fft_for_line = visible_bg_fft(runtime.ring.get_display_fft_linear(), force=True)
|
||||
except Exception:
|
||||
bg_fft_for_line = None
|
||||
|
||||
if redraw_needed:
|
||||
xs = resolve_curve_xs(
|
||||
@ -952,25 +1037,24 @@ def run_pyqtgraph(args) -> None:
|
||||
sweep_for_fft = runtime.current_sweep_norm if runtime.current_sweep_norm is not None else runtime.current_sweep_raw
|
||||
distance_axis = runtime.current_distances if runtime.current_distances is not None else runtime.ring.distance_axis
|
||||
if sweep_for_fft is not None and sweep_for_fft.size > 0 and distance_axis is not None:
|
||||
if runtime.current_fft_db is None or runtime.current_fft_db.size != distance_axis.size or runtime.plot_dirty:
|
||||
runtime.current_fft_db = compute_fft_row(
|
||||
sweep_for_fft,
|
||||
runtime.current_freqs,
|
||||
distance_axis.size,
|
||||
mode=fft_mode,
|
||||
)
|
||||
fft_vals = runtime.current_fft_db
|
||||
xs_fft = distance_axis[: 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]
|
||||
if runtime.current_fft_mag is None or runtime.current_fft_mag.size != distance_axis.size or runtime.plot_dirty:
|
||||
refresh_current_fft_cache(sweep_for_fft, distance_axis.size)
|
||||
fft_mag = runtime.current_fft_mag
|
||||
xs_fft = distance_axis[: fft_mag.size]
|
||||
active_background = None
|
||||
try:
|
||||
active_background = resolve_active_background(fft_mag.size)
|
||||
except Exception:
|
||||
active_background = None
|
||||
display_fft_mag = fft_mag
|
||||
if active_background is not None:
|
||||
try:
|
||||
display_fft_mag = subtract_fft_background(fft_mag, active_background)
|
||||
except Exception as exc:
|
||||
set_status_note(f"фон: не удалось применить ({exc})")
|
||||
active_background = None
|
||||
display_fft_mag = fft_mag
|
||||
fft_vals = fft_mag_to_db(display_fft_mag)
|
||||
curve_fft.setData(xs_fft, fft_vals)
|
||||
finite_x = xs_fft[np.isfinite(xs_fft)]
|
||||
if finite_x.size > 0:
|
||||
@ -1006,7 +1090,7 @@ def run_pyqtgraph(args) -> None:
|
||||
for box in fft_peak_boxes:
|
||||
box.setVisible(False)
|
||||
|
||||
if fft_bg_subtract_enabled and bg_fft_for_line is not None:
|
||||
if active_background is not None:
|
||||
p_fft.setYRange(-10.0, 30.0, padding=0)
|
||||
else:
|
||||
finite_y = y_for_range[np.isfinite(y_for_range)]
|
||||
@ -1109,7 +1193,7 @@ def run_pyqtgraph(args) -> None:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if changed and runtime.ring.ring_fft is not None:
|
||||
if redraw_needed and runtime.ring.ring_fft is not None:
|
||||
disp_fft_lin = runtime.ring.get_display_fft_linear()
|
||||
if spec_mean_sec > 0.0:
|
||||
disp_times = runtime.ring.get_display_times()
|
||||
@ -1124,16 +1208,21 @@ def run_pyqtgraph(args) -> 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)
|
||||
active_background = None
|
||||
try:
|
||||
active_background = resolve_active_background(disp_fft_lin.shape[0])
|
||||
except Exception:
|
||||
active_background = None
|
||||
if active_background is not None:
|
||||
try:
|
||||
disp_fft_lin = subtract_fft_background(disp_fft_lin, active_background)
|
||||
except Exception as exc:
|
||||
set_status_note(f"фон: не удалось применить ({exc})")
|
||||
active_background = None
|
||||
disp_fft = fft_mag_to_db(disp_fft_lin)
|
||||
|
||||
levels = None
|
||||
if bg_spec is not None:
|
||||
if active_background is not None:
|
||||
try:
|
||||
p5 = float(np.nanpercentile(disp_fft, 5))
|
||||
p95 = float(np.nanpercentile(disp_fft, 95))
|
||||
|
||||
@ -1,5 +1,11 @@
|
||||
"""Pure sweep-processing helpers."""
|
||||
|
||||
from rfg_adc_plotter.processing.background import (
|
||||
load_fft_background,
|
||||
save_fft_background,
|
||||
subtract_fft_background,
|
||||
validate_fft_background,
|
||||
)
|
||||
from rfg_adc_plotter.processing.calibration import (
|
||||
build_calib_envelope,
|
||||
calibrate_freqs,
|
||||
@ -47,11 +53,15 @@ __all__ = [
|
||||
"get_calibration_base",
|
||||
"get_calibration_coeffs",
|
||||
"load_calib_envelope",
|
||||
"load_fft_background",
|
||||
"normalize_by_envelope",
|
||||
"normalize_by_calib",
|
||||
"parse_spec_clip",
|
||||
"recalculate_calibration_c",
|
||||
"rolling_median_ref",
|
||||
"save_calib_envelope",
|
||||
"save_fft_background",
|
||||
"set_calibration_base_value",
|
||||
"subtract_fft_background",
|
||||
"validate_fft_background",
|
||||
]
|
||||
|
||||
66
rfg_adc_plotter/processing/background.py
Normal file
66
rfg_adc_plotter/processing/background.py
Normal file
@ -0,0 +1,66 @@
|
||||
"""Helpers for persisted FFT background profiles."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
def validate_fft_background(background: np.ndarray) -> np.ndarray:
|
||||
"""Validate a saved FFT background payload."""
|
||||
values = np.asarray(background)
|
||||
if values.ndim != 1:
|
||||
raise ValueError("FFT background must be a 1D array")
|
||||
if not np.issubdtype(values.dtype, np.number):
|
||||
raise ValueError("FFT background must be numeric")
|
||||
values = np.asarray(values, dtype=np.float32).reshape(-1)
|
||||
if values.size == 0:
|
||||
raise ValueError("FFT background is empty")
|
||||
return values
|
||||
|
||||
|
||||
def _normalize_background_path(path: str | Path) -> Path:
|
||||
out = Path(path).expanduser()
|
||||
if out.suffix.lower() != ".npy":
|
||||
out = out.with_suffix(".npy")
|
||||
return out
|
||||
|
||||
|
||||
def save_fft_background(path: str | Path, background: np.ndarray) -> str:
|
||||
"""Persist an FFT background profile as a .npy file."""
|
||||
normalized_path = _normalize_background_path(path)
|
||||
values = validate_fft_background(background)
|
||||
np.save(normalized_path, values.astype(np.float32, copy=False))
|
||||
return str(normalized_path)
|
||||
|
||||
|
||||
def load_fft_background(path: str | Path) -> np.ndarray:
|
||||
"""Load and validate an FFT background profile from a .npy file."""
|
||||
normalized_path = _normalize_background_path(path)
|
||||
loaded = np.load(normalized_path, allow_pickle=False)
|
||||
return validate_fft_background(loaded)
|
||||
|
||||
|
||||
def subtract_fft_background(signal_mag: np.ndarray, background_mag: np.ndarray) -> np.ndarray:
|
||||
"""Subtract a background profile from FFT magnitudes in linear amplitude."""
|
||||
signal = np.asarray(signal_mag, dtype=np.float32)
|
||||
background = validate_fft_background(background_mag)
|
||||
if signal.ndim == 1:
|
||||
if signal.size != background.size:
|
||||
raise ValueError("FFT background size does not match signal size")
|
||||
valid = np.isfinite(signal) & np.isfinite(background)
|
||||
out = np.full_like(signal, np.nan, dtype=np.float32)
|
||||
if np.any(valid):
|
||||
out[valid] = np.maximum(signal[valid] - background[valid], 0.0)
|
||||
return out
|
||||
|
||||
if signal.ndim == 2:
|
||||
if signal.shape[0] != background.size:
|
||||
raise ValueError("FFT background size does not match signal rows")
|
||||
background_2d = background[:, None]
|
||||
valid = np.isfinite(signal) & np.isfinite(background_2d)
|
||||
diff = signal - background_2d
|
||||
return np.where(valid, np.maximum(diff, 0.0), np.nan).astype(np.float32, copy=False)
|
||||
|
||||
raise ValueError("FFT background subtraction supports only 1D or 2D signals")
|
||||
@ -1,6 +1,7 @@
|
||||
"""Runtime state helpers."""
|
||||
|
||||
from rfg_adc_plotter.state.background_buffer import BackgroundMedianBuffer
|
||||
from rfg_adc_plotter.state.ring_buffer import RingBuffer
|
||||
from rfg_adc_plotter.state.runtime_state import RuntimeState
|
||||
|
||||
__all__ = ["RingBuffer", "RuntimeState"]
|
||||
__all__ = ["BackgroundMedianBuffer", "RingBuffer", "RuntimeState"]
|
||||
|
||||
49
rfg_adc_plotter/state/background_buffer.py
Normal file
49
rfg_adc_plotter/state/background_buffer.py
Normal file
@ -0,0 +1,49 @@
|
||||
"""Rolling median buffer for persisted FFT background capture."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
class BackgroundMedianBuffer:
|
||||
"""Store recent FFT rows and expose their median profile."""
|
||||
|
||||
def __init__(self, max_rows: int):
|
||||
self.max_rows = max(1, int(max_rows))
|
||||
self.width = 0
|
||||
self.head = 0
|
||||
self.count = 0
|
||||
self.rows: Optional[np.ndarray] = None
|
||||
|
||||
def reset(self) -> None:
|
||||
self.width = 0
|
||||
self.head = 0
|
||||
self.count = 0
|
||||
self.rows = None
|
||||
|
||||
def push(self, fft_mag: np.ndarray) -> None:
|
||||
values = np.asarray(fft_mag, dtype=np.float32).reshape(-1)
|
||||
if values.size == 0:
|
||||
return
|
||||
if self.rows is None or self.width != values.size:
|
||||
self.width = values.size
|
||||
self.rows = np.full((self.max_rows, self.width), np.nan, dtype=np.float32)
|
||||
self.head = 0
|
||||
self.count = 0
|
||||
self.rows[self.head, :] = values
|
||||
self.head = (self.head + 1) % self.max_rows
|
||||
self.count = min(self.count + 1, self.max_rows)
|
||||
|
||||
def median(self) -> Optional[np.ndarray]:
|
||||
if self.rows is None or self.count <= 0:
|
||||
return None
|
||||
rows = self.rows[: self.count] if self.count < self.max_rows else self.rows
|
||||
valid_rows = np.any(np.isfinite(rows), axis=1)
|
||||
if not np.any(valid_rows):
|
||||
return None
|
||||
median = np.nanmedian(rows[valid_rows], axis=0).astype(np.float32, copy=False)
|
||||
if not np.any(np.isfinite(median)):
|
||||
return None
|
||||
return np.nan_to_num(median, nan=0.0).astype(np.float32, copy=False)
|
||||
@ -25,6 +25,7 @@ class RingBuffer:
|
||||
self.ring_fft: Optional[np.ndarray] = None
|
||||
self.x_shared: Optional[np.ndarray] = None
|
||||
self.distance_axis: Optional[np.ndarray] = None
|
||||
self.last_fft_mag: Optional[np.ndarray] = None
|
||||
self.last_fft_db: Optional[np.ndarray] = None
|
||||
self.last_freqs: Optional[np.ndarray] = None
|
||||
self.y_min_fft: Optional[float] = None
|
||||
@ -47,6 +48,7 @@ class RingBuffer:
|
||||
self.ring_fft = None
|
||||
self.x_shared = None
|
||||
self.distance_axis = None
|
||||
self.last_fft_mag = None
|
||||
self.last_fft_db = None
|
||||
self.last_freqs = None
|
||||
self.y_min_fft = None
|
||||
@ -125,6 +127,7 @@ class RingBuffer:
|
||||
last_idx = (self.head - 1) % self.max_sweeps
|
||||
if self.ring_fft.shape[0] > 0:
|
||||
last_fft = self.ring_fft[last_idx]
|
||||
self.last_fft_mag = np.asarray(last_fft, dtype=np.float32).copy()
|
||||
self.last_fft_db = fft_mag_to_db(last_fft)
|
||||
finite = self.ring_fft[np.isfinite(self.ring_fft)]
|
||||
if finite.size > 0:
|
||||
@ -155,6 +158,7 @@ class RingBuffer:
|
||||
|
||||
fft_mag = compute_fft_mag_row(sweep, freqs, self.fft_bins, mode=self.fft_mode)
|
||||
self.ring_fft[self.head, :] = fft_mag
|
||||
self.last_fft_mag = np.asarray(fft_mag, dtype=np.float32).copy()
|
||||
self.last_fft_db = fft_mag_to_db(fft_mag)
|
||||
|
||||
if self.last_fft_db.size > 0:
|
||||
@ -178,6 +182,11 @@ class RingBuffer:
|
||||
base = self.ring_fft if self.head == 0 else np.roll(self.ring_fft, -self.head, axis=0)
|
||||
return base.T
|
||||
|
||||
def get_last_fft_linear(self) -> Optional[np.ndarray]:
|
||||
if self.last_fft_mag is None:
|
||||
return None
|
||||
return np.asarray(self.last_fft_mag, dtype=np.float32).copy()
|
||||
|
||||
def get_display_times(self) -> Optional[np.ndarray]:
|
||||
if self.ring_time is None:
|
||||
return None
|
||||
|
||||
@ -7,6 +7,8 @@ from typing import Dict, List, Optional
|
||||
|
||||
import numpy as np
|
||||
|
||||
from rfg_adc_plotter.constants import BACKGROUND_MEDIAN_SWEEPS
|
||||
from rfg_adc_plotter.state.background_buffer import BackgroundMedianBuffer
|
||||
from rfg_adc_plotter.state.ring_buffer import RingBuffer
|
||||
from rfg_adc_plotter.types import SweepAuxCurves, SweepInfo
|
||||
|
||||
@ -23,12 +25,17 @@ class RuntimeState:
|
||||
current_sweep_raw: Optional[np.ndarray] = None
|
||||
current_aux_curves: SweepAuxCurves = None
|
||||
current_sweep_norm: Optional[np.ndarray] = None
|
||||
current_fft_mag: Optional[np.ndarray] = None
|
||||
current_fft_db: Optional[np.ndarray] = None
|
||||
last_calib_sweep: Optional[np.ndarray] = None
|
||||
calib_envelope: Optional[np.ndarray] = None
|
||||
calib_file_path: Optional[str] = None
|
||||
background_buffer: BackgroundMedianBuffer = field(
|
||||
default_factory=lambda: BackgroundMedianBuffer(BACKGROUND_MEDIAN_SWEEPS)
|
||||
)
|
||||
background_profile: Optional[np.ndarray] = None
|
||||
background_file_path: Optional[str] = None
|
||||
current_info: Optional[SweepInfo] = None
|
||||
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]] = field(default_factory=list)
|
||||
|
||||
Reference in New Issue
Block a user