0
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2025-07-13 19:21:50 +02:00

feat(utils): FPS counter utility based on the viewBox updates, integrated to waveform and image widget

This commit is contained in:
2024-10-09 16:33:45 +02:00
committed by wyzula_j
parent b681b13a33
commit 8c5ef26843
8 changed files with 236 additions and 8 deletions

View File

@ -1135,6 +1135,15 @@ class BECImageShow(RPCBase):
y(bool): Show grid on the y-axis.
"""
@rpc_call
def enable_fps_monitor(self, enable: "bool" = True):
"""
Enable the FPS monitor.
Args:
enable(bool): True to enable, False to disable.
"""
@rpc_call
def lock_aspect_ratio(self, lock):
"""
@ -1330,6 +1339,15 @@ class BECImageWidget(RPCBase):
y_grid(bool): Visibility of the y-axis grid.
"""
@rpc_call
def enable_fps_monitor(self, enabled: "bool"):
"""
Enable the FPS monitor of the plot widget.
Args:
enabled(bool): If True, enable the FPS monitor.
"""
@rpc_call
def lock_aspect_ratio(self, lock: "bool"):
"""
@ -1666,6 +1684,15 @@ class BECPlotBase(RPCBase):
show(bool): Show the outer axes.
"""
@rpc_call
def enable_fps_monitor(self, enable: "bool" = True):
"""
Enable the FPS monitor.
Args:
enable(bool): True to enable, False to disable.
"""
@rpc_call
def lock_aspect_ratio(self, lock):
"""
@ -2069,6 +2096,15 @@ class BECWaveform(RPCBase):
colormap(str, optional): Scale the colors of curves to colormap. If None, use the default color palette.
"""
@rpc_call
def enable_fps_monitor(self, enable: "bool" = True):
"""
Enable the FPS monitor.
Args:
enable(bool): True to enable, False to disable.
"""
@rpc_call
def lock_aspect_ratio(self, lock):
"""
@ -2374,6 +2410,15 @@ class BECWaveformWidget(RPCBase):
y_grid(bool): Visibility of the y-axis grid.
"""
@rpc_call
def enable_fps_monitor(self, enabled: "bool"):
"""
Enable the FPS monitor of the plot widget.
Args:
enabled(bool): If True, enable the FPS monitor.
"""
@rpc_call
def lock_aspect_ratio(self, lock: "bool"):
"""

View File

@ -0,0 +1,84 @@
"""
This module provides a utility class for counting and reporting frames per second (FPS) in a PyQtGraph application.
Classes:
FPSCounter: A class that monitors the paint events of a `ViewBox` to calculate and emit FPS values.
Usage:
The `FPSCounter` class can be used to monitor the rendering performance of a `ViewBox` in a PyQtGraph application.
It connects to the `ViewBox`'s paint event and calculates the FPS over a specified interval, emitting the FPS value
at regular intervals.
Example:
from qtpy import QtWidgets, QtCore
import pyqtgraph as pg
from fps_counter import FPSCounter
app = pg.mkQApp("FPS Counter Example")
win = pg.GraphicsLayoutWidget()
win.show()
vb = pg.ViewBox()
plot_item = pg.PlotItem(viewBox=vb)
win.addItem(plot_item)
fps_counter = FPSCounter(vb)
fps_counter.sigFpsUpdate.connect(lambda fps: print(f"FPS: {fps:.2f}"))
sys.exit(app.exec_())
"""
from time import perf_counter
import pyqtgraph as pg
from qtpy import QtCore
class FPSCounter(QtCore.QObject):
"""
A utility class for counting and reporting frames per second (FPS).
This class connects to a `ViewBox`'s paint event to count the number of
frames rendered and calculates the FPS over a specified interval. It emits
a signal with the FPS value at regular intervals.
Attributes:
sigFpsUpdate (QtCore.Signal): Signal emitted with the FPS value.
view_box (pg.ViewBox): The `ViewBox` instance to monitor.
"""
sigFpsUpdate = QtCore.Signal(float)
def __init__(self, view_box):
super().__init__()
self.view_box = view_box
self.view_box.sigPaint.connect(self.increment_count)
self.count = 0
self.last_update = perf_counter()
self.timer = QtCore.QTimer()
self.timer.timeout.connect(self.calculate_fps)
self.timer.start(1000)
def increment_count(self):
"""
Increment the frame count when the `ViewBox` is painted.
"""
self.count += 1
def calculate_fps(self):
"""
Calculate the frames per second (FPS) based on the number of frames
"""
now = perf_counter()
elapsed = now - self.last_update
fps = self.count / elapsed if elapsed > 0 else 0.0
self.last_update = now
self.count = 0
self.sigFpsUpdate.emit(fps)
def cleanup(self):
"""
Clean up the FPS counter by stopping the timer and disconnecting the signal.
"""
self.timer.stop()
self.timer.timeout.disconnect(self.calculate_fps)

View File

