fix
This commit is contained in:
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import math
|
||||||
import signal
|
import signal
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
@ -31,7 +32,12 @@ from rfg_adc_plotter.processing.calibration import (
|
|||||||
save_complex_calibration,
|
save_complex_calibration,
|
||||||
set_calibration_base_value,
|
set_calibration_base_value,
|
||||||
)
|
)
|
||||||
from rfg_adc_plotter.processing.fft import compute_fft_complex_row, compute_fft_mag_row, fft_mag_to_db
|
from rfg_adc_plotter.processing.fft import (
|
||||||
|
compute_distance_axis,
|
||||||
|
compute_fft_complex_row,
|
||||||
|
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.formatting import compute_auto_ylim, format_status_kv, parse_spec_clip
|
||||||
from rfg_adc_plotter.processing.normalization import (
|
from rfg_adc_plotter.processing.normalization import (
|
||||||
normalize_by_complex_calibration,
|
normalize_by_complex_calibration,
|
||||||
@ -49,6 +55,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
|
||||||
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
|
||||||
@ -1346,6 +1353,8 @@ def run_pyqtgraph(args) -> None:
|
|||||||
last_packet_processed_at: Optional[float] = None
|
last_packet_processed_at: Optional[float] = 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_levels_state: Dict[str, Optional[Tuple[float, float]]] = {"held": None, "held_bg": 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] = {}
|
||||||
@ -1441,6 +1450,43 @@ def run_pyqtgraph(args) -> None:
|
|||||||
image_rect_cache[key] = values
|
image_rect_cache[key] = values
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def stable_full_scale_distance_axis() -> Optional[np.ndarray]:
|
||||||
|
"""Full-scale B-scan distance axis, computed once per (range, mode).
|
||||||
|
|
||||||
|
The per-sweep ``runtime.ring.distance_axis`` jitters when the reported
|
||||||
|
``f_max`` varies between sweeps, which rescales the B-scan y-axis every
|
||||||
|
frame. Deriving the axis from the configured range keeps it stable and
|
||||||
|
only recomputes when the operator changes ``f min``/``f max`` or the mode.
|
||||||
|
"""
|
||||||
|
key = (
|
||||||
|
round(float(runtime.range_min_ghz), 9),
|
||||||
|
round(float(runtime.range_max_ghz), 9),
|
||||||
|
str(fft_mode),
|
||||||
|
)
|
||||||
|
cached = bscan_axis_cache.get(key)
|
||||||
|
if cached is not None:
|
||||||
|
return cached
|
||||||
|
if display_distance_transform_enabled(fft_mode):
|
||||||
|
# positive_only_exact derives its step from real sweep geometry, so a
|
||||||
|
# synthetic 2-point freq array would be wrong. Hold the first valid
|
||||||
|
# real axis instead (still keyed by mode, so it re-seeds on switch).
|
||||||
|
real_axis = runtime.ring.distance_axis
|
||||||
|
if real_axis is None:
|
||||||
|
return None
|
||||||
|
axis = np.asarray(real_axis, dtype=np.float64).reshape(-1)
|
||||||
|
if axis.size <= 0 or not np.all(np.isfinite(axis)):
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
synth_freqs = np.asarray(
|
||||||
|
[runtime.range_min_ghz, runtime.range_max_ghz], dtype=np.float64
|
||||||
|
)
|
||||||
|
axis = compute_distance_axis(synth_freqs, fft_bins, mode=fft_mode)
|
||||||
|
if axis is None or axis.size <= 0 or not np.all(np.isfinite(axis)):
|
||||||
|
return None
|
||||||
|
bscan_axis_cache.clear()
|
||||||
|
bscan_axis_cache[key] = axis
|
||||||
|
return axis
|
||||||
|
|
||||||
def set_item_visible_if_changed(key: str, item, visible: bool) -> bool:
|
def set_item_visible_if_changed(key: str, item, visible: bool) -> bool:
|
||||||
visible_value = bool(visible)
|
visible_value = bool(visible)
|
||||||
if item_visibility_cache.get(key) == visible_value:
|
if item_visibility_cache.get(key) == visible_value:
|
||||||
@ -1887,6 +1933,10 @@ 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
|
||||||
|
# scale across the background on/off transition.
|
||||||
|
bscan_levels_state["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
|
||||||
@ -1905,6 +1955,9 @@ def run_pyqtgraph(args) -> None:
|
|||||||
runtime.ring.reset()
|
runtime.ring.reset()
|
||||||
axis_range_cache.clear()
|
axis_range_cache.clear()
|
||||||
image_rect_cache.clear()
|
image_rect_cache.clear()
|
||||||
|
bscan_axis_cache.clear()
|
||||||
|
bscan_levels_state["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
|
||||||
@ -2526,6 +2579,7 @@ def run_pyqtgraph(args) -> None:
|
|||||||
except Exception:
|
except Exception:
|
||||||
fft_mode = "symmetric"
|
fft_mode = "symmetric"
|
||||||
runtime.ring.set_fft_mode(fft_mode)
|
runtime.ring.set_fft_mode(fft_mode)
|
||||||
|
bscan_axis_cache.clear()
|
||||||
reset_background_state(clear_profile=True)
|
reset_background_state(clear_profile=True)
|
||||||
runtime.current_distances = runtime.ring.distance_axis
|
runtime.current_distances = runtime.ring.distance_axis
|
||||||
runtime.current_fft_mag = None
|
runtime.current_fft_mag = None
|
||||||
@ -3765,6 +3819,15 @@ def run_pyqtgraph(args) -> None:
|
|||||||
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, :]
|
||||||
disp_fft_display_axis = disp_fft_display_axis[::-1]
|
disp_fft_display_axis = disp_fft_display_axis[::-1]
|
||||||
|
# Use a stable full-scale distance axis for the y-range/rect so the
|
||||||
|
# B-scan always spans ~0..12 m and never rescales per sweep. Fall
|
||||||
|
# back to the per-frame axis only until a stable axis is available.
|
||||||
|
stable_axis = stable_full_scale_distance_axis()
|
||||||
|
if stable_axis is not None:
|
||||||
|
distance_bounds = resolve_axis_bounds(
|
||||||
|
display_distance_axis_for_mode(stable_axis, fft_mode)
|
||||||
|
)
|
||||||
|
else:
|
||||||
distance_bounds = resolve_axis_bounds(disp_fft_display_axis)
|
distance_bounds = resolve_axis_bounds(disp_fft_display_axis)
|
||||||
if distance_bounds is not None:
|
if distance_bounds is not None:
|
||||||
d_min, d_max = distance_bounds
|
d_min, d_max = distance_bounds
|
||||||
@ -3776,6 +3839,28 @@ 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