Nous allons voir dans ce tutoriel comment observer un signal temporel sous forme de graphique avec PyQt grâce à PyQtGraph. Si vous créer des interfaces graphiques, il peut être intéressant plutôt que d’afficher des nombres qui défilent de les afficher sous forme de courbes comme sur un oscilloscope.
Installation
- PyQt (ou PySide)
pip install PyQt5
ou
pip install pyside6
- PyQtGraph
pip install pyqtgraph
Code pour afficher une simple courbe avec PyQtGraph
Pour afficher une courbe avec PyQtGraph, il suffit de rajouter un objet PlotWidget dans un objet PyQT QWidget.
import cv2 import sys #from PyQt5.QtWidgets import QMainWindow, QWidget, QLabel, QApplication #from PyQt5.QtCore import QThread, Qt, pyqtSignal, pyqtSlot #from PyQt5.QtGui import QImage, QPixmap from pyqtgraph import PlotWidget, plot from PySide6.QtWidgets import QMainWindow, QWidget, QLabel, QApplication, QVBoxLayout from PySide6.QtCore import QThread, Qt, Signal, Slot from PySide6.QtGui import QImage, QPixmap pyqtSignal = Signal #convert pyqt to pyside pyqtSlot = Slot class SignalContainer(QWidget): def __init__(self): super().__init__() self.title = 'Signal' self.initUI() def initUI(self): self.setWindowTitle(self.title) #self.resize(1200, 800) layout = QVBoxLayout() self.setLayout(layout) # create widget self.graphWidget = PlotWidget() layout.addWidget(self.graphWidget) #plot data time = [1,2,3,4,5,6,7,8,9,10] data = [30,32,34,32,33,31,29,32,35,45] self.graphWidget.plot(time, data) if __name__ == '__main__': app = QApplication(sys.argv) ex = SignalContainer() ex.show() sys.exit(app.exec())
Code pour créer un signal temporel
Pour faire vivre cette courbe, nous allons créer un objet QThread pour ne pas bloquer l’application qui va nous permettre de créer un signal sinusoïdale qui va évoluer dans le temps. A chaque itération, nous allons envoyer une mise à jour à l’aide du signal changeData.
class Thread(QThread): changeData = pyqtSignal(float,float) def run(self): self.isRunning=True self.time = 0 self.data = 0 f = 1. w = 2. * np.pi * f while self.isRunning: self.time+=0.01 self.data=2*np.sin(w*self.time) self.changeData.emit(self.time,self.data) time.sleep(0.01) def stop(self): self.isRunning=False self.quit() self.terminate()
Code de l’application PyQt
Pour afficher la courbe dans une application, nous allons créer un graphique PlotWidget dans un QWidget qui va instancier le QThread et tracer la courbe. La courbe sera mise à jour à chaque réception du signal changeData grâce à la fonction setData
- fonction setData
@pyqtSlot(float,float) def setData(self, t,d): #append data self.time.append(t) self.data.append(d) #remove first item self.time.pop(0) self.data.pop(0) #update graph self.graphWidget.clear() self.graphWidget.plot(self.time, self.data)
- signal changeData
self.th.changeData.connect(self.setData)
Code complet d’affichage d’un signal temporel
import cv2 import sys #from PyQt5.QtWidgets import QMainWindow, QWidget, QLabel, QApplication #from PyQt5.QtCore import QThread, Qt, pyqtSignal, pyqtSlot #from PyQt5.QtGui import QImage, QPixmap from pyqtgraph import PlotWidget, plot from PySide6.QtWidgets import QMainWindow, QWidget, QLabel, QApplication, QVBoxLayout from PySide6.QtCore import QThread, Qt, Signal, Slot from PySide6.QtGui import QImage, QPixmap pyqtSignal = Signal pyqtSlot = Slot import numpy as np import time class Thread(QThread): changeData = pyqtSignal(float,float) def run(self): self.isRunning=True self.time = 0 self.data = 0 f = 1. w = 2. * np.pi * f while self.isRunning: self.time+=0.01 self.data=2*np.sin(w*self.time) self.changeData.emit(self.time,self.data) time.sleep(0.01) def stop(self): self.isRunning=False self.quit() self.terminate() class SignalContainer(QWidget): def __init__(self): super().__init__() self.title = 'Signal' self.time = [0]*100 self.data = [0]*100 self.initUI() @pyqtSlot(float,float) def setData(self, t,d): #append data self.time.append(t) self.data.append(d) #remove first item self.time.pop(0) self.data.pop(0) #update graph self.graphWidget.clear() self.graphWidget.plot(self.time, self.data) def initUI(self): self.setWindowTitle(self.title) #self.resize(1200, 800) layout = QVBoxLayout() self.setLayout(layout) # create widget self.graphWidget = PlotWidget() layout.addWidget(self.graphWidget) #plot data #self.time = [1,2,3,4,5,6,7,8,9,10] #self.data = [30,32,34,32,33,31,29,32,35,45] self.graphWidget.plot(self.time, self.data) self.th = Thread(self) self.th.changeData.connect(self.setData) self.th.start() import signal #close signal with Ctrl+C signal.signal(signal.SIGINT, signal.SIG_DFL) if __name__ == '__main__': app = QApplication(sys.argv) ex = SignalContainer() ex.show() app.aboutToQuit.connect(ex.th.stop) #stop qthread when closing window sys.exit(app.exec())
Grâce à PyQtGraph, nous pouvons voir une fenêtre s’afficher avec le signal défiler dans l’interface PyQt comme sur un oscilloscope.
Configuration du style de PlotWidget
Il y a de nombreuse option pour configurer le style du graphique (couleur, legend, label, etc.)
- style de la courbe self.pen = mkPen()
- définir la couleur du fond self.graphWidget.setBackground
- ajouter un titre self.graphWidget.setTitle
- ajouter les labels sur les axes self.graphWidget.setLabel
- afficher la grille self.graphWidget.showGrid
- ajouter une légende self.graphWidget.addLegend
#tune plots self.pen = mkPen(color=(255, 0, 0), width=3, style=Qt.DashLine) #line style self.graphWidget.setBackground((50,50,50,220)) # RGBA #background self.graphWidget.setTitle("Signal(t)", color="w", size="20pt") #add title styles = {'color':'r', 'font-size':'20px'} #add label style self.graphWidget.setLabel('left', 'signal [SI]', **styles) #add ylabel self.graphWidget.setLabel('bottom', 'time [s]', **styles) #add xlabel self.graphWidget.showGrid(x=True, y=True) #add grid self.graphWidget.addLegend() #add grid self.graphWidget.setYRange(-2, 2, padding=0.1) #plot data self.graphWidget.plot(self.time, self.data,name = "signal",pen=self.pen,symbol='+', symbolSize=5, symbolBrush='w')
Bonus: Configurer le signal provenant de QThread
Il est possible de modifier les paramètres du signal géré par le QThread directement depuis l’interface. Dans cet exemple, nous allons modifier la fréquence, l’amplitude et l’échantillonnage du signal.
Pour cela nous allons créer trois champs et un bouton qui vont nous permettre de configurer le signal
N.B.: notez que chaque champs est connecté à la fonction setParam par le signal returnPressed qui détecte la touche « entrée »
#create param self.amplbl = QLabel("Ampl") self.amp=QLineEdit("2") self.amp.returnPressed.connect(self.setParam) self.freqlbl = QLabel("Freq") self.freq=QLineEdit("1") self.freq.returnPressed.connect(self.setParam) self.samplbl = QLabel("Ts") self.samp=QLineEdit("0.02") self.samp.returnPressed.connect(self.setParam) self.conf = QPushButton("Configure") self.conf.clicked.connect(self.setParam)
Lors d’un changement de configuration, la fonction setParam est exécutée. La fonction émet le signal changeParam avec un dictionnaire en argument
def setParam(self): if self.amp.text()!='' and self.freq.text()!='' and self.samp.text()!='': if float(self.samp.text())>0: d={"amp":float(self.amp.text()),"freq":float(self.freq.text()),"samp":float(self.samp.text())} self.changeParam.emit(d)
Le signal changeParam se connecte à la fonction setParam du QThread() dans la définiton de SignalContainer
self.th.changeData.connect(self.setData) #reception self.changeParam.connect(self.th.setParam) #emission
Du côté de l’objet QThread, nous ajoutons une fonction setParam qui va mettre à jour les paramètres du signal
@pyqtSlot(dict) def setParam(self,param): self.amp=param["amp"] self.freq=param["freq"] self.samp=max(0.0001,param["samp"])
Nous pouvons ainsi modifier le signal depuis l’interface PyQt et l’afficher avec PyQtGraph
Code complet
#!/usr/bin/env python # -*- coding: utf-8 -*- import cv2 import sys #from PyQt5.QtWidgets import QMainWindow, QWidget, QLabel, QApplication #from PyQt5.QtCore import QThread, Qt, pyqtSignal, pyqtSlot #from PyQt5.QtGui import QImage, QPixmap from pyqtgraph import PlotWidget, mkPen from PySide6.QtWidgets import QMainWindow, QWidget, QLabel, QApplication, QVBoxLayout, QHBoxLayout, QLineEdit, QPushButton from PySide6.QtCore import QThread, Qt, Signal, Slot from PySide6.QtGui import QImage, QPixmap pyqtSignal = Signal pyqtSlot = Slot import numpy as np import time class Thread(QThread): changeData = pyqtSignal(float,float) def __init__(self,a): super(Thread,self).__init__() self.amp=2 self.freq=1 self.samp=0.02 self.time = 0 self.data = 0 def run(self): self.isRunning=True while self.isRunning: self.time+=self.samp self.data=self.amp*np.sin(2. * np.pi * self.freq *self.time) self.changeData.emit(self.time,self.data) time.sleep(0.1) def stop(self): self.isRunning=False self.quit() self.terminate() @pyqtSlot(dict) def setParam(self,param): self.amp=param["amp"] self.freq=param["freq"] self.samp=max(0.0001,param["samp"]) class SignalContainer(QWidget): changeParam = pyqtSignal(dict) def __init__(self): super().__init__() self.title = 'Signal' self.span=10 self.time = [0]*1000 self.data = [0]*1000 self.initUI() @pyqtSlot(float,float) def setData(self, t,d): #append data self.time.append(t) self.data.append(d) #remove first item self.time.pop(0) self.data.pop(0) #update graph self.graphWidget.clear() self.graphWidget.plot(self.time, self.data,name = "signal",pen=self.pen,symbol='+', symbolSize=5, symbolBrush='w') if self.time[-1]>self.span: self.graphWidget.setXRange(self.time[-1]-self.span, self.time[-1], padding=0) self.graphWidget.setYRange(min(-2,min(self.data)), max(2,max(self.data)), padding=0.1) def initUI(self): self.setWindowTitle(self.title) self.resize(800, 400) layout = QVBoxLayout() self.setLayout(layout) #create param self.amplbl = QLabel("Ampl") self.amp=QLineEdit("2") self.amp.returnPressed.connect(self.setParam) self.freqlbl = QLabel("Freq") self.freq=QLineEdit("1") self.freq.returnPressed.connect(self.setParam) self.samplbl = QLabel("Ts") self.samp=QLineEdit("0.02") self.samp.returnPressed.connect(self.setParam) self.conf = QPushButton("Configure") self.conf.clicked.connect(self.setParam) hlayo = QHBoxLayout() hlayo.addWidget(self.amplbl) hlayo.addWidget(self.amp) hlayo.addWidget(self.freqlbl) hlayo.addWidget(self.freq) hlayo.addWidget(self.samplbl) hlayo.addWidget(self.samp) hlayo.addWidget(self.conf) layout.addLayout(hlayo) # create widget self.graphWidget = PlotWidget() layout.addWidget(self.graphWidget) #tune plots self.pen = mkPen(color=(255, 0, 0), width=3, style=Qt.DashLine) #line style self.graphWidget.setBackground((50,50,50,220)) # RGBA #background self.graphWidget.setTitle("Signal(t)", color="w", size="20pt") #add title styles = {'color':'r', 'font-size':'20px'} #add label style self.graphWidget.setLabel('left', 'signal [SI]', **styles) #add ylabel self.graphWidget.setLabel('bottom', 'time [s]', **styles) #add xlabel self.graphWidget.showGrid(x=True, y=True) #add grid self.graphWidget.addLegend() #add grid self.graphWidget.setXRange(0, self.span, padding=0) self.graphWidget.setYRange(-2, 2, padding=0.1) #plot data self.graphWidget.plot(self.time, self.data,name = "signal",pen=self.pen,symbol='+', symbolSize=5, symbolBrush='w') #manage thread self.th = Thread(self) self.amp.setText(str(self.th.amp)) self.freq.setText(str(self.th.freq)) self.samp.setText(str(self.th.samp)) self.th.changeData.connect(self.setData) #reception self.changeParam.connect(self.th.setParam) #emission self.th.start() def setParam(self): if self.amp.text()!='' and self.freq.text()!='' and self.samp.text()!='': if float(self.samp.text())>0: d={"amp":float(self.amp.text()),"freq":float(self.freq.text()),"samp":float(self.samp.text())} self.changeParam.emit(d) import signal #close signal with Ctrl+C signal.signal(signal.SIGINT, signal.SIG_DFL) if __name__ == '__main__': app = QApplication(sys.argv) ex = SignalContainer() ex.show() app.aboutToQuit.connect(ex.th.stop) #stop qthread when closing window sys.exit(app.exec())