low freq filter

This commit is contained in:
awe
2026-04-10 22:17:08 +03:00
parent 17540c3b11
commit d0d2f5a59e
2 changed files with 154 additions and 5 deletions

View File

@ -48,6 +48,8 @@ UI_BACKLOG_LATEST_ONLY_THRESHOLD_MULTIPLIER = 4
UI_HEAVY_REFRESH_BACKLOG_MULTIPLIER = 2
UI_HEAVY_REFRESH_MAX_STRIDE = 4
UI_DATA_WAIT_NOTE_AFTER_S = 3.0
FFT_LOW_CUT_SLIDER_SCALE = 10
FFT_LOW_CUT_MAX_PERCENT = 99.0
DEFAULT_MAIN_WINDOW_WIDTH = 1200
DEFAULT_MAIN_WINDOW_HEIGHT = 680
MIN_MAIN_WINDOW_WIDTH = 640
@ -397,6 +399,57 @@ def apply_working_range_to_signal(
return np.asarray(signal_arr[valid], dtype=np.float32)
def resolve_distance_cut_start(
distance_axis: Optional[np.ndarray],
cut_percent: float,
) -> Optional[float]:
"""Return distance threshold for hiding the beginning of FFT/B-scan axis."""
if distance_axis is None:
return None
axis_arr = np.asarray(distance_axis, dtype=np.float64).reshape(-1)
finite = axis_arr[np.isfinite(axis_arr)]
if finite.size <= 0:
return None
d_min = float(np.min(finite))
d_max = float(np.max(finite))
if not (np.isfinite(d_min) and np.isfinite(d_max)):
return None
if d_max <= d_min:
return d_min
pct = float(np.clip(float(cut_percent), 0.0, FFT_LOW_CUT_MAX_PERCENT))
start = d_min + (d_max - d_min) * (pct / 100.0)
if start >= d_max:
start = float(np.nextafter(d_max, d_min))
return float(start)
def apply_distance_cut_to_axis(
distance_axis: Optional[np.ndarray],
min_distance_m: Optional[float],
) -> Tuple[np.ndarray, np.ndarray]:
"""Apply distance threshold and return ``(cropped_axis, keep_mask)``."""
if distance_axis is None:
return np.zeros((0,), dtype=np.float64), np.zeros((0,), dtype=bool)
axis_arr = np.asarray(distance_axis, dtype=np.float64).reshape(-1)
if axis_arr.size <= 0:
return np.zeros((0,), dtype=np.float64), np.zeros((0,), dtype=bool)
finite = np.isfinite(axis_arr)
if min_distance_m is None or not np.isfinite(min_distance_m):
keep = finite
else:
keep = finite & (axis_arr >= float(min_distance_m))
if not np.any(keep) and np.any(finite):
# Keep the farthest finite point so the view never becomes completely empty.
keep = np.zeros((axis_arr.size,), dtype=bool)
last_idx = int(np.flatnonzero(finite)[-1])
keep[last_idx] = True
return axis_arr[keep], keep
def resolve_visible_aux_curves(aux_curves: SweepAuxCurves, enabled: bool) -> SweepAuxCurves:
"""Return auxiliary curves only when their display is enabled."""
if (not enabled) or aux_curves is None:
@ -753,6 +806,16 @@ def run_pyqtgraph(args) -> None:
fft_mode_combo.addItem("Симметричный", "symmetric")
fft_mode_combo.addItem("Нули [-max,+min]", "positive_only")
fft_mode_combo.addItem("Нули [-max,+min] точный", "positive_only_exact")
fft_low_cut_label = QtWidgets.QLabel("Срез начала FFT/B-scan")
fft_low_cut_slider = QtWidgets.QSlider(QtCore.Qt.Horizontal)
fft_low_cut_slider.setRange(0, int(FFT_LOW_CUT_MAX_PERCENT * FFT_LOW_CUT_SLIDER_SCALE))
fft_low_cut_slider.setValue(0)
fft_low_cut_value_label = QtWidgets.QLabel("0.0%")
fft_low_cut_row = QtWidgets.QHBoxLayout()
fft_low_cut_row.setContentsMargins(0, 0, 0, 0)
fft_low_cut_row.setSpacing(6)
fft_low_cut_row.addWidget(fft_low_cut_slider)
fft_low_cut_row.addWidget(fft_low_cut_value_label)
peak_search_cb = QtWidgets.QCheckBox("поиск пиков")
calib_group = QtWidgets.QGroupBox("Калибровка")
calib_group_layout = QtWidgets.QVBoxLayout(calib_group)
@ -842,6 +905,8 @@ def run_pyqtgraph(args) -> None:
settings_layout.addWidget(background_group)
settings_layout.addWidget(fft_mode_label)
settings_layout.addWidget(fft_mode_combo)
settings_layout.addWidget(fft_low_cut_label)
settings_layout.addLayout(fft_low_cut_row)
settings_layout.addWidget(fft_curve_group)
settings_layout.addWidget(peak_search_cb)
@ -855,6 +920,7 @@ def run_pyqtgraph(args) -> None:
fft_real_enabled = True
fft_imag_enabled = True
fft_mode = "symmetric"
fft_low_cut_percent = 0.0
status_note = ""
waiting_data_note = ""
status_note_expires_at: Optional[float] = None
@ -897,6 +963,24 @@ def run_pyqtgraph(args) -> None:
p_spec.setRange(xRange=(0, max_sweeps - 1), yRange=(0.0, 1.0), padding=0)
p_fft.setXRange(0.0, 1.0, padding=0)
def _active_distance_axis() -> Optional[np.ndarray]:
if runtime.current_distances is not None and runtime.current_distances.size > 0:
return runtime.current_distances
return runtime.ring.distance_axis
def _active_distance_cut_start() -> Optional[float]:
return resolve_distance_cut_start(_active_distance_axis(), fft_low_cut_percent)
def refresh_fft_low_cut_label() -> None:
text = f"{fft_low_cut_percent:.1f}%"
cut_start = _active_distance_cut_start()
if cut_start is not None and np.isfinite(cut_start):
text = f"{text} (~{cut_start:.4g} м)"
try:
fft_low_cut_value_label.setText(text)
except Exception:
pass
def update_physical_axes() -> None:
freq_bounds = resolve_axis_bounds(runtime.current_freqs)
if freq_bounds is None:
@ -912,8 +996,14 @@ def run_pyqtgraph(args) -> None:
distance_bounds = resolve_axis_bounds(runtime.ring.distance_axis)
if distance_bounds is not None:
d_min, d_max = distance_bounds
set_image_rect_if_ready(img_fft, 0.0, d_min, float(max_sweeps), d_max - d_min)
d_cut = _active_distance_cut_start()
if d_cut is not None and np.isfinite(d_cut):
d_min = max(float(d_min), float(d_cut))
span = max(1e-9, float(d_max - d_min))
set_image_rect_if_ready(img_fft, 0.0, d_min, float(max_sweeps), span)
p_spec.setRange(xRange=(0, max_sweeps - 1), yRange=(d_min, d_max), padding=0)
p_fft.setXRange(d_min, d_max, padding=0)
refresh_fft_low_cut_label()
def resolve_curve_xs(size: int) -> np.ndarray:
if size <= 0:
@ -1341,6 +1431,17 @@ def run_pyqtgraph(args) -> None:
set_status_note("фон: профиль не загружен")
runtime.mark_dirty()
def set_fft_low_cut_percent() -> None:
nonlocal fft_low_cut_percent
try:
fft_low_cut_percent = float(fft_low_cut_slider.value()) / float(FFT_LOW_CUT_SLIDER_SCALE)
except Exception:
fft_low_cut_percent = 0.0
fft_low_cut_percent = float(np.clip(fft_low_cut_percent, 0.0, FFT_LOW_CUT_MAX_PERCENT))
refresh_fft_low_cut_label()
update_physical_axes()
runtime.mark_dirty()
def set_fft_mode() -> None:
nonlocal fft_mode
try:
@ -1371,6 +1472,7 @@ def run_pyqtgraph(args) -> None:
set_background_enabled()
set_fft_curve_visibility()
set_fft_mode()
set_fft_low_cut_percent()
try:
range_min_spin.valueChanged.connect(lambda _v: set_working_range())
@ -1385,6 +1487,7 @@ def run_pyqtgraph(args) -> None:
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())
fft_low_cut_slider.valueChanged.connect(lambda _v: set_fft_low_cut_percent())
fft_abs_cb.stateChanged.connect(lambda _v: set_fft_curve_visibility())
fft_real_cb.stateChanged.connect(lambda _v: set_fft_curve_visibility())
fft_imag_cb.stateChanged.connect(lambda _v: set_fft_curve_visibility())
@ -1809,7 +1912,7 @@ def run_pyqtgraph(args) -> None:
refresh_current_fft_cache(sweep_for_fft, distance_axis.size)
fft_mag = runtime.current_fft_mag
fft_complex = runtime.current_fft_complex
xs_fft = distance_axis[: fft_mag.size]
xs_fft = np.asarray(distance_axis[: fft_mag.size], dtype=np.float64)
active_background = None
try:
active_background = resolve_active_background(fft_mag.size)
@ -1823,17 +1926,29 @@ def run_pyqtgraph(args) -> None:
set_status_note(f"фон: не удалось применить ({exc})")
active_background = None
display_fft_mag = fft_mag
fft_mag_plot = np.asarray(display_fft_mag[: xs_fft.size], dtype=np.float32).reshape(-1)
fft_complex_plot = None
if fft_complex is not None:
fft_complex_plot = np.asarray(fft_complex[: xs_fft.size], dtype=np.complex64).reshape(-1)
fft_cut_start = _active_distance_cut_start()
xs_fft, fft_keep_mask = apply_distance_cut_to_axis(xs_fft, fft_cut_start)
if fft_keep_mask.size > 0:
fft_mag_plot = fft_mag_plot[fft_keep_mask]
if fft_complex_plot is not None and fft_complex_plot.size == fft_keep_mask.size:
fft_complex_plot = fft_complex_plot[fft_keep_mask]
elif fft_complex_plot is not None:
fft_complex_plot = None
fft_x_bounds = resolve_axis_bounds(xs_fft)
if fft_x_bounds is not None:
p_fft.setXRange(fft_x_bounds[0], fft_x_bounds[1], padding=0)
fft_vals_db = fft_mag_to_db(display_fft_mag)
fft_vals_db = fft_mag_to_db(fft_mag_plot)
ref_curve_for_range = None
if complex_sweep_mode:
visible_abs, visible_real, visible_imag = resolve_visible_fft_curves(
fft_complex,
display_fft_mag,
fft_complex_plot,
fft_mag_plot,
complex_mode=True,
show_abs=fft_abs_enabled,
show_real=fft_real_enabled,
@ -2099,6 +2214,17 @@ def run_pyqtgraph(args) -> None:
)
else:
disp_fft_lin = runtime.ring.get_display_fft_linear()
disp_fft_axis = runtime.ring.distance_axis
if disp_fft_axis is not None:
axis_arr = np.asarray(disp_fft_axis, dtype=np.float64).reshape(-1)
row_take = min(axis_arr.size, disp_fft_lin.shape[0])
axis_arr = axis_arr[:row_take]
disp_fft_lin = disp_fft_lin[:row_take, :]
fft_cut_start = _active_distance_cut_start()
axis_arr, keep_mask = apply_distance_cut_to_axis(axis_arr, fft_cut_start)
if keep_mask.size > 0:
disp_fft_lin = disp_fft_lin[keep_mask, :]
disp_fft_axis = axis_arr
if spec_mean_sec > 0.0:
disp_times = runtime.ring.get_display_times()
if disp_times is not None:
@ -2159,6 +2285,11 @@ def run_pyqtgraph(args) -> None:
and runtime.ring.y_min_fft != runtime.ring.y_max_fft
):
levels = (runtime.ring.y_min_fft, runtime.ring.y_max_fft)
distance_bounds = resolve_axis_bounds(disp_fft_axis)
if distance_bounds is not None:
d_min, d_max = distance_bounds
set_image_rect_if_ready(img_fft, 0.0, d_min, float(max_sweeps), max(1e-9, d_max - d_min))
p_spec.setRange(xRange=(0, max_sweeps - 1), yRange=(d_min, d_max), padding=0)
if levels is not None:
img_fft.setImage(disp_fft, autoLevels=False, levels=levels)
else: