|
|
|
@ -827,9 +827,15 @@ 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]:
|
|
|
|
def try_parse_bscan_db_range(spec: Optional[str]) -> Optional[Tuple[float, float]]:
|
|
|
|
"""Parse a fixed B-scan dB color window 'min,max'. Falls back to the default."""
|
|
|
|
"""Parse an explicit B-scan dB window 'min,max'. Returns None if not explicit.
|
|
|
|
if spec:
|
|
|
|
|
|
|
|
|
|
|
|
'auto' (or empty/invalid) returns None, meaning "seed once from the first frame".
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
if not spec:
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
if str(spec).strip().lower() == "auto":
|
|
|
|
|
|
|
|
return None
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
p0, p1 = str(spec).strip().replace(";", ",").split(",")
|
|
|
|
p0, p1 = str(spec).strip().replace(";", ",").split(",")
|
|
|
|
lo, hi = float(p0), float(p1)
|
|
|
|
lo, hi = float(p0), float(p1)
|
|
|
|
@ -837,7 +843,13 @@ def parse_bscan_db_range(spec: Optional[str]) -> Tuple[float, float]:
|
|
|
|
return (lo, hi)
|
|
|
|
return (lo, hi)
|
|
|
|
except Exception:
|
|
|
|
except Exception:
|
|
|
|
pass
|
|
|
|
pass
|
|
|
|
return BSCAN_DB_RANGE_DEFAULT
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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."""
|
|
|
|
|
|
|
|
parsed = try_parse_bscan_db_range(spec)
|
|
|
|
|
|
|
|
return parsed if parsed is not None else BSCAN_DB_RANGE_DEFAULT
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def compute_background_subtracted_bscan_levels(
|
|
|
|
def compute_background_subtracted_bscan_levels(
|
|
|
|
@ -922,7 +934,10 @@ 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))
|
|
|
|
bscan_db_explicit_range = try_parse_bscan_db_range(getattr(args, "bscan_db_range", None))
|
|
|
|
|
|
|
|
bscan_db_vmin, bscan_db_vmax = (
|
|
|
|
|
|
|
|
bscan_db_explicit_range if bscan_db_explicit_range is not None else BSCAN_DB_RANGE_DEFAULT
|
|
|
|
|
|
|
|
)
|
|
|
|
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),
|
|
|
|
@ -1389,6 +1404,10 @@ def run_pyqtgraph(args) -> None:
|
|
|
|
# Fixed manual dB color window for the normal (no-background) B-scan view.
|
|
|
|
# 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.
|
|
|
|
# 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}
|
|
|
|
bscan_manual_levels: Dict[str, float] = {"vmin": bscan_db_vmin, "vmax": bscan_db_vmax}
|
|
|
|
|
|
|
|
# Seed the manual window once from the first real frame (unless the user set an
|
|
|
|
|
|
|
|
# explicit --bscan-db-range) so the signal is visible out of the box; it stays
|
|
|
|
|
|
|
|
# fixed/manual afterwards.
|
|
|
|
|
|
|
|
bscan_levels_seeded: Dict[str, bool] = {"done": bscan_db_explicit_range is not None}
|
|
|
|
# Frozen levels for the background-subtracted residual view (different scale):
|
|
|
|
# Frozen levels for the background-subtracted residual view (different scale):
|
|
|
|
# computed once when background is active, held until a reset event clears it.
|
|
|
|
# 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}
|
|
|
|
bscan_bg_levels_frozen: Dict[str, Optional[Tuple[float, float]]] = {"held": None}
|
|
|
|
@ -1691,9 +1710,15 @@ def run_pyqtgraph(args) -> None:
|
|
|
|
)
|
|
|
|
)
|
|
|
|
set_x_range_if_changed("line_x", p_line, f_min, f_max, padding=0)
|
|
|
|
set_x_range_if_changed("line_x", p_line, f_min, f_max, padding=0)
|
|
|
|
|
|
|
|
|
|
|
|
distance_bounds = resolve_axis_bounds(runtime.ring.distance_axis)
|
|
|
|
# Use the SAME stable full-scale axis as the B-scan update block so both
|
|
|
|
if distance_bounds is not None:
|
|
|
|
# write identical bounds to the shared fft_waterfall cache. Using the
|
|
|
|
display_axis_full = display_distance_axis_for_mode(runtime.ring.distance_axis, fft_mode)
|
|
|
|
# per-frame runtime.ring.distance_axis here made the two thrash the cache
|
|
|
|
|
|
|
|
# and the B-scan y-axis jumped every frame.
|
|
|
|
|
|
|
|
bscan_axis = stable_full_scale_distance_axis()
|
|
|
|
|
|
|
|
if bscan_axis is None:
|
|
|
|
|
|
|
|
bscan_axis = runtime.ring.distance_axis
|
|
|
|
|
|
|
|
if bscan_axis is not None:
|
|
|
|
|
|
|
|
display_axis_full = display_distance_axis_for_mode(bscan_axis, fft_mode)
|
|
|
|
display_bounds = resolve_axis_bounds(display_axis_full)
|
|
|
|
display_bounds = resolve_axis_bounds(display_axis_full)
|
|
|
|
if display_bounds is not None:
|
|
|
|
if display_bounds is not None:
|
|
|
|
d_min_display, d_max_display = display_bounds
|
|
|
|
d_min_display, d_max_display = display_bounds
|
|
|
|
@ -2131,8 +2156,13 @@ def run_pyqtgraph(args) -> None:
|
|
|
|
runtime.current_secondary_phase = None
|
|
|
|
runtime.current_secondary_phase = None
|
|
|
|
|
|
|
|
|
|
|
|
if runtime.current_sweep_raw.size == 0:
|
|
|
|
if runtime.current_sweep_raw.size == 0:
|
|
|
|
if push_to_ring:
|
|
|
|
# A single empty/partial sweep (no points in the working range) must NOT
|
|
|
|
reset_ring_buffers()
|
|
|
|
# wipe the whole waterfall — that made the B-scan flash to zero and back
|
|
|
|
|
|
|
|
# (the "мерцание"/jumping max). Skip this frame and keep the existing ring
|
|
|
|
|
|
|
|
# history intact. Deliberate resets come through reset_ring=True (handled
|
|
|
|
|
|
|
|
# at the top of this function), so nothing to clear here.
|
|
|
|
|
|
|
|
if push_to_ring and not reset_ring:
|
|
|
|
|
|
|
|
log_debug_event("empty_sweep_skipped", "ui empty sweep skipped (ring preserved)")
|
|
|
|
runtime.current_freqs = None
|
|
|
|
runtime.current_freqs = None
|
|
|
|
runtime.current_sweep_raw = None
|
|
|
|
runtime.current_sweep_raw = None
|
|
|
|
runtime.current_fft_source = None
|
|
|
|
runtime.current_fft_source = None
|
|
|
|
@ -3847,7 +3877,27 @@ def run_pyqtgraph(args) -> None:
|
|
|
|
if levels is not None:
|
|
|
|
if levels is not None:
|
|
|
|
bscan_bg_levels_frozen["held"] = levels
|
|
|
|
bscan_bg_levels_frozen["held"] = levels
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
# Normal view: constant manual dB window from the spin boxes.
|
|
|
|
# Normal view: seed the manual window once from the first real
|
|
|
|
|
|
|
|
# frame (unless the user gave an explicit --bscan-db-range), then
|
|
|
|
|
|
|
|
# hold it fixed so the image never flickers.
|
|
|
|
|
|
|
|
if not bscan_levels_seeded["done"]:
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
lo = float(np.nanpercentile(disp_fft, 2.0))
|
|
|
|
|
|
|
|
hi = float(np.nanpercentile(disp_fft, 98.0))
|
|
|
|
|
|
|
|
if np.isfinite(lo) and np.isfinite(hi) and lo < hi:
|
|
|
|
|
|
|
|
bscan_manual_levels["vmin"] = lo
|
|
|
|
|
|
|
|
bscan_manual_levels["vmax"] = hi
|
|
|
|
|
|
|
|
for _spin, _val in (
|
|
|
|
|
|
|
|
(bscan_db_min_spin, lo),
|
|
|
|
|
|
|
|
(bscan_db_max_spin, hi),
|
|
|
|
|
|
|
|
):
|
|
|
|
|
|
|
|
_spin.blockSignals(True)
|
|
|
|
|
|
|
|
_spin.setValue(_val)
|
|
|
|
|
|
|
|
_spin.blockSignals(False)
|
|
|
|
|
|
|
|
bscan_levels_seeded["done"] = True
|
|
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
# Constant manual dB window from the spin boxes.
|
|
|
|
levels = (bscan_manual_levels["vmin"], bscan_manual_levels["vmax"])
|
|
|
|
levels = (bscan_manual_levels["vmin"], bscan_manual_levels["vmax"])
|
|
|
|
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]:
|
|
|
|
|