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

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

Комментарии (14)


  1. Oksenija Автор
    03.02.2025 08:25

    К сожалению не удалось качественно протестировать неплохо выглядящую на первый взгляд PyVista. Ее последние версии перешли на безусловный бэкэнд Trame который при тестировании заработал только на 1 из 4-х колмпьютеров. Надо будет видимо откатиться в прошлое на несколько лет и разбираться с зависимостями для старых версий PyVista когда бэкендом у нее был Panel. И на 2-х разных Mac Intel и M1 также версия с Trame не заработала. Но одно из интересных свойств PyVista что она может экспортировать ИНТЕРАКТИВНЫЙ график с элементами управления GUI в автономный работающий off-line html/javascript Vue.js с поддержкой GUI компонентов (кнопки, слайдеры и т.д.).


    1. N-Cube
      03.02.2025 08:25

      PyVista это обертка для библиотеки VTK, которую можно использовать многими способами. И с работоспособностью PyVista проблем нет - у меня много примеров спутниковой интерферометрии доступны на гугл колаб и в докер образе с интерактивной 3д визуализацией в jupyter notebook. После последнего большого обновления гугл колаб я обсуждал с разработчиками PyVista, почему на колабе перестало работать и как починить, можете найти обсуждение, там много интересного предлагалось, в итоге, я нашел работающий вариант и с тех пор не менял его. Смотрите примеры на Google Colab на https://insar.dev, здесь же ссылки на гитхаб и докерхаб.


  1. zzzzzzerg
    03.02.2025 08:25

    Статья какая-то бессмысленная и беспощадная. Нет единой методологии тестирования, в скриптах используются различные входные данные, тестируются на каких-то детских объемах точек.

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

    Не понятно, кто кого может лимитировать.

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

    Если уж и писать про возможности - то писать о подходах, которые на данных библиотеках позволяют достигать высоких значений. От громадных простыней кода пользы чуть меньше, чем никакой.

    Ну и, конечно, ссылка на телеграм канал.


    1. Oksenija Автор
      03.02.2025 08:25

      отвечу на замечания:

      Данные примерно одни и теже, это sin() + шум

      методика проста - вывод sin()+шум в виде графика с максимальным fps и измерение этого fps

      объем точек соответствует задаче вывода графика звукового сигнала в реальном времени. При 60 гц fps 44100 частоте дискртеизации это 735 точек при меньшей частоте дискретизации будет меньшее кол-во точек, так что с кол-вом точек все Ок.

      Что может лимитировть действительно никому не понятно, например 12 fps в matplotlib самой совершенной и мошной ситеме, ведь ее писали одни из лучших программистов в мире и вот 12 fps Ну как так? Может вы нам объясните?

      Весь код примерно по 100 строк и меньше для каждого пакета. Думаю это для даже среднего программиста не является огромной простыней. Кроме того спойлер нельзя вставить внутрь тега код. Потому пришлось полностью. но это вопрос не ко мне.


      1. zzzzzzerg
        03.02.2025 08:25

        x = np.linspace(0, 10, 1000)
        x = np.linspace(0, 2 * np.pi, 100)

        Согласитесь, это немного разные количества.

        методика проста - вывод sin()+шум в виде графика с максимальным fps и измерение этого fps

        объем точек соответствует задаче вывода графика звукового сигнала в реальном времени. При 60 гц fps 44100 частоте дискртеизации это 735 точек при меньшей частоте дискретизации будет меньшее кол-во точек, так что с кол-вом точек все Ок.

        Ну так напишите в самом начале о своей методике. В моей практике ограничивающим фактором для выбора библиотек для построения графиков были сотни тысяч точек. И там уже важными становятся другие возможности.

        Что может лимитировть действительно никому не понятно, например 12 fps в matplotlib самой совершенной и мошной ситеме, ведь ее писали одни из лучших программистов в мире и вот 12 fps Ну как так? Может вы нам объясните?

        Наверное, все таки стоит поправить свое изначальное предложение. Было:

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

        Стало (например)

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

        Вроде как можно
        import numpy as np
        from mayavi import mlab
        import time
        
        # Создаем фигуру и оси
        fig = mlab.figure(size=(800, 600), bgcolor=(1, 1, 1))
        


        1. Oksenija Автор
          03.02.2025 08:25

          данном контексте 100 и 1000 мало отличаются так как размер поля вывода примерно 800..1000 по оси Х и все равно будет произведена интерполяция или децимация до данного размера т.е. и 100 и 1000 все равно превратятся в 800 так как будет выведено все равно 800 по оси Х без разрывов, конечно при не const сигнале точек будет больше. Но принципиально это не повлияет на быстродействие по крайней мере при использовании указанных пакетов и кодов в статье. По крайней мере на сигналах похожих на реальные типа sin()+небольшой шум измерения fps показали незначительное отличие. Вероятно если использовать белый шум с постоянным спектром большой амплитуды это приведет к закрашиванию вертикальными линиями всего поля вывода и вероятно приведет к падению fps но на сигналах сильно похожих на обычные для звуковых смеси синус и небольшого уровня шумов падение fps незначительное и изучать данное явление возможно если это интересно будет читателям я буду в другой статье...


        1. Oksenija Автор
          03.02.2025 08:25

          на счет спойлера внутри кода - научите плиз как вы это сделали - у меня в редакторе хабра внутри тега код просто нет кнопочки для вставки другого тега


          1. zzzzzzerg
            03.02.2025 08:25

            Скрытый текст
            test

            В самой статье должно быть также.


            1. pulsework
              03.02.2025 08:25

              да ладно и так норм


      1. Oksenija Автор
        03.02.2025 08:25

        добавлю, что весь код в одном файле Jupyter Notebook + файлы enviroment.yaml для установки через Anaconda-Navigator можно скачать из моей телеги


  1. Oksenija Автор
    03.02.2025 08:25

    попробую разъяснить, кажется предыдущий обсуждающий не понял, что в статье нет единой одной длинной простыни кода. Для каждого тестируемого пакета только код генерации sin()+шум ПРИМЕРНО совпадает, а вся ОБВЯЗКА специфична для каждого пакета и таким образом вместо простыни имеет несколько отдельных самостоятельных программок по ПРИМЕРНО 100 строк. Вероятно возможно написать универсальных код работающий сразу на всех пакетах, но такой задачи не ставилось.