low freq filter
This commit is contained in:
@ -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:
|
||||
|
||||
@ -7,6 +7,7 @@ import unittest
|
||||
|
||||
from rfg_adc_plotter.constants import C_M_S, FFT_LEN, SWEEP_FREQ_MAX_GHZ, SWEEP_FREQ_MIN_GHZ
|
||||
from rfg_adc_plotter.gui.pyqtgraph_backend import (
|
||||
apply_distance_cut_to_axis,
|
||||
apply_working_range,
|
||||
apply_working_range_to_aux_curves,
|
||||
build_main_window_layout,
|
||||
@ -16,6 +17,7 @@ from rfg_adc_plotter.gui.pyqtgraph_backend import (
|
||||
resolve_axis_bounds,
|
||||
resolve_heavy_refresh_stride,
|
||||
resolve_initial_window_size,
|
||||
resolve_distance_cut_start,
|
||||
sanitize_curve_data_for_display,
|
||||
sanitize_image_for_display,
|
||||
set_image_rect_if_ready,
|
||||
@ -317,6 +319,22 @@ class ProcessingTests(unittest.TestCase):
|
||||
|
||||
self.assertIsNone(bounds)
|
||||
|
||||
def test_resolve_distance_cut_start_interpolates_with_percent(self):
|
||||
axis = np.asarray([0.0, 1.0, 2.0, 3.0], dtype=np.float64)
|
||||
cut_start = resolve_distance_cut_start(axis, 50.0)
|
||||
|
||||
self.assertIsNotNone(cut_start)
|
||||
self.assertAlmostEqual(float(cut_start), 1.5, places=6)
|
||||
|
||||
def test_apply_distance_cut_to_axis_keeps_farthest_point_for_extreme_cut(self):
|
||||
axis = np.asarray([0.0, 1.0, 2.0, 3.0], dtype=np.float64)
|
||||
cut_axis, keep_mask = apply_distance_cut_to_axis(axis, 10.0)
|
||||
|
||||
self.assertEqual(cut_axis.shape, (1,))
|
||||
self.assertEqual(keep_mask.shape, axis.shape)
|
||||
self.assertTrue(bool(keep_mask[-1]))
|
||||
self.assertAlmostEqual(float(cut_axis[0]), 3.0, places=6)
|
||||
|
||||
def test_resolve_initial_window_size_stays_within_small_screen(self):
|
||||
width, height = resolve_initial_window_size(800, 480)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user