reference
This commit is contained in:
@ -2,9 +2,11 @@
|
||||
Визуализация данных с использованием pyqtgraph (быстрый бэкенд).
|
||||
"""
|
||||
|
||||
import csv
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime
|
||||
from queue import Empty, Queue
|
||||
from typing import Optional, Tuple
|
||||
|
||||
@ -13,11 +15,13 @@ import numpy as np
|
||||
try:
|
||||
import pyqtgraph as pg
|
||||
from PyQt5 import QtCore, QtWidgets # noqa: F401
|
||||
from PyQt5.QtWidgets import QPushButton, QWidget, QHBoxLayout, QCheckBox, QFileDialog
|
||||
except Exception:
|
||||
# Возможно установлена PySide6
|
||||
try:
|
||||
import pyqtgraph as pg
|
||||
from PySide6 import QtCore, QtWidgets # noqa: F401
|
||||
from PySide6.QtWidgets import QPushButton, QWidget, QHBoxLayout, QCheckBox, QFileDialog
|
||||
except Exception as e:
|
||||
raise RuntimeError(
|
||||
"pyqtgraph/PyQt5(Pyside6) не найдены. Установите: pip install pyqtgraph PyQt5"
|
||||
@ -138,6 +142,131 @@ def run_pyqtgraph(args):
|
||||
status = pg.LabelItem(justify="left")
|
||||
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
|
||||
head = 0
|
||||
@ -145,6 +274,9 @@ def run_pyqtgraph(args):
|
||||
x_shared: Optional[np.ndarray] = None
|
||||
current_sweep: Optional[np.ndarray] = None
|
||||
current_info: Optional[SweepInfo] = None
|
||||
# Медианные данные для вычитания
|
||||
median_data: Optional[np.ndarray] = None
|
||||
median_subtract_enabled = False
|
||||
# Авто-уровни цветовой шкалы водопада сырых данных пересчитываются по видимой области.
|
||||
# Для спектров (полное FFT для отрицательных частот)
|
||||
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
|
||||
if s is None or s.size == 0 or ring is None:
|
||||
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]
|
||||
row = np.full((w,), np.nan, dtype=np.float32)
|
||||
take = min(w, s.size)
|
||||
@ -336,20 +477,27 @@ def run_pyqtgraph(args):
|
||||
def update():
|
||||
changed = drain_queue() > 0
|
||||
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:
|
||||
xs = np.arange(current_sweep.size)
|
||||
curve.setData(xs, current_sweep, autoDownsample=True)
|
||||
xs = np.arange(display_sweep.size)
|
||||
curve.setData(xs, display_sweep, autoDownsample=True)
|
||||
if fixed_ylim is None:
|
||||
y0 = float(np.nanmin(current_sweep))
|
||||
y1 = float(np.nanmax(current_sweep))
|
||||
y0 = float(np.nanmin(display_sweep))
|
||||
y1 = float(np.nanmax(display_sweep))
|
||||
if np.isfinite(y0) and np.isfinite(y1):
|
||||
margin = 0.05 * max(1.0, (y1 - y0))
|
||||
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:
|
||||
# Создаем буфер для полного FFT (с отрицательными частотами)
|
||||
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)
|
||||
|
||||
# Подготовка данных с окном Хэннинга
|
||||
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)
|
||||
|
||||
# Размещаем данные в правильной позиции
|
||||
|
||||
Reference in New Issue
Block a user