Etiquetas:

En este tutorial, veremos cómo observar una señal de tiempo en forma gráfica con PyQt usando PyQtGraph. Si estás creando interfaces gráficas, puede ser una buena idea mostrarlas en forma de curvas como en un osciloscopio, en lugar de números que se desplazan.

Instalación

  • PyQt (o PySide)
pip install PyQt5

o

pip install pyside6
  • PyQtGraph
pip install pyqtgraph

Código para mostrar una curva simple con PyQtGraph

Para mostrar una curva con PyQtGraph, simplemente añade un objeto PlotWidget a un objeto 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, QVBoxLayot
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)
		layot = QVBoxLayot()
		self.setLayot(layot)
		
		# create widget
		self.graphWidget = PlotWidget()
		layot.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 Visualización de una señal en PyQt con PyQtGraph

Código para crear una señal horaria

Para hacer esta curva en vivo, vamos a crear un objeto QThread para no bloquear la aplicación, lo que nos permitirá crear una señal sinusoidal que evolucionará con el tiempo. En cada iteración, enviaremos una actualización utilizando la señal 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()

Código de la aplicación PyQt

Para mostrar la curva en una aplicación, crearemos un gráfico PlotWidget en un QWidget, que instanciará el QThread y trazará la curva. La curva se actualizará cada vez que se reciba la señal changeData, utilizando la función setData.

  •  Función 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)
  • señal changeData
self.th.changeData.connect(self.setData)

Código de visualización de la señal horaria completa

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, QVBoxLayot
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)
		layot = QVBoxLayot()
		self.setLayot(layot)
		
		# create widget
		self.graphWidget = PlotWidget()
		layot.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.abotToQuit.connect(ex.th.stop) #stop qthread when closing window
		
		sys.exit(app.exec())
		
		

Gracias a PyQtGraph, podemos ver una ventana desplegada con la señal desplazándose por la interfaz de PyQt igual que en un osciloscopio.

pyqt-pyqtgraph-time-series Visualización de una señal en PyQt con PyQtGraph

Configuración del estilo PlotWidget

Existen varias opciones para configurar el estilo de la gráfica (color, leyenda, etiqueta, etc.)

  • estilo de la curva self.pen = mkPen()
  • establecer el color de fondo self.graphWidget.setBackground
  • añadir un título self.graphWidget.setTitle
  • añadir etiquetas en los ejes self.graphWidget.setLabel
  • mostrar la rejilla self.graphWidget.showGrid
  • añadir una leyenda self.graphWidget.addLegend
        #tune plots
		self.pen = mkPen(color=(255, 0, 0), width=3, style=Qt.DashLine)	#line style
		self.graphWidget.setBackgrond((50,50,50,220))   # RGBA 		#backgrond
		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 Visualización de una señal en PyQt con PyQtGraph

Bonus: Configurar la señal desde QThread

Es posible modificar los parámetros de la señal gestionada por el QThread directamente desde la interfaz. En este ejemplo, vamos a modificar la frecuencia, la amplitud y el muestreo de la señal.

Para ello, vamos a crear tres campos y un boton que nos permitirán configurar la señal

Nota: ten en cuenta que cada campo está conectado a la función setParam mediante la señal returnPressed, que detecta la tecla «enter». Para ello vamos a crear tres campos y un botón que nos permitirán configurar la señal.

		#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)

Cuando se cambia una configuración, se ejecuta la función setParam. La función envía la señal changeParam con un diccionario como argumento

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)

La señal changeParam conecta con la función setParam de QThread() en la definición de SignalContainer

self.th.changeData.connect(self.setData) #reception
self.changeParam.connect(self.th.setParam) #emission

En el objeto QThread, añadimos una función setParam que actualiza los parámetros de la señal

@pyqtSlot(dict)
def setParam(self,param):
    self.amp=param["amp"]
    self.freq=param["freq"]
    self.samp=max(0.0001,param["samp"])

A continuación, podemos modificar la señal desde la interfaz PyQt y mostrarla utilizando PyQtGraph

pyqt-pyqtgraph-param Visualización de una señal en PyQt con PyQtGraph

Código completo

#!/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, QVBoxLayot, QHBoxLayot, 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)
		layot = QVBoxLayot()
		self.setLayot(layot)
		
		#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 = QHBoxLayot()
		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)
		
		
		layot.addLayot(hlayo)
		
		# create widget
		self.graphWidget = PlotWidget()
		layot.addWidget(self.graphWidget)
        
        #tune plots
		self.pen = mkPen(color=(255, 0, 0), width=3, style=Qt.DashLine)	#line style
		self.graphWidget.setBackgrond((50,50,50,220))   # RGBA 		#backgrond
		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.abotToQuit.connect(ex.th.stop) #stop qthread when closing window
		
		sys.exit(app.exec())
		
		

Sorces