reference

This commit is contained in:
awe
2026-02-03 15:10:22 +03:00
parent 3bc2382bd0
commit 61816cf894

View File

@ -2,9 +2,11 @@
Визуализация данных с использованием pyqtgraph (быстрый бэкенд). Визуализация данных с использованием pyqtgraph (быстрый бэкенд).
""" """
import csv
import sys import sys
import threading import threading
import time import time
from datetime import datetime
from queue import Empty, Queue from queue import Empty, Queue
from typing import Optional, Tuple from typing import Optional, Tuple
@ -13,11 +15,13 @@ import numpy as np
try: try:
import pyqtgraph as pg import pyqtgraph as pg
from PyQt5 import QtCore, QtWidgets # noqa: F401 from PyQt5 import QtCore, QtWidgets # noqa: F401
from PyQt5.QtWidgets import QPushButton, QWidget, QHBoxLayout, QCheckBox, QFileDialog
except Exception: except Exception:
# Возможно установлена PySide6 # Возможно установлена PySide6
try: try:
import pyqtgraph as pg import pyqtgraph as pg
from PySide6 import QtCore, QtWidgets # noqa: F401 from PySide6 import QtCore, QtWidgets # noqa: F401
from PySide6.QtWidgets import QPushButton, QWidget, QHBoxLayout, QCheckBox, QFileDialog
except Exception as e: except Exception as e:
raise RuntimeError( raise RuntimeError(
"pyqtgraph/PyQt5(Pyside6) не найдены. Установите: pip install pyqtgraph PyQt5" "pyqtgraph/PyQt5(Pyside6) не найдены. Установите: pip install pyqtgraph PyQt5"
@ -138,6 +142,131 @@ def run_pyqtgraph(args):
status = pg.LabelItem(justify="left") status = pg.LabelItem(justify="left")
win.addItem(status, row=3, col=0, colspan=2) win.addItem(status, row=3, col=0, colspan=2)
# Функция сохранения медианы последних 1000 свипов
def save_median_data():
"""Сохранить медиану последних 1000 свипов в CSV файл"""
if ring is None:
status.setText("Нет данных для сохранения")
return
# Определяем сколько свипов доступно
n_sweeps = 1000
available = min(n_sweeps, max_sweeps)
# Проверяем сколько свипов реально заполнено
filled_count = np.count_nonzero(~np.isnan(ring[:, 0]))
if filled_count == 0:
status.setText("Нет данных для сохранения")
return
available = min(available, filled_count)
# Получаем хронологически упорядоченные данные
ordered = ring if head == 0 else np.roll(ring, -head, axis=0)
# Берем последние n свипов
recent_sweeps = ordered[-available:, :]
# Вычисляем медиану по свипам (ось 0)
median_sweep = np.nanmedian(recent_sweeps, axis=0)
# Сохраняем в CSV
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"median_sweep_{timestamp}.csv"
try:
with open(filename, 'w', newline='') as f:
writer = csv.writer(f)
writer.writerow(['Index', 'Median_Value'])
for i, value in enumerate(median_sweep):
if np.isfinite(value):
writer.writerow([i, float(value)])
status.setText(f"Сохранено {available} свипов (медиана) в {filename}")
except Exception as e:
status.setText(f"Ошибка сохранения: {e}")
# Функция загрузки медианного файла
def load_median_file():
"""Загрузить медианный файл из CSV"""
nonlocal median_data
filename, _ = QFileDialog.getOpenFileName(
None,
"Выберите файл с медианой",
"",
"CSV Files (*.csv);;All Files (*)"
)
if not filename:
return
try:
# Загружаем CSV файл
data = []
with open(filename, 'r') as f:
reader = csv.reader(f)
next(reader) # Пропускаем заголовок
for row in reader:
if len(row) >= 2:
try:
data.append(float(row[1]))
except ValueError:
continue
if not data:
status.setText("Ошибка: файл пустой или неверный формат")
return
median_data = np.array(data, dtype=np.float32)
status.setText(f"Загружена медиана из {filename} ({len(median_data)} точек)")
# Автоматически включаем чекбокс
subtract_checkbox.setChecked(True)
except Exception as e:
status.setText(f"Ошибка загрузки: {e}")
median_data = None
# Функция переключения вычитания медианы
def toggle_median_subtraction(state):
nonlocal median_subtract_enabled
median_subtract_enabled = bool(state)
if median_subtract_enabled and median_data is None:
status.setText("Сначала загрузите файл с медианой")
subtract_checkbox.setChecked(False)
elif median_subtract_enabled:
status.setText("Вычитание медианы включено")
else:
status.setText("Вычитание медианы выключено")
# Создаем контейнер для кнопок управления
button_container = QWidget()
button_layout = QHBoxLayout()
# Кнопка сохранения медианы
save_btn = QPushButton("Сохранить медиану (1000 свипов)")
save_btn.clicked.connect(save_median_data)
button_layout.addWidget(save_btn)
# Кнопка загрузки медианы
load_btn = QPushButton("Загрузить медиану")
load_btn.clicked.connect(load_median_file)
button_layout.addWidget(load_btn)
# Чекбокс для включения вычитания
subtract_checkbox = QCheckBox("Вычитать медиану")
subtract_checkbox.stateChanged.connect(toggle_median_subtraction)
button_layout.addWidget(subtract_checkbox)
button_layout.setContentsMargins(5, 5, 5, 5)
button_container.setLayout(button_layout)
# Добавляем кнопки в окно
proxy_widget = QtWidgets.QGraphicsProxyWidget()
proxy_widget.setWidget(button_container)
win.addItem(proxy_widget, row=4, col=0, colspan=2)
# Состояние # Состояние
ring: Optional[np.ndarray] = None ring: Optional[np.ndarray] = None
head = 0 head = 0
@ -145,6 +274,9 @@ def run_pyqtgraph(args):
x_shared: Optional[np.ndarray] = None x_shared: Optional[np.ndarray] = None
current_sweep: Optional[np.ndarray] = None current_sweep: Optional[np.ndarray] = None
current_info: Optional[SweepInfo] = None current_info: Optional[SweepInfo] = None
# Медианные данные для вычитания
median_data: Optional[np.ndarray] = None
median_subtract_enabled = False
# Авто-уровни цветовой шкалы водопада сырых данных пересчитываются по видимой области. # Авто-уровни цветовой шкалы водопада сырых данных пересчитываются по видимой области.
# Для спектров (полное FFT для отрицательных частот) # Для спектров (полное FFT для отрицательных частот)
fft_bins = FFT_LEN fft_bins = FFT_LEN
@ -232,6 +364,15 @@ def run_pyqtgraph(args):
nonlocal ring_phase, prev_phase_per_bin, phase_offset_per_bin, y_min_phase, y_max_phase nonlocal ring_phase, prev_phase_per_bin, phase_offset_per_bin, y_min_phase, y_max_phase
if s is None or s.size == 0 or ring is None: if s is None or s.size == 0 or ring is None:
return return
# Применяем вычитание медианы если включено
if median_subtract_enabled and median_data is not None:
# Вычитаем медиану из сигнала
take_median = min(s.size, median_data.size)
s_corrected = s.copy()
s_corrected[:take_median] = s[:take_median] - median_data[:take_median]
s = s_corrected
w = ring.shape[1] w = ring.shape[1]
row = np.full((w,), np.nan, dtype=np.float32) row = np.full((w,), np.nan, dtype=np.float32)
take = min(w, s.size) take = min(w, s.size)
@ -336,20 +477,27 @@ def run_pyqtgraph(args):
def update(): def update():
changed = drain_queue() > 0 changed = drain_queue() > 0
if current_sweep is not None and x_shared is not None: if current_sweep is not None and x_shared is not None:
if current_sweep.size <= x_shared.size: # Применяем вычитание медианы для отображения
xs = x_shared[: current_sweep.size] display_sweep = current_sweep
if median_subtract_enabled and median_data is not None:
take_median = min(current_sweep.size, median_data.size)
display_sweep = current_sweep.copy()
display_sweep[:take_median] = current_sweep[:take_median] - median_data[:take_median]
if display_sweep.size <= x_shared.size:
xs = x_shared[: display_sweep.size]
else: else:
xs = np.arange(current_sweep.size) xs = np.arange(display_sweep.size)
curve.setData(xs, current_sweep, autoDownsample=True) curve.setData(xs, display_sweep, autoDownsample=True)
if fixed_ylim is None: if fixed_ylim is None:
y0 = float(np.nanmin(current_sweep)) y0 = float(np.nanmin(display_sweep))
y1 = float(np.nanmax(current_sweep)) y1 = float(np.nanmax(display_sweep))
if np.isfinite(y0) and np.isfinite(y1): if np.isfinite(y0) and np.isfinite(y1):
margin = 0.05 * max(1.0, (y1 - y0)) margin = 0.05 * max(1.0, (y1 - y0))
p_line.setYRange(y0 - margin, y1 + margin, padding=0) p_line.setYRange(y0 - margin, y1 + margin, padding=0)
# Обновим спектр и фазу # Обновим спектр и фазу
take_fft = min(int(current_sweep.size), FFT_LEN) take_fft = min(int(display_sweep.size), FFT_LEN)
if take_fft > 0 and freq_shared is not None: if take_fft > 0 and freq_shared is not None:
# Создаем буфер для полного FFT (с отрицательными частотами) # Создаем буфер для полного FFT (с отрицательными частотами)
fft_in = np.zeros((FFT_LEN,), dtype=np.float32) fft_in = np.zeros((FFT_LEN,), dtype=np.float32)
@ -365,7 +513,7 @@ def run_pyqtgraph(args):
data_points = min(data_points, take_fft, FFT_LEN - start_idx) data_points = min(data_points, take_fft, FFT_LEN - start_idx)
# Подготовка данных с окном Хэннинга # Подготовка данных с окном Хэннинга
seg = np.nan_to_num(current_sweep[:data_points], nan=0.0).astype(np.float32, copy=False) seg = np.nan_to_num(display_sweep[:data_points], nan=0.0).astype(np.float32, copy=False)
win = np.hanning(data_points).astype(np.float32) win = np.hanning(data_points).astype(np.float32)
# Размещаем данные в правильной позиции # Размещаем данные в правильной позиции