javascript

Python 2D графика. Визуализация данных в реальном времени. Matplotlib, PyQTGraph, pyOpenGL, VisPy, …

  • вторник, 4 февраля 2025 г. в 00:00:11
https://habr.com/ru/articles/878002/

На мой взгляд, быстрое преобразование больших массивов цифровых данных в наглядные и доступные для восприятия человеком формы приобрело большую востребованность. Даже двумерные графики, отображаемые на экранах мониторов, все еще продолжают сохранять свою актуальность и популярность в таких разнообразных сферах, как торговля ценными бумагами, технические и научные измерения (осциллограммы) и исследования, а также в узких областях, таких как компьютерные студии звукозаписи (БПФ, эквализация, тюнеры). Контекст данной статьи - это цифровая обработка звука.

Для 8 графических пакетов в статье приведены 8 максимально коротких и простых специфичных для каждого пакета кода на python, отображающий на экране с максимально возможным FPS для данного пакета график sin()+noise.

Пример 2D графика в акустических исследованиях
Пример 2D графика в акустических исследованиях

Получение высокого FPS для 2D графики на Python

При разработке 2D графики на Python лимитировать может производительность, особенно если вы стремитесь к высокому количеству кадров в секунду (FPS). В этой статье рассмотрим несколько наиболее популярных графических библиотек, их производительность и возможности достижения высоких значений FPS.

Предварительные данные по информации из интернет источников

  1. Mayavi 3D: Хотя Mayavi в первую очередь предназначен для 3D-визуализации, он может использоваться для 2D-графиков. Однако его производительность для 2D задач может быть ниже, чем у специализированных библиотек.

  2. PyVista: Эта библиотека также ориентирована на 3D, но может использоваться для 2D. PyVista предлагает хорошую производительность, но для 2D задач может быть избыточной.

  3. Matplotlib: Широко используемая библиотека для построения графиков, но не оптимизирована для высоких FPS. Обычно Matplotlib работает на уровне 10-30 FPS, что может быть недостаточно для динамичных приложений.

  4. PyQTGraph: Эта библиотека специально разработана для быстрого отображения графиков и может достигать 50 FPS и выше. PyQTGraph использует OpenGL для рендеринга, что значительно увеличивает производительность.

  5. Plotly: Отлично подходит для интерактивных графиков, но его производительность может быть ограничена при большом количестве данных. FPS обычно ниже 50.

  6. PyGame: Одна из самых популярных библиотек для создания игр на Python. PyGame может достигать 60 FPS и выше, если правильно настроить рендеринг и управление событиями.

  7. Arcade: Современная библиотека для создания 2D-игр, которая также может достигать 60 FPS и выше. Arcade использует OpenGL и предлагает простые инструменты для работы с графикой.

  8. pyOpenGL: Это обертка для OpenGL, которая позволяет создавать высокопроизводительные графические приложения. С правильной оптимизацией можно достичь FPS в 100 и выше.

  9. VisPy: Библиотека для визуализации данных, использующая OpenGL. VisPy может достигать высоких значений FPS, особенно при работе с большими объемами данных.

  10. Bokeh: Хотя Bokeh в первую очередь предназначен для веб-визуализации, его производительность для 2D графиков может быть ограничена, и FPS обычно 10..50.

Тестирование на скорость рисования 2D графиков

Для тестирования производительности различных библиотек использовались простые сценарии, которые рисуют sin() + noise() на экране и измеряют FPS. Важно учитывать, что производительность может зависеть от аппаратного обеспечения и настроек системы.

Достижение частоты кадров (FPS) > 30 кадров в секунду вполне осуществимо с использованием популярных библиотек. Однако для достижения FPS >= 60 потребуется обращение к низкоуровневым библиотекам, а также тщательная оптимизация кода.

