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:
@ -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"):
|
||||
"""
|
||||
|
84
bec_widgets/utils/fps_counter.py
Normal file
84
bec_widgets/utils/fps_counter.py
Normal 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)
|
@ -57,6 +57,7 @@ class BECImageShow(BECPlotBase):
|
||||
"set_x_lim",
|
||||
"set_y_lim",
|
||||
"set_grid",
|
||||
"enable_fps_monitor",
|
||||
"lock_aspect_ratio",
|
||||
"export",
|
||||
"remove",
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -72,6 +72,7 @@ class BECWaveform(BECPlotBase):
|
||||
"set_y_lim",
|
||||
"set_grid",
|
||||
"set_colormap",
|
||||
"enable_fps_monitor",
|
||||
"lock_aspect_ratio",
|
||||
"export",
|
||||
"remove",
|
||||
|
@ -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.
|
||||
|
@ -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):
|
||||
"""
|
||||
|
Reference in New Issue
Block a user