implemented dynamic waterfall scaling

This commit is contained in:
2025-12-21 14:26:54 +03:00
parent b798558d76
commit 476c601b95

View File

@ -34,6 +34,28 @@ WF_WIDTH = 1000 # максимальное число точек в ряду в
FFT_LEN = 1024 # длина БПФ для спектра/водопада спектров
def _parse_spec_clip(spec: Optional[str]) -> Optional[Tuple[float, float]]:
"""Разобрать строку вида "low,high" процентов для контрастного отображения водопада спектров.
Возвращает пару (low, high) или None для отключения. Допустимы значения 0..100, low < high.
Ключевые слова отключения: "off", "none", "no".
"""
if not spec:
return None
s = str(spec).strip().lower()
if s in ("off", "none", "no"):
return None
try:
p0, p1 = s.replace(";", ",").split(",")
low = float(p0)
high = float(p1)
if not (0.0 <= low < high <= 100.0):
return None
return (low, high)
except Exception:
return None
def try_open_pyserial(path: str, baud: int, timeout: float):
try:
import serial # type: ignore
@ -335,6 +357,14 @@ def main():
parser.add_argument("--max-sweeps", type=int, default=200, help="Количество видимых свипов в водопаде")
parser.add_argument("--max-fps", type=float, default=30.0, help="Лимит частоты отрисовки, кадров/с")
parser.add_argument("--cmap", default="viridis", help="Цветовая карта водопада")
parser.add_argument(
"--spec-clip",
default="2,98",
help=(
"Процентильная обрезка уровней водопада спектров, % (min,max). "
"Напр. 2,98. 'off' — отключить"
),
)
parser.add_argument("--title", default="ADC Sweeps", help="Заголовок окна")
parser.add_argument(
"--ylim",
@ -394,6 +424,8 @@ def main():
ring_fft = None # type: Optional[np.ndarray]
y_min_fft, y_max_fft = None, None
freq_shared: Optional[np.ndarray] = None
# Параметры контраста водопада спектров
spec_clip = _parse_spec_clip(getattr(args, "spec_clip", None))
# Линейный график последнего свипа
line_obj, = ax_line.plot([], [], lw=1)
@ -609,9 +641,27 @@ def main():
if changed and ring_fft is not None:
disp_fft = make_display_ring_fft()
img_fft_obj.set_data(disp_fft)
if y_min_fft is not None and y_max_fft is not None and np.isfinite(y_min_fft) and np.isfinite(y_max_fft):
if y_min_fft != y_max_fft:
img_fft_obj.set_clim(vmin=y_min_fft, vmax=y_max_fft)
# Автодиапазон по средним (по видимым данным): берём средний спектр по времени
try:
mean_spec = np.nanmean(disp_fft, axis=0)
vmin_v = float(np.nanmin(mean_spec))
vmax_v = float(np.nanmax(mean_spec))
except Exception:
vmin_v = vmax_v = None
# Если средние не дают валидный диапазон — используем процентильную обрезку (если задана)
if (vmin_v is None or not np.isfinite(vmin_v)) or (vmax_v is None or not np.isfinite(vmax_v)) or vmin_v == vmax_v:
if 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]))
except Exception:
vmin_v = vmax_v = None
# Фолбэк к отслеживаемым минимум/максимумам
if (vmin_v is None or not np.isfinite(vmin_v)) or (vmax_v is None or not np.isfinite(vmax_v)) or vmin_v == vmax_v:
if y_min_fft is not None and y_max_fft is not None and np.isfinite(y_min_fft) and np.isfinite(y_max_fft) and y_min_fft != y_max_fft:
vmin_v, vmax_v = y_min_fft, y_max_fft
if vmin_v is not None and vmax_v is not None and vmin_v != vmax_v:
img_fft_obj.set_clim(vmin=vmin_v, vmax=vmax_v)
# Возвращаем обновлённые артисты
return (line_obj, img_obj, fft_line_obj, img_fft_obj)
@ -700,6 +750,8 @@ def run_pyqtgraph(args):
ring_fft: Optional[np.ndarray] = None
freq_shared: Optional[np.ndarray] = None
y_min_fft, y_max_fft = None, None
# Параметры контраста водопада спектров (процентильная обрезка)
spec_clip = _parse_spec_clip(getattr(args, "spec_clip", None))
# Диапазон по Y: авто по умолчанию (поддерживает отрицательные значения)
fixed_ylim: Optional[Tuple[float, float]] = None
if args.ylim:
@ -833,8 +885,30 @@ def run_pyqtgraph(args):
if changed and ring_fft is not None:
disp_fft = ring_fft if head == 0 else np.roll(ring_fft, -head, axis=0)
if y_min_fft is not None and y_max_fft is not None and y_min_fft != y_max_fft and np.isfinite(y_min_fft) and np.isfinite(y_max_fft):
img_fft.setImage(disp_fft, autoLevels=False, levels=(y_min_fft, y_max_fft))
# Автодиапазон по среднему спектру за видимый интервал
levels = None
try:
mean_spec = np.nanmean(disp_fft, axis=0)
vmin_v = float(np.nanmin(mean_spec))
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 y_min_fft is not None and y_max_fft is not None and np.isfinite(y_min_fft) and np.isfinite(y_max_fft) and y_min_fft != y_max_fft:
levels = (y_min_fft, y_max_fft)
if levels is not None:
img_fft.setImage(disp_fft, autoLevels=False, levels=levels)
else:
img_fft.setImage(disp_fft, autoLevels=False)