@ -57,6 +57,7 @@ class BECImageShow(BECPlotBase):
"set_x_lim",
"set_y_lim",
"set_grid",
"enable_fps_monitor",
"lock_aspect_ratio",
"export",
"remove",

View File

@ -307,7 +307,7 @@ class BECImageItem(BECConnector, pg.ImageItem):
if vrange is not None:
self.color_bar.setLevels(low=vrange[0], high=vrange[1])
self.color_bar.setImageItem(self)
self.parent_image.addItem(self.color_bar) # , row=0, col=1)
self.parent_image.addItem(self.color_bar, row=1, col=1)
self.config.color_bar = "simple"
elif color_bar_style == "full":
# Setting histogram
@ -321,7 +321,7 @@ class BECImageItem(BECConnector, pg.ImageItem):
)
# Adding histogram to the layout
self.parent_image.addItem(self.color_bar) # , row=0, col=1)
self.parent_image.addItem(self.color_bar, row=1, col=1)
# save settings
self.config.color_bar = "full"

View File

@ -1,6 +1,5 @@
from __future__ import annotations
from collections import defaultdict
from typing import Literal, Optional
import bec_qthemes
@ -12,6 +11,7 @@ from qtpy.QtWidgets import QApplication, QWidget
from bec_widgets.utils import BECConnector, ConnectionConfig
from bec_widgets.utils.crosshair import Crosshair
from bec_widgets.utils.fps_counter import FPSCounter
from bec_widgets.utils.plot_indicator_items import BECArrowItem, BECTickItem
logger = bec_logger.logger
@ -51,6 +51,11 @@ class SubplotConfig(ConnectionConfig):
class BECViewBox(pg.ViewBox):
sigPaint = Signal()
def paint(self, painter, opt, widget):
super().paint(painter, opt, widget)
self.sigPaint.emit()
def itemBoundsChanged(self, item):
self._itemBoundsCache.pop(item, None)
@ -79,6 +84,7 @@ class BECPlotBase(BECConnector, pg.GraphicsLayout):
"set_y_lim",
"set_grid",
"set_outer_axes",
"enable_fps_monitor",
"lock_aspect_ratio",
"export",
"remove",
@ -100,12 +106,13 @@ class BECPlotBase(BECConnector, pg.GraphicsLayout):
self.figure = parent_figure
# self.plot_item = self.addPlot(row=0, col=0)
self.plot_item = pg.PlotItem(viewBox=BECViewBox(parent=self, enableMenu=True), parent=self)
self.addItem(self.plot_item, row=0, col=0)
self.addItem(self.plot_item, row=1, col=0)
self.add_legend()
self.crosshair = None
self.fps_monitor = None
self.fps_label = None
self.tick_item = BECTickItem(parent=self, plot_item=self.plot_item)
self.arrow_item = BECArrowItem(parent=self, plot_item=self.plot_item)
self._connect_to_theme_change()
@ -379,6 +386,10 @@ class BECPlotBase(BECConnector, pg.GraphicsLayout):
"""
self.plot_item.enableAutoRange(axis, enabled)
############################################################
###################### Crosshair ###########################
############################################################
def hook_crosshair(self) -> None:
"""Hook the crosshair to all plots."""
if self.crosshair is None:
@ -417,6 +428,54 @@ class BECPlotBase(BECConnector, pg.GraphicsLayout):
self.crosshair.clear_markers()
self.crosshair.update_markers()
############################################################
##################### FPS Counter ##########################
############################################################
def update_fps_label(self, fps: float) -> None:
"""
Update the FPS label.
Args:
fps(float): The frames per second.
"""
if self.fps_label:
self.fps_label.setText(f"FPS: {fps:.2f}")
def hook_fps_monitor(self):
"""Hook the FPS monitor to the plot."""
if self.fps_monitor is None:
# text_color = self.get_text_color()#TODO later
self.fps_monitor = FPSCounter(self.plot_item.vb) # text_color=text_color)
self.fps_label = pg.LabelItem(justify="right")
self.addItem(self.fps_label, row=0, col=0)
self.fps_monitor.sigFpsUpdate.connect(self.update_fps_label)
def unhook_fps_monitor(self):
"""Unhook the FPS monitor from the plot."""
if self.fps_monitor is not None:
# Remove Monitor
self.fps_monitor.cleanup()
self.fps_monitor.deleteLater()
self.fps_monitor = None
# Remove Label
self.removeItem(self.fps_label)
self.fps_label.deleteLater()
self.fps_label = None
def enable_fps_monitor(self, enable: bool = True):
"""
Enable the FPS monitor.
Args:
enable(bool): True to enable, False to disable.
"""
if enable and self.fps_monitor is None:
self.hook_fps_monitor()
elif not enable and self.fps_monitor is not None:
self.unhook_fps_monitor()
def export(self):
"""Show the Export Dialog of the plot widget."""
scene = self.plot_item.scene()
@ -431,6 +490,7 @@ class BECPlotBase(BECConnector, pg.GraphicsLayout):
def cleanup_pyqtgraph(self):
"""Cleanup pyqtgraph items."""
self.unhook_crosshair()
self.unhook_fps_monitor()
self.tick_item.cleanup()
self.arrow_item.cleanup()
item = self.plot_item

View File

@ -72,6 +72,7 @@ class BECWaveform(BECPlotBase):
"set_y_lim",
"set_grid",
"set_colormap",
"enable_fps_monitor",
"lock_aspect_ratio",
"export",
"remove",

View File

@ -40,6 +40,7 @@ class BECImageWidget(BECWidget, QWidget):
"set_rotation",
"set_log",
"set_grid",
"enable_fps_monitor",
"lock_aspect_ratio",
]
@ -104,6 +105,9 @@ class BECImageWidget(BECWidget, QWidget):
icon_name="reset_settings", tooltip="Reset Image Settings"
),
"separator_3": SeparatorAction(),
"fps_monitor": MaterialIconAction(
icon_name="speed", tooltip="Show FPS Monitor", checkable=True
),
"axis_settings": MaterialIconAction(
icon_name="settings", tooltip="Open Configuration Dialog"
),
@ -150,6 +154,7 @@ class BECImageWidget(BECWidget, QWidget):
self.toolbar.widgets["reset"].action.triggered.connect(self.reset_settings)
# sepatator
self.toolbar.widgets["axis_settings"].action.triggered.connect(self.show_axis_settings)
self.toolbar.widgets["fps_monitor"].action.toggled.connect(self.enable_fps_monitor)
###################################
# Dialog Windows
@ -450,6 +455,18 @@ class BECImageWidget(BECWidget, QWidget):
self.toolbar.widgets["rectangle_mode"].action.setChecked(False)
self._image.plot_item.getViewBox().setMouseMode(pg.ViewBox.PanMode)
@SafeSlot()
def enable_fps_monitor(self, enabled: bool):
"""
Enable the FPS monitor of the plot widget.
Args:
enabled(bool): If True, enable the FPS monitor.
"""
self._image.enable_fps_monitor(enabled)
if self.toolbar.widgets["fps_monitor"].action.isChecked() != enabled:
self.toolbar.widgets["fps_monitor"].action.setChecked(enabled)
def export(self):
"""
Show the export dialog for the plot widget.

