From 476c601b957fe092d4110467b54c43e2731ca08e Mon Sep 17 00:00:00 2001 From: Theodor Chikin Date: Sun, 21 Dec 2025 14:26:54 +0300 Subject: [PATCH] implemented dynamic waterfall scaling --- RFG_ADC_dataplotter.py | 84 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 79 insertions(+), 5 deletions(-) diff --git a/RFG_ADC_dataplotter.py b/RFG_ADC_dataplotter.py index 4f27d69..f094220 100755 --- a/RFG_ADC_dataplotter.py +++ b/RFG_ADC_dataplotter.py @@ -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)