fbpixel
Étiquettes :

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())
pyqt-pyqtgraph-simple-curve Afficher un signal dans PyQt avec PyQtGraph

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.

pyqt-pyqtgraph-time-series Afficher un signal dans PyQt avec PyQtGraph

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')
pyqt-pyqtgraph-format Afficher un signal dans PyQt avec PyQtGraph

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

pyqt-pyqtgraph-param Afficher un signal dans PyQt 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())
		
		

Sources