View File

@ -52,6 +52,7 @@ class BECWaveformWidget(BECWidget, QWidget):
"set_legend_label_size",
"set_auto_range",
"set_grid",
"enable_fps_monitor",
"lock_aspect_ratio",
"export",
"export_to_matplotlib",
@ -118,9 +119,7 @@ class BECWaveformWidget(BECWidget, QWidget):
"fit_params": MaterialIconAction(
icon_name="monitoring", tooltip="Open Fitting Parameters"
),
"axis_settings": MaterialIconAction(
icon_name="settings", tooltip="Open Configuration Dialog"
),
"separator_3": SeparatorAction(),
"crosshair": MaterialIconAction(
icon_name="point_scan", tooltip="Show Crosshair", checkable=True
),
@ -129,6 +128,13 @@ class BECWaveformWidget(BECWidget, QWidget):
tooltip="Add ROI region for DAP",
checkable=True,
),
"separator_4": SeparatorAction(),
"fps_monitor": MaterialIconAction(
icon_name="speed", tooltip="Show FPS Monitor", checkable=True
),
"axis_settings": MaterialIconAction(
icon_name="settings", tooltip="Open Configuration Dialog"
),
},
target_widget=self,
)
@ -186,6 +192,7 @@ class BECWaveformWidget(BECWidget, QWidget):
self.toolbar.widgets["axis_settings"].action.triggered.connect(self.show_axis_settings)
self.toolbar.widgets["crosshair"].action.triggered.connect(self.waveform.toggle_crosshair)
self.toolbar.widgets["roi_select"].action.toggled.connect(self.waveform.toggle_roi)
self.toolbar.widgets["fps_monitor"].action.toggled.connect(self.enable_fps_monitor)
# self.toolbar.widgets["import"].action.triggered.connect(
# lambda: self.load_config(path=None, gui=True)
# )
@ -594,6 +601,8 @@ class BECWaveformWidget(BECWidget, QWidget):
checked(bool): If True, enable the linear region selector.
"""
self.waveform.toggle_roi(checked)
if self.toolbar.widgets["roi_select"].action.isChecked() != checked:
self.toolbar.widgets["roi_select"].action.setChecked(checked)
def select_roi(self, region: tuple):
"""
@ -604,6 +613,17 @@ class BECWaveformWidget(BECWidget, QWidget):
"""
self.waveform.select_roi(region)
def enable_fps_monitor(self, enabled: bool):
"""
Enable the FPS monitor of the plot widget.
Args:
enabled(bool): If True, enable the FPS monitor.
"""
self.waveform.enable_fps_monitor(enabled)
if self.toolbar.widgets["fps_monitor"].action.isChecked() != enabled:
self.toolbar.widgets["fps_monitor"].action.setChecked(enabled)
@SafeSlot()
def _auto_range_from_toolbar(self):
"""