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())
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.
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')
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
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())