fix
This commit is contained in:
@ -37,6 +37,15 @@ def build_parser() -> argparse.ArgumentParser:
|
|||||||
"в водопаде спектров (0 — отключить)"
|
"в водопаде спектров (0 — отключить)"
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--bscan-db-range",
|
||||||
|
dest="bscan_db_range",
|
||||||
|
default="80,150",
|
||||||
|
help=(
|
||||||
|
"Фиксированный диапазон цветовой шкалы B-scan в дБ (min,max). "
|
||||||
|
"Напр. 80,150. Применяется к обычному виду (без вычитания фона)."
|
||||||
|
),
|
||||||
|
)
|
||||||
parser.add_argument("--title", default="ADC Sweeps", help="Заголовок окна")
|
parser.add_argument("--title", default="ADC Sweeps", help="Заголовок окна")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--fancy",
|
"--fancy",
|
||||||
|
|||||||
@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import math
|
|
||||||
import signal
|
import signal
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
@ -55,7 +54,7 @@ from rfg_adc_plotter.types import SweepAuxCurves, SweepInfo, SweepPacket
|
|||||||
RAW_PLOT_MAX_POINTS = 2048
|
RAW_PLOT_MAX_POINTS = 2048
|
||||||
RAW_WATERFALL_MAX_POINTS = 2048
|
RAW_WATERFALL_MAX_POINTS = 2048
|
||||||
BSCAN_MAX_POINTS = 256
|
BSCAN_MAX_POINTS = 256
|
||||||
BSCAN_LEVEL_TAU_S = 2.0
|
BSCAN_DB_RANGE_DEFAULT: Tuple[float, float] = (80.0, 150.0)
|
||||||
UI_QUEUE_MAXSIZE = 128
|
UI_QUEUE_MAXSIZE = 128
|
||||||
UI_MAX_PACKETS_PER_TICK = 8
|
UI_MAX_PACKETS_PER_TICK = 8
|
||||||
DEBUG_FRAME_LOG_EVERY = 10
|
DEBUG_FRAME_LOG_EVERY = 10
|
||||||
@ -828,6 +827,19 @@ def _db_to_linear_amplitude(values: np.ndarray) -> np.ndarray:
|
|||||||
return np.maximum(out, 0.0).astype(np.float32, copy=False)
|
return np.maximum(out, 0.0).astype(np.float32, copy=False)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_bscan_db_range(spec: Optional[str]) -> Tuple[float, float]:
|
||||||
|
"""Parse a fixed B-scan dB color window 'min,max'. Falls back to the default."""
|
||||||
|
if spec:
|
||||||
|
try:
|
||||||
|
p0, p1 = str(spec).strip().replace(";", ",").split(",")
|
||||||
|
lo, hi = float(p0), float(p1)
|
||||||
|
if np.isfinite(lo) and np.isfinite(hi) and lo < hi:
|
||||||
|
return (lo, hi)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return BSCAN_DB_RANGE_DEFAULT
|
||||||
|
|
||||||
|
|
||||||
def compute_background_subtracted_bscan_levels(
|
def compute_background_subtracted_bscan_levels(
|
||||||
disp_fft_lin: np.ndarray,
|
disp_fft_lin: np.ndarray,
|
||||||
disp_fft: np.ndarray,
|
disp_fft: np.ndarray,
|
||||||
@ -910,6 +922,7 @@ def run_pyqtgraph(args) -> None:
|
|||||||
fft_bins = FFT_LEN // 2 + 1
|
fft_bins = FFT_LEN // 2 + 1
|
||||||
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))
|
||||||
|
bscan_db_vmin, bscan_db_vmax = parse_bscan_db_range(getattr(args, "bscan_db_range", None))
|
||||||
runtime = RuntimeState(
|
runtime = RuntimeState(
|
||||||
ring=RingBuffer(max_sweeps),
|
ring=RingBuffer(max_sweeps),
|
||||||
range_min_ghz=float(SWEEP_FREQ_MIN_GHZ),
|
range_min_ghz=float(SWEEP_FREQ_MIN_GHZ),
|
||||||
@ -1163,6 +1176,24 @@ def run_pyqtgraph(args) -> None:
|
|||||||
range_max_spin.setValue(runtime.range_max_ghz)
|
range_max_spin.setValue(runtime.range_max_ghz)
|
||||||
range_group_layout.addRow("f min", range_min_spin)
|
range_group_layout.addRow("f min", range_min_spin)
|
||||||
range_group_layout.addRow("f max", range_max_spin)
|
range_group_layout.addRow("f max", range_max_spin)
|
||||||
|
bscan_db_group = QtWidgets.QGroupBox("B-scan дБ шкала")
|
||||||
|
bscan_db_group_layout = QtWidgets.QFormLayout(bscan_db_group)
|
||||||
|
bscan_db_group_layout.setContentsMargins(6, 6, 6, 6)
|
||||||
|
bscan_db_group_layout.setSpacing(6)
|
||||||
|
bscan_db_min_spin = QtWidgets.QDoubleSpinBox()
|
||||||
|
bscan_db_max_spin = QtWidgets.QDoubleSpinBox()
|
||||||
|
for spin in (bscan_db_min_spin, bscan_db_max_spin):
|
||||||
|
spin.setDecimals(1)
|
||||||
|
spin.setRange(-100.0, 400.0)
|
||||||
|
spin.setSingleStep(1.0)
|
||||||
|
try:
|
||||||
|
spin.setSuffix(" dB")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
bscan_db_min_spin.setValue(bscan_db_vmin)
|
||||||
|
bscan_db_max_spin.setValue(bscan_db_vmax)
|
||||||
|
bscan_db_group_layout.addRow("дБ мин", bscan_db_min_spin)
|
||||||
|
bscan_db_group_layout.addRow("дБ макс", bscan_db_max_spin)
|
||||||
fft_mode_label = QtWidgets.QLabel("IFFT режим")
|
fft_mode_label = QtWidgets.QLabel("IFFT режим")
|
||||||
fft_mode_combo = QtWidgets.QComboBox()
|
fft_mode_combo = QtWidgets.QComboBox()
|
||||||
fft_mode_combo.addItem("Обычный", "direct")
|
fft_mode_combo.addItem("Обычный", "direct")
|
||||||
@ -1312,6 +1343,7 @@ def run_pyqtgraph(args) -> None:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
settings_layout.addWidget(range_group)
|
settings_layout.addWidget(range_group)
|
||||||
|
settings_layout.addWidget(bscan_db_group)
|
||||||
settings_layout.addWidget(calib_group)
|
settings_layout.addWidget(calib_group)
|
||||||
settings_layout.addWidget(complex_calib_group)
|
settings_layout.addWidget(complex_calib_group)
|
||||||
settings_layout.addWidget(tty_range_group)
|
settings_layout.addWidget(tty_range_group)
|
||||||
@ -1354,7 +1386,12 @@ def run_pyqtgraph(args) -> None:
|
|||||||
axis_range_cache: Dict[str, Tuple[float, ...]] = {}
|
axis_range_cache: Dict[str, Tuple[float, ...]] = {}
|
||||||
image_rect_cache: Dict[str, Tuple[float, ...]] = {}
|
image_rect_cache: Dict[str, Tuple[float, ...]] = {}
|
||||||
bscan_axis_cache: Dict[Tuple, np.ndarray] = {}
|
bscan_axis_cache: Dict[Tuple, np.ndarray] = {}
|
||||||
bscan_levels_state: Dict[str, Optional[Tuple[float, float]]] = {"held": None, "held_bg": None}
|
# Fixed manual dB color window for the normal (no-background) B-scan view.
|
||||||
|
# Never recomputed per frame; only changed by the user via the spin boxes.
|
||||||
|
bscan_manual_levels: Dict[str, float] = {"vmin": bscan_db_vmin, "vmax": bscan_db_vmax}
|
||||||
|
# Frozen levels for the background-subtracted residual view (different scale):
|
||||||
|
# computed once when background is active, held until a reset event clears it.
|
||||||
|
bscan_bg_levels_frozen: Dict[str, Optional[Tuple[float, float]]] = {"held": None}
|
||||||
curve_has_data_cache: Dict[str, bool] = {}
|
curve_has_data_cache: Dict[str, bool] = {}
|
||||||
item_visibility_cache: Dict[str, bool] = {}
|
item_visibility_cache: Dict[str, bool] = {}
|
||||||
text_value_cache: Dict[str, str] = {}
|
text_value_cache: Dict[str, str] = {}
|
||||||
@ -1933,10 +1970,9 @@ def run_pyqtgraph(args) -> None:
|
|||||||
runtime.background_buffer.reset()
|
runtime.background_buffer.reset()
|
||||||
if clear_profile:
|
if clear_profile:
|
||||||
runtime.background_profile = None
|
runtime.background_profile = None
|
||||||
# Re-seed the held B-scan color levels so contrast doesn't drag an old
|
# Re-freeze the background-subtracted B-scan levels so contrast doesn't
|
||||||
# scale across the background on/off transition.
|
# drag an old residual scale across the background on/off transition.
|
||||||
bscan_levels_state["held"] = None
|
bscan_bg_levels_frozen["held"] = None
|
||||||
bscan_levels_state["held_bg"] = None
|
|
||||||
runtime.current_fft_complex = None
|
runtime.current_fft_complex = None
|
||||||
runtime.current_fft_mag = None
|
runtime.current_fft_mag = None
|
||||||
runtime.current_fft_db = None
|
runtime.current_fft_db = None
|
||||||
@ -1956,8 +1992,7 @@ def run_pyqtgraph(args) -> None:
|
|||||||
axis_range_cache.clear()
|
axis_range_cache.clear()
|
||||||
image_rect_cache.clear()
|
image_rect_cache.clear()
|
||||||
bscan_axis_cache.clear()
|
bscan_axis_cache.clear()
|
||||||
bscan_levels_state["held"] = None
|
bscan_bg_levels_frozen["held"] = None
|
||||||
bscan_levels_state["held_bg"] = None
|
|
||||||
runtime.current_distances = None
|
runtime.current_distances = None
|
||||||
runtime.current_fft_complex = None
|
runtime.current_fft_complex = None
|
||||||
runtime.current_fft_mag = None
|
runtime.current_fft_mag = None
|
||||||
@ -2291,6 +2326,20 @@ def run_pyqtgraph(args) -> None:
|
|||||||
finally:
|
finally:
|
||||||
range_change_in_progress = False
|
range_change_in_progress = False
|
||||||
|
|
||||||
|
def set_bscan_db_range() -> None:
|
||||||
|
try:
|
||||||
|
new_min = float(bscan_db_min_spin.value())
|
||||||
|
new_max = float(bscan_db_max_spin.value())
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
if (not np.isfinite(new_min)) or (not np.isfinite(new_max)) or new_min >= new_max:
|
||||||
|
set_status_note("B-scan дБ: мин должен быть меньше макс")
|
||||||
|
return
|
||||||
|
bscan_manual_levels["vmin"] = new_min
|
||||||
|
bscan_manual_levels["vmax"] = new_max
|
||||||
|
set_status_note(f"B-scan дБ: {new_min:.1f}..{new_max:.1f}")
|
||||||
|
runtime.mark_dirty()
|
||||||
|
|
||||||
def set_working_range() -> None:
|
def set_working_range() -> None:
|
||||||
nonlocal range_change_in_progress
|
nonlocal range_change_in_progress
|
||||||
if range_change_in_progress:
|
if range_change_in_progress:
|
||||||
@ -2559,6 +2608,8 @@ def run_pyqtgraph(args) -> None:
|
|||||||
background_enabled = False
|
background_enabled = False
|
||||||
if background_enabled and runtime.background_profile is None:
|
if background_enabled and runtime.background_profile is None:
|
||||||
set_status_note("фон: профиль не загружен")
|
set_status_note("фон: профиль не загружен")
|
||||||
|
# Re-freeze residual levels so toggling background recomputes them once.
|
||||||
|
bscan_bg_levels_frozen["held"] = None
|
||||||
runtime.mark_dirty()
|
runtime.mark_dirty()
|
||||||
|
|
||||||
def set_fft_low_cut_percent() -> None:
|
def set_fft_low_cut_percent() -> None:
|
||||||
@ -2613,6 +2664,8 @@ def run_pyqtgraph(args) -> None:
|
|||||||
try:
|
try:
|
||||||
range_min_spin.valueChanged.connect(lambda _v: set_working_range())
|
range_min_spin.valueChanged.connect(lambda _v: set_working_range())
|
||||||
range_max_spin.valueChanged.connect(lambda _v: set_working_range())
|
range_max_spin.valueChanged.connect(lambda _v: set_working_range())
|
||||||
|
bscan_db_min_spin.valueChanged.connect(lambda _v: set_bscan_db_range())
|
||||||
|
bscan_db_max_spin.valueChanged.connect(lambda _v: set_bscan_db_range())
|
||||||
tty_range_spin.valueChanged.connect(lambda _v: set_tty_range())
|
tty_range_spin.valueChanged.connect(lambda _v: set_tty_range())
|
||||||
calib_cb.stateChanged.connect(lambda _v: set_calib_enabled())
|
calib_cb.stateChanged.connect(lambda _v: set_calib_enabled())
|
||||||
complex_calib_cb.stateChanged.connect(lambda _v: set_complex_calib_enabled())
|
complex_calib_cb.stateChanged.connect(lambda _v: set_complex_calib_enabled())
|
||||||
@ -3783,38 +3836,19 @@ def run_pyqtgraph(args) -> None:
|
|||||||
log_debug_event("invalid_fft_image", "ui invalid FFT waterfall suppressed")
|
log_debug_event("invalid_fft_image", "ui invalid FFT waterfall suppressed")
|
||||||
return
|
return
|
||||||
|
|
||||||
levels = None
|
# Fixed color scale — never recomputed per frame, so the image no
|
||||||
|
# longer "changes"/flickers as new sweeps roll in.
|
||||||
if active_background is not None:
|
if active_background is not None:
|
||||||
levels = compute_background_subtracted_bscan_levels(disp_fft_lin, disp_fft)
|
# Residuals are on a different scale than absolute dB; use the
|
||||||
|
# dedicated bg-subtracted levels, computed ONCE and frozen.
|
||||||
|
levels = bscan_bg_levels_frozen["held"]
|
||||||
if levels is None:
|
if levels is None:
|
||||||
try:
|
levels = compute_background_subtracted_bscan_levels(disp_fft_lin, disp_fft)
|
||||||
finite_rows = np.any(np.isfinite(disp_fft), axis=1)
|
if levels is not None:
|
||||||
if np.any(finite_rows):
|
bscan_bg_levels_frozen["held"] = levels
|
||||||
mean_spec = np.nanmean(disp_fft[finite_rows], axis=1)
|
else:
|
||||||
if mean_spec.size > 0:
|
# Normal view: constant manual dB window from the spin boxes.
|
||||||
vmin_v = float(np.nanmin(mean_spec))
|
levels = (bscan_manual_levels["vmin"], bscan_manual_levels["vmax"])
|
||||||
vmax_v = float(np.nanmax(mean_spec))
|
|
||||||
if np.isfinite(vmin_v) and np.isfinite(vmax_v) and vmin_v != vmax_v:
|
|
||||||
levels = (vmin_v, vmax_v)
|
|
||||||
except Exception:
|
|
||||||
levels = None
|
|
||||||
if levels is None and spec_clip is not None:
|
|
||||||
try:
|
|
||||||
vmin_v = float(np.nanpercentile(disp_fft, spec_clip[0]))
|
|
||||||
vmax_v = float(np.nanpercentile(disp_fft, spec_clip[1]))
|
|
||||||
if np.isfinite(vmin_v) and np.isfinite(vmax_v) and vmin_v != vmax_v:
|
|
||||||
levels = (vmin_v, vmax_v)
|
|
||||||
except Exception:
|
|
||||||
levels = None
|
|
||||||
if (
|
|
||||||
levels is None
|
|
||||||
and runtime.ring.y_min_fft is not None
|
|
||||||
and runtime.ring.y_max_fft is not None
|
|
||||||
and np.isfinite(runtime.ring.y_min_fft)
|
|
||||||
and np.isfinite(runtime.ring.y_max_fft)
|
|
||||||
and runtime.ring.y_min_fft != runtime.ring.y_max_fft
|
|
||||||
):
|
|
||||||
levels = (runtime.ring.y_min_fft, runtime.ring.y_max_fft)
|
|
||||||
disp_fft_display_axis = display_distance_axis_for_mode(disp_fft_axis, fft_mode)
|
disp_fft_display_axis = display_distance_axis_for_mode(disp_fft_axis, fft_mode)
|
||||||
if display_distance_transform_enabled(fft_mode) and disp_fft_display_axis.size == disp_fft.shape[0]:
|
if display_distance_transform_enabled(fft_mode) and disp_fft_display_axis.size == disp_fft.shape[0]:
|
||||||
disp_fft = disp_fft[::-1, :]
|
disp_fft = disp_fft[::-1, :]
|
||||||
@ -3839,28 +3873,6 @@ def run_pyqtgraph(args) -> None:
|
|||||||
y_bounds=(d_min, d_max),
|
y_bounds=(d_min, d_max),
|
||||||
padding=0,
|
padding=0,
|
||||||
)
|
)
|
||||||
# Smooth the color levels with a slow EMA so brightness stops
|
|
||||||
# flickering while still tracking genuine signal-strength changes.
|
|
||||||
# Background-subtracted levels use a separate slot (different scale).
|
|
||||||
level_slot = "held_bg" if active_background is not None else "held"
|
|
||||||
if levels is not None:
|
|
||||||
prev_levels = bscan_levels_state[level_slot]
|
|
||||||
if prev_levels is None:
|
|
||||||
smoothed_levels = (float(levels[0]), float(levels[1]))
|
|
||||||
else:
|
|
||||||
alpha = 1.0 - math.exp(
|
|
||||||
-(interval_ms / 1000.0) / max(1e-6, BSCAN_LEVEL_TAU_S)
|
|
||||||
)
|
|
||||||
smoothed_levels = (
|
|
||||||
prev_levels[0] + alpha * (float(levels[0]) - prev_levels[0]),
|
|
||||||
prev_levels[1] + alpha * (float(levels[1]) - prev_levels[1]),
|
|
||||||
)
|
|
||||||
bscan_levels_state[level_slot] = smoothed_levels
|
|
||||||
levels = smoothed_levels
|
|
||||||
else:
|
|
||||||
# No fresh measurement this frame: reuse the held levels so the
|
|
||||||
# image never flashes to autoLevels.
|
|
||||||
levels = bscan_levels_state[level_slot]
|
|
||||||
if levels is not None:
|
if levels is not None:
|
||||||
img_fft.setImage(disp_fft, autoLevels=False, levels=levels)
|
img_fft.setImage(disp_fft, autoLevels=False, levels=levels)
|
||||||
else:
|
else:
|
||||||
|
|||||||
Reference in New Issue
Block a user