implemented dynamic waterfall scaling
This commit is contained in:
@ -34,6 +34,28 @@ WF_WIDTH = 1000 # максимальное число точек в ряду в
|
|||||||
FFT_LEN = 1024 # длина БПФ для спектра/водопада спектров
|
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):
|
def try_open_pyserial(path: str, baud: int, timeout: float):
|
||||||
try:
|
try:
|
||||||
import serial # type: ignore
|
import serial # type: ignore
|
||||||
@ -335,6 +357,14 @@ def main():
|
|||||||
parser.add_argument("--max-sweeps", type=int, default=200, help="Количество видимых свипов в водопаде")
|
parser.add_argument("--max-sweeps", type=int, default=200, help="Количество видимых свипов в водопаде")
|
||||||
parser.add_argument("--max-fps", type=float, default=30.0, help="Лимит частоты отрисовки, кадров/с")
|
parser.add_argument("--max-fps", type=float, default=30.0, help="Лимит частоты отрисовки, кадров/с")
|
||||||
parser.add_argument("--cmap", default="viridis", 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("--title", default="ADC Sweeps", help="Заголовок окна")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--ylim",
|
"--ylim",
|
||||||
@ -394,6 +424,8 @@ def main():
|
|||||||
ring_fft = None # type: Optional[np.ndarray]
|
ring_fft = None # type: Optional[np.ndarray]
|
||||||
y_min_fft, y_max_fft = None, None
|
y_min_fft, y_max_fft = None, None
|
||||||
freq_shared: Optional[np.ndarray] = None
|
freq_shared: Optional[np.ndarray] = None
|
||||||
|
# Параметры контраста водопада спектров
|
||||||
|
spec_clip = _parse_spec_clip(getattr(args, "spec_clip", None))
|
||||||
|
|
||||||
# Линейный график последнего свипа
|
# Линейный график последнего свипа
|
||||||
line_obj, = ax_line.plot([], [], lw=1)
|
line_obj, = ax_line.plot([], [], lw=1)
|
||||||
@ -609,9 +641,27 @@ def main():
|
|||||||
if changed and ring_fft is not None:
|
if changed and ring_fft is not None:
|
||||||
disp_fft = make_display_ring_fft()
|
disp_fft = make_display_ring_fft()
|
||||||
img_fft_obj.set_data(disp_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:
|
try:
|
||||||
img_fft_obj.set_clim(vmin=y_min_fft, vmax=y_max_fft)
|
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)
|
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
|
ring_fft: Optional[np.ndarray] = None
|
||||||
freq_shared: Optional[np.ndarray] = None
|
freq_shared: Optional[np.ndarray] = None
|
||||||
y_min_fft, y_max_fft = None, None
|
y_min_fft, y_max_fft = None, None
|
||||||
|
# Параметры контраста водопада спектров (процентильная обрезка)
|
||||||
|
spec_clip = _parse_spec_clip(getattr(args, "spec_clip", None))
|
||||||
# Диапазон по Y: авто по умолчанию (поддерживает отрицательные значения)
|
# Диапазон по Y: авто по умолчанию (поддерживает отрицательные значения)
|
||||||
fixed_ylim: Optional[Tuple[float, float]] = None
|
fixed_ylim: Optional[Tuple[float, float]] = None
|
||||||
if args.ylim:
|
if args.ylim:
|
||||||
@ -833,8 +885,30 @@ def run_pyqtgraph(args):
|
|||||||
|
|
||||||
if changed and ring_fft is not None:
|
if changed and ring_fft is not None:
|
||||||
disp_fft = ring_fft if head == 0 else np.roll(ring_fft, -head, axis=0)
|
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:
|
else:
|
||||||
img_fft.setImage(disp_fft, autoLevels=False)
|
img_fft.setImage(disp_fft, autoLevels=False)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user