На мой взгляд, быстрое преобразование больших массивов цифровых данных в наглядные и доступные для восприятия человеком формы приобрело большую востребованность. Даже двумерные графики, отображаемые на экранах мониторов, все еще продолжают сохранять свою актуальность и популярность в таких разнообразных сферах, как торговля ценными бумагами, технические и научные измерения (осциллограммы) и исследования, а также в узких областях, таких как компьютерные студии звукозаписи (БПФ, эквализация, тюнеры). Контекст данной статьи - это цифровая обработка звука.
Для 8 графических пакетов в статье приведены 8 максимально коротких и простых специфичных для каждого пакета кода на python, отображающий на экране с максимально возможным FPS для данного пакета график sin()+noise.
Получение высокого FPS для 2D графики на Python
При разработке 2D графики на Python лимитировать может производительность, особенно если вы стремитесь к высокому количеству кадров в секунду (FPS). В этой статье рассмотрим несколько наиболее популярных графических библиотек, их производительность и возможности достижения высоких значений FPS.
Предварительные данные по информации из интернет источников
- Mayavi 3D: Хотя Mayavi в первую очередь предназначен для 3D-визуализации, он может использоваться для 2D-графиков. Однако его производительность для 2D задач может быть ниже, чем у специализированных библиотек.
- PyVista: Эта библиотека также ориентирована на 3D, но может использоваться для 2D. PyVista предлагает хорошую производительность, но для 2D задач может быть избыточной.
- Matplotlib: Широко используемая библиотека для построения графиков, но не оптимизирована для высоких FPS. Обычно Matplotlib работает на уровне 10-30 FPS, что может быть недостаточно для динамичных приложений.
- PyQTGraph: Эта библиотека специально разработана для быстрого отображения графиков и может достигать 50 FPS и выше. PyQTGraph использует OpenGL для рендеринга, что значительно увеличивает производительность.
- Plotly: Отлично подходит для интерактивных графиков, но его производительность может быть ограничена при большом количестве данных. FPS обычно ниже 50.
- PyGame: Одна из самых популярных библиотек для создания игр на Python. PyGame может достигать 60 FPS и выше, если правильно настроить рендеринг и управление событиями.
- Arcade: Современная библиотека для создания 2D-игр, которая также может достигать 60 FPS и выше. Arcade использует OpenGL и предлагает простые инструменты для работы с графикой.
- pyOpenGL: Это обертка для OpenGL, которая позволяет создавать высокопроизводительные графические приложения. С правильной оптимизацией можно достичь FPS в 100 и выше.
- VisPy: Библиотека для визуализации данных, использующая OpenGL. VisPy может достигать высоких значений FPS, особенно при работе с большими объемами данных.
- Bokeh: Хотя Bokeh в первую очередь предназначен для веб-визуализации, его производительность для 2D графиков может быть ограничена, и FPS обычно 10..50.
Тестирование на скорость рисования 2D графиков
Для тестирования производительности различных библиотек использовались простые сценарии, которые рисуют sin() + noise() на экране и измеряют FPS. Важно учитывать, что производительность может зависеть от аппаратного обеспечения и настроек системы.
Достижение частоты кадров (FPS) > 30 кадров в секунду вполне осуществимо с использованием популярных библиотек. Однако для достижения FPS >= 60 потребуется обращение к низкоуровневым библиотекам, а также тщательная оптимизация кода.
Важно отметить, что включение вертикальной синхронизации (VSync=On) не всегда доступно, поскольку это зависит от конкретной видеокарты, драйверов и мониторов, включая современные 4K телевизоры. Даже если VSync доступна, не все значения частоты обновления могут быть выбраны произвольно, и не всегда они будут корректно обрабатывать сигналы VSync в графических пакетах. Например, синхронизация может быть доступна на 30, 50 Гц, но не на 60 Гц или 44 Гц и так далее.
Тестирование
MatplotLib: FPS 35
# ЭТО ПЕРВЫЙ ВАРИАНТ КОДА, медленный FPS 12, ниже улучшенный FPS 35 (на основе рекомендаций из комментов и рекомендаций AI)
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_()
Улучшенный код для Matplotlib
Скрытый текст
Bokeh: FPS 35
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
# ЭТО ПЕРВЫЙ ВАРИАНТ КОДА, медленный FPS 12, ниже улучшенный FPS 35 (на основе рекомендаций из комментов и рекомендаций AI)
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_()
Выводы
Выбор библиотеки для 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 сможет отображать осциллограммы звукового сигнала в реальном времени.