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

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

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

Пример 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
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 сможет отображать осциллограммы звукового сигнала в реальном времени.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Тесты
100% протеcтируй PyVista / Trame1
100% протестируй на Mac Intel / Apple Silicon1
Проголосовал 1 пользователь. Воздержавшихся нет.
Источник: https://habr.com/ru/articles/878002/


Интересные статьи

Интересные статьи

Привет, Хабр! Меня зовут Сергей Бевзенко, я ведущий разработчик Delivery Club в команде Discovery. Наша команда занимается навигацией пользователя по приложению Delivery Club: мы отве...
Всем привет! Не так давно на работе в рамках тестирования нового бизнес-процесса мне понадобилась возможность авторизации под разными пользователями. Переход в соответствующий р...
В этой статье мы рассмотрим, как система управления 1С-Битрикс справляется с большими нагрузками. Данный вопрос особенно актуален сегодня, когда электронная торговля начинает конкурировать по обороту ...
Метод изучения различных дисциплин «по карточкам», который еще называют системой Лейтнера, известен уже около 40 лет. Несмотря на то, что карточки чаще всего он применяются, чтобы пополнить лекси...
Если честно, к Д7 у меня несколько неоднозначное отношение. В некоторых местах я попискиваю от восторга, а в некоторых хочется топать ногами и ругаться неприличными словами.