reference
This commit is contained in:
@ -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)
|
||||||
|
|
||||||
# Размещаем данные в правильной позиции
|
# Размещаем данные в правильной позиции
|
||||||
|
|||||||
Reference in New Issue
Block a user