Важно отметить, что включение вертикальной синхронизации (VSync=On) не всегда доступно, поскольку это зависит от конкретной видеокарты, драйверов и мониторов, включая современные 4K телевизоры. Даже если VSync доступна, не все значения частоты обновления могут быть выбраны произвольно, и не всегда они будут корректно обрабатывать сигналы VSync в графических пакетах. Например, синхронизация может быть доступна на 30, 50 Гц, но не на 60 Гц или 44 Гц и так далее.

Тестирование

MatplotLib

Matplotlib
Matplotlib
import matplotlib.pyplot as plt
import numpy as np
from PyQt5 import QtWidgets
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
import time

class MyApp(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        self.figure, self.ax = plt.subplots()
        self.canvas = FigureCanvas(self.figure)
        self.setCentralWidget(self.canvas)
        self.x = np.linspace(0, 17, 735)
        self.y = np.sin(self.x)
        self.line, = self.ax.plot(self.x, self.y)

        # Настройка координатной сетки
        self.ax.grid(True, linestyle='--', linewidth=0.5, color='gray', alpha=0.7)  # Серая пунктирная сетка

        self.timer = self.startTimer(16)  # ~60 FPS
        self.frame_count = 0
        self.start_time = time.time()

    def timerEvent(self, event):
        # Генерация шума с равномерным распределением и амплитудой 20% от амплитуды синуса
        noise_amplitude = 0.2 * np.max(np.abs(self.y))  # 20% от амплитуды синуса
        noise = noise_amplitude * np.random.uniform(-1, 1, len(self.x))  # Равномерно распределенный шум
     #  noise = noise_amplitude * np.random.normal(0, 1, len(self.x))  # Нормальный шум

        # Обновление данных графика (синус + шум)
        self.y = np.sin(self.x + 0.1 * event.timerId()) + noise
        self.line.set_ydata(self.y)
        self.canvas.draw()

        # Подсчет FPS
        self.frame_count += 1
        elapsed_time = time.time() - self.start_time
        if elapsed_time > 1:  # Обновляем FPS каждую секунду
            fps = self.frame_count / elapsed_time
            #print(f"FPS: {fps:.2f}")
            # Устанавливаем заголовок с увеличенным, жирным и зеленым шрифтом
            self.ax.set_title(f"Matplotlib FPS: {fps:.2f}", fontsize=16, fontweight='bold', color='green')
            self.canvas.draw()  # Перерисовываем график с новым заголовком
            self.frame_count = 0
            self.start_time = time.time()

app = QtWidgets.QApplication([])
window = MyApp()
window.show()
app.exec_()

Bokeh

Bokeh
Bokeh
import numpy as np
from bokeh.plotting import figure, show
from bokeh.io import output_notebook
from bokeh.models import Button, CustomJS, ColumnDataSource, Div
from bokeh.layouts import column

# Включаем вывод графиков в Jupyter Notebook (если вы используете его)
output_notebook()

# Генерация данных
def generate_data():
    x = np.linspace(0, 10, 200)  # 200 точек от 0 до 10
    y = np.sin(x)  # Значения синуса
    noise_amplitude = 0.2 * np.abs(y)  # Амплитуда шума (20% от амплитуды синуса)
    noise = np.random.normal(0, noise_amplitude)  # Нормально распределенный шум
    y_noisy = y + noise  # Добавление шума к значениям синуса
    return x, y, y_noisy

# Создание графика с изменёнными размерами
p = figure(title="Синус с нормально распределенным шумом", x_axis_label='X', y_axis_label='Y', 
           width=800, height=400)  # Увеличение ширины в 1.5 раза и уменьшение высоты в 1.5 раза

# Изначальные данные
x, y, y_noisy = generate_data()

# Создание источника данных
source_noisy = ColumnDataSource(data=dict(x=x, y=y_noisy))
source_original = ColumnDataSource(data=dict(x=x, y=y))

# Добавление линий на график с использованием источников данных
line_noisy = p.line('x', 'y', source=source_noisy, line_width=2, color="navy", alpha=0.7, legend_label="Синус + шум")
line_original = p.line('x', 'y', source=source_original, line_width=2, color="orange", alpha=0.5, legend_label="Синус")

# Создание Div для отображения FPS с изменённым стилем
fps_div = Div(text="FPS: 0", width=100, styles={'font-size': '20px', 'color': 'red', 'font-weight': 'bold'})

# Функция обновления графика и FPS
callback = CustomJS(args=dict(source_noisy=source_noisy, source_original=source_original, fps_div=fps_div), code="""
    let fpsCounter = 0;
    let lastTime = Date.now();

    setInterval(() => {
        const currentTime = Date.now();
        fpsCounter++;

        // Обновление данных графика
        const x = [];
        const y_noisy = [];
        const y_original = [];

        for (let i = 0; i < 200; i++) {
            x.push(i * 0.05);
            const sinValue = Math.sin(x[i]);
            const noiseAmplitude = 0.2 * Math.abs(sinValue);
            const noise = Math.random() * noiseAmplitude * 2 - noiseAmplitude;
            y_noisy.push(sinValue + noise);
            y_original.push(sinValue);
        }

        source_noisy.data['x'] = x;
        source_noisy.data['y'] = y_noisy;
        source_original.data['x'] = x;
        source_original.data['y'] = y_original;

        // Обновление FPS в Div
        if (currentTime - lastTime >= 1000) {
            fps_div.text = `FPS: ${fpsCounter}`;
            fpsCounter = 0;
            lastTime = currentTime;
        }

        source_noisy.change.emit();
        //source_original.change.emit();
    }, 10); // Обновление каждые ~66.67 мс (15 раз в секунду)
""")

# Кнопка обновления
button = Button(label="Обновить")
button.js_on_click(callback)

# Отображение графика и кнопки с FPS Div
layout = column(button, fps_div, p)
show(layout)

OpenGL

import sys
import ctypes
import random
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import QTimer, Qt
from OpenGL.GL import *
import math

# Глобальная переменная для хранения данных сигнала
line_vertices = []
num_points = 1000

# Первоначальная генерация данных
for i in range(num_points):
    t = i / (num_points - 1)
    x = -math.pi + 2 * math.pi * t
    y = 0.1 * math.sin(x * 18)
    line_vertices.extend([x, y])

def generate_sine_wave(num_points=1000):
    global line_vertices
    line_vertices = []
    for i in range(num_points):
        t = i / (num_points - 1)
        x = -math.pi + 2 * math.pi * t
        base_signal = 0.3 * math.sin(x * 8)
        noise = random.uniform(-0.2, 0.2)
        y = base_signal + noise
        line_vertices.extend([x, y])

class OpenGLWidget(QOpenGLWidget):
    def __init__(self, parent=None):
        super(OpenGLWidget, self).__init__(parent)
        self.line_vao = None
        self.line_vbo = None
        self.line_program = None

    def initializeGL(self):
        self.line_program = QOpenGLShaderProgram()
        
        line_vertex_shader_source = """
        #version 330 core
        layout(location = 0) in vec2 position;
        void main() {
            gl_Position = vec4(position, 0.0, 1.0);
        }
        """
        
        line_fragment_shader_source = """
        #version 330 core
        out vec4 fragColor;
        void main() {
            fragColor = vec4(1.0, 1.0, 1.0, 1.0);
        }
        """
        
        self.line_program.addShaderFromSourceCode(QOpenGLShader.Vertex, line_vertex_shader_source)
        self.line_program.addShaderFromSourceCode(QOpenGLShader.Fragment, line_fragment_shader_source)
        self.line_program.link()
        
        self.line_vao = QOpenGLVertexArrayObject()
        self.line_vao.create()
        self.line_vbo = QOpenGLBuffer(QOpenGLBuffer.VertexBuffer)
        self.line_vbo.create()
        
        self.update_buffer()
        glClearColor(0.0, 0.0, 1.0, 1.0)

    def update_buffer(self):
        self.line_vao.bind()
        self.line_vbo.bind()
        
        line_data = (ctypes.c_float * len(line_vertices))(*line_vertices)
        self.line_vbo.allocate(line_data, len(line_vertices) * ctypes.sizeof(ctypes.c_float))
        
        self.line_program.bind()
        self.line_program.setAttributeBuffer(0, GL_FLOAT, 0, 2)
        self.line_program.enableAttributeArray(0)
        
        self.line_vbo.release()
        self.line_vao.release()

    def resizeGL(self, width, height):
        glViewport(0, 0, width, height)

    def paintGL(self):
        glClear(GL_COLOR_BUFFER_BIT)
        self.line_program.bind()
        self.line_vao.bind()
        glDrawArrays(GL_LINE_STRIP, 0, len(line_vertices) // 2)
        self.line_vao.release()
        self.line_program.release()

class MainWindow(QMainWindow):
    def __init__(self):
        super(MainWindow, self).__init__()
        
        self.opengl_widget = OpenGLWidget()
        self.update_button = QPushButton("Update Graph")
        self.timer_button = QPushButton("СТАРТ/СТОП ТАЙМЕР")
        
        # FPS display
        self.fps_label = QLabel("FPS: 0")
        self.fps_label.setAlignment(Qt.AlignCenter)
        self.fps_label.setFixedHeight(50)  # Фиксированная высота
        self.fps_label.setStyleSheet("""
            QLabel {
                color: red;
                font-weight: bold;
                font-size: 20px;
            }
        """)
        
        # Таймеры
        self.render_timer = QTimer()
        self.render_timer.setInterval(5)  # 10 FPS (100 ms)
        self.fps_timer = QTimer()
        
        # Счетчики
        self.frame_count = 0
        self.current_fps = 0

        # Настройка соединений
        self.update_button.clicked.connect(self.update_graph)
        self.timer_button.clicked.connect(self.toggle_timer)
        self.render_timer.timeout.connect(self.update_graph)
        self.fps_timer.timeout.connect(self.update_fps)
        
        # Настройка интерфейса
        layout = QVBoxLayout()
        layout.addWidget(self.opengl_widget)
        layout.addWidget(self.update_button)
        layout.addWidget(self.timer_button)
        layout.addWidget(self.fps_label)
        
        container = QWidget()
        container.setLayout(layout)
        self.setCentralWidget(container)
        
        self.setFixedSize(800, 600)
        self.setWindowTitle("Noisy Sine Wave Generator with FPS")
        
        # Запуск FPS таймера
        self.fps_timer.start(1000)  # Обновление FPS каждую секунду

    def update_graph(self):
        generate_sine_wave()
        self.opengl_widget.update_buffer()
        self.opengl_widget.update()
        self.frame_count += 1  # Увеличиваем счетчик кадров

    def toggle_timer(self):
        if self.render_timer.isActive():
            self.render_timer.stop()
            self.timer_button.setText("СТАРТ ТАЙМЕР")
        else:
            self.render_timer.start()
            self.timer_button.setText("СТОП ТАЙМЕР")

    def update_fps(self):
        self.current_fps = self.frame_count
        self.frame_count = 0  # Сбрасываем счетчик
        self.fps_label.setText(f"FPS: {self.current_fps}")

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    app.exec()

VisPy

%gui qt
import vispy
#vispy.use(app="pyqt5")
import numpy as np
from vispy import app, scene
from vispy.visuals.filters import ShadingFilter

# Создаем холст и view
canvas = scene.SceneCanvas(keys='interactive', size=(800, 600), show=True)
view = canvas.central_widget.add_view()

# Настройка камеры
view.camera = 'panzoom'
view.camera.set_range(x=(-10, 10), y=(-10, 10))

# Создаем линию с начальными точками
num_points = 200
line_data = np.zeros((num_points, 3), dtype=np.float32)
line = scene.Line(line_data, color='cyan', width=3, parent=view.scene)

# Добавляем эффекты
#shading = ShadingFilter(shading='smooth')
#line.attach(shading)

# Параметры анимации
angle = 0.0
speed = 0.05
radius = 8.0

async def main():
    global angle, line_data
    
    while True:
        # Обновляем позиции точек по спирали
        angle += speed
        t = np.linspace(0, 4 * np.pi, num_points)
        x = radius * np.cos(t + angle) * np.cos(0.5 * angle)
        y = radius * np.sin(t + angle) * np.sin(0.5 * angle)
        
        # Обновляем данные линии
        line_data[:, 0] = x
        line_data[:, 1] = y
        line.set_data(line_data)
        
        # Ожидаем следующий кадр
        await asyncio.sleep(1/60)
        canvas.update()

if __name__ == '__main__':
    import asyncio
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())
    app.run()

PyGame

import pygame
from pygame.locals import OPENGL
import numpy as np
import time

# Инициализация pygame
pygame.init()

# Параметры окна
WIDTH, HEIGHT = 800, 600
FPS = 60

# Цвета
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
GRAY = (200, 200, 200)
GREEN = (0, 255, 0)
RED = (255, 0, 0)

# Настройка шрифта
font = pygame.font.Font(None, 36)
frame_count = 1
elapsed_time = time.time()
fps = 1
fps_text = font.render(f"Pygame FPS: {fps:.2f}", True, RED)
start_time = time.time()

# Создание окна
screen = pygame.display.set_mode((WIDTH, HEIGHT))
#screen = pygame.display.set_mode((800, 600), pygame.RESIZABLE, vsync=1)

pygame.display.set_caption("Pygame Sin Wave with Noise")

# Параметры графика
x = np.linspace(0, 17, 735)
y = np.sin(x)
noise_amplitude = 0.2 * np.max(np.abs(y))

# Функция для отрисовки сетки
def draw_grid():
    for i in range(0, WIDTH, 50):
        pygame.draw.line(screen, GRAY, (i, 0), (i, HEIGHT), 1)
    for j in range(0, HEIGHT, 50):
        pygame.draw.line(screen, GRAY, (0, j), (WIDTH, j), 1)

# Основной цикл
clock = pygame.time.Clock()
frame_count = 0
start_time = time.time()

running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

    # Генерация шума
    noise = noise_amplitude * np.random.uniform(-1, 1, len(x))
    y = np.sin(x + 0.1 * frame_count) + noise

    # Очистка экрана
    screen.fill(WHITE)

    # Отрисовка сетки
    draw_grid()

    # Отрисовка графика
    points = [(int((x[i] / 17) * WIDTH), int((y[i]*0.7 + 1) * HEIGHT / 2)) for i in range(len(x))]
    pygame.draw.lines(screen, BLACK, False, points, 2)

    # Подсчет FPS
    frame_count += 1
    elapsed_time = time.time() - start_time
    if elapsed_time > 1:
        fps = frame_count / elapsed_time
        fps_text = font.render(f"Pygame FPS: {fps:.2f}", True, RED)
        #screen.blit(fps_text, (10, 10))
        frame_count = 0
        start_time = time.time()
    
    screen.blit(fps_text, (10, 10))
    # Обновление экрана
    pygame.display.flip()
    clock.tick(FPS)

pygame.quit()

code

Arcade

import arcade
import math
import random

# Constants
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600
SCREEN_TITLE = "Sin with Random Noise"
AMPLITUDE = 0.2  # Amplitude of the random noise

class MyGame(arcade.Window):
    def __init__(self, width, height, title):
        super().__init__(width, height, title, vsync=True)  #будет 30 или 50 кадров  60 не поддерживает fps становится 350
        
        # Set the background color to white
        arcade.set_background_color(arcade.color.WHITE)
        
        # Generate initial points for the sin() function with random noise
        self.point_list = []
        self.base_sin_points = []
        self.generate_sin_with_noise()

        # Schedule update function to be called every frame
        arcade.schedule(self.update, 1/60)  # Update at 60 FPS ##########################################################
        
        # Initialize variables for FPS calculation and display
        self.frame_count = 0
        self.fps = 0

        # Create a text object for displaying FPS
        self.fps_text = arcade.Text(
            f"FPS: {self.fps}",
            x=10,
            y=10,
            color=arcade.color.RED,
            font_size=24,
            anchor_x="left",
            anchor_y="baseline"
        )

        # Schedule FPS calculation function to be called every second ####################################################
        arcade.schedule(self.calculate_fps, 1)

    def generate_sin_with_noise(self):
        self.point_list = []
        self.base_sin_points = []
        for x in range(SCREEN_WIDTH):
            base_y = SCREEN_HEIGHT // 2 + 100 * math.sin(2 * math.pi * x / SCREEN_WIDTH)  # Base sin function
            self.base_sin_points.append((x, base_y))
            y = base_y + random.uniform(-AMPLITUDE, AMPLITUDE) * 100  # Add random noise
            self.point_list.append((x, y))

    def on_draw(self):
        self.clear()
        
        # Draw the sin() function with random noise using lines
        arcade.draw_line_strip(self.point_list, arcade.color.BLUE, 1)

        # Display FPS
        self.fps_text.draw()

    def update(self, delta_time: float):
        # Increment frame count
        self.frame_count += 1

        # Regenerate the points with new random noise every frame (60 times per second)
        for i in range(len(self.base_sin_points)):
            base_x, base_y = self.base_sin_points[i]
            y = base_y + random.uniform(-AMPLITUDE, AMPLITUDE) * 100  # Add new random noise
            self.point_list[i] = (base_x, y)

    def calculate_fps(self, delta_time: float):
        # Calculate FPS
        self.fps = self.frame_count

        # Reset frame count
        self.frame_count = 0

        # Update the FPS text
        self.fps_text.text = f"FPS: {self.fps}"

    def on_key_press(self, symbol, modifiers):
        if symbol == arcade.key.S:
            # Regenerate the points with new random noise
            for i in range(len(self.base_sin_points)):
                base_x, base_y = self.base_sin_points[i]
                y = base_y + random.uniform(-AMPLITUDE, AMPLITUDE) * 100  # Add new random noise
                self.point_list[i] = (base_x, y)
            self.on_draw()  # Redraw the updated graph

# Open the window
window = MyGame(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)

# Keep the window open until the user closes it
arcade.run()

Plotly

FPS

Особенности

Matplotlib

11.8

возможности 8/10, 2D, 3D

Bokeh

33

JS, 2D, возможности 7/10

PyOpenGL

50

низкоуровневый

VisPy

93

возможности 3/10, 2D. 3D

PyGame

60 (VSync=On)

возможности 3/10, 2D

Arcade

98

возможности 3/10, 2D

Potly

37

JS, возможности 7/10

PyQTGraph

70

возможности 6/10

MayAvi / VTK

90

возможности3/10(2D)3D(9/10)

PyVista

60 (JS, Jupyter/Anaconda)

нестабильный Trame backend

PyQTGraph

import pyqtgraph as pg
import numpy as np

app = pg.mkQApp()

main_widget = pg.QtWidgets.QWidget()
layout = pg.QtWidgets.QVBoxLayout(main_widget)
layout.setContentsMargins(0, 0, 0, 0)

fps_label = pg.QtWidgets.QLabel("FPS: 0.0")
fps_label.setStyleSheet("font: bold 20px; color: black; padding: 5px;")
layout.addWidget(fps_label, stretch=0)

plot = pg.PlotWidget()
layout.addWidget(plot, stretch=1)

# Настройки графика
plot.showGrid(x=True, y=True, alpha=0.3)  # Светло-серая сетка (alpha=0.3)
plot.getAxis('bottom').setPen(pg.mkPen(color='#888'))  # Серые оси
plot.getAxis('left').setPen(pg.mkPen(color='#888'))    # Серые оси

x = np.linspace(0, 10, 1000)
y = np.sin(x) + np.random.uniform(-0.2, 0.2, 1000)
curve = plot.plot(x, y, pen=pg.mkPen('g', width=4))  # Зеленый, толщина 4px

last_time = pg.QtCore.QTime.currentTime()
fps_average = 0

def update():
    global last_time, fps_average
    curve.setData(y=np.sin(x) + np.random.uniform(-0.2, 0.2, 1000))
    
    current_time = pg.QtCore.QTime.currentTime()
    elapsed = last_time.msecsTo(current_time)
    fps = 1000 / elapsed if elapsed > 0 else 0
    fps_average = 0.99 * fps_average + 0.01 * fps
    fps_label.setText(f"FPS: {fps_average:.1f}")
    last_time = current_time

timer = pg.QtCore.QTimer(); timer.timeout.connect(update); timer.start(10)
main_widget.show()
app.exec()

MayAvi / VTK (2D 3D)

import numpy as np
from mayavi import mlab
import time

# Создаем фигуру и оси
fig = mlab.figure(size=(800, 600), bgcolor=(1, 1, 1))

# Создаем данные для синусоидальной функции с шумом
x = np.linspace(0, 2 * np.pi, 100)
y = np.sin(x) + np.random.normal(0, 0.1, x.shape)
z = np.zeros_like(x)
fps1=1
# Создаем линию
line = mlab.plot3d(x, y, z, color=(1, 0, 0), tube_radius=0.01*3)

# Добавляем текст для отображения FPS
fps_text = mlab.text(0.05, 0.05, "FPS: 0", width=0.2, color=(0, 0, 0))

# Функция для обновления FPS
def update_fps():
    global fps1
    current_time = time.time()
    if hasattr(update_fps, "last_time"):
        delta_time = current_time - update_fps.last_time
        if delta_time > 0:
           fps = 1 / delta_time
        else:
           fps=fps1
        fps1 = 0.99*fps1 + 0.01*fps
        fps_text.set(text=f"FPS: {int(fps1)}")
    update_fps.last_time = current_time

# Функция для обновления данных линии
@mlab.animate(delay=10)
def update_line():
    while True:
        # Обновляем данные линии с новым шумом
        noise = np.random.normal(0, 0.1, x.shape)
        y_new = np.sin(x) + noise
        line.mlab_source.set(y=y_new)
        update_fps()
        yield

# Запускаем анимацию
update_line()

# Запускаем визуализацию
mlab.show()

Железо

Hardware
Hardware

Выводы

Выбор библиотеки для 2D графики на Python зависит от ваших требований к производительности и функциональности. Matplotlib медленная. Bokeh и Plotly под капотом имеют JavaScript и богатую инфраструктуру для отрисовки вспомогательных элементов графика (сетка, оси, легенды, шрифты, типы графиков, GUI элементы). Но придется решать задачу передачи данных из контекста python в контекст JavaScript. Для достижения высоких значений FPS разумно использовать подходящие инструменты (MayAvi, PyGame, VisPy). Компромиссное решение - PyQTGraph. С правильным подходом можно достичь впечатляющих результатов в визуализации, высокий FPS и хорошей интерактивности.

P,S, код проверен на Python 3.9 и 3.12, Jupyter Notebook, Anaconda, Windows 11. В случае необходимости есть файл enviroment.yaml. Он позволяет установить все зависимости за несколько минут.

P.P.S все файлы к этой статье в моей "телеге"

Похоже на Mac Intell OS 10.* python 3.12 сможет отображать осциллограммы звукового сигнала в реальном времени.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Тесты
25% протеcтируй PyVista / Trame1
100% протестируй на Mac Intel / Apple Silicon4
Проголосовали 4 пользователя. Воздержались 2 пользователя.