Neste tutorial, veremos como observar um sinal de tempo em forma de gráfico com o PyQt usando o PyQtGraph. Se estiver criando interfaces gráficas, pode ser uma boa ideia exibi-las na forma de curvas, como em um osciloscópio, em vez de números rolantes.
Instalação
- PyQt (ou PySide)
pip install PyQt5
ou
pip install pyside6
- PyQtGraph
pip install pyqtgraph
Código para exibir uma curva simples usando PyQtGraph
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())
Código para criar um sinal horário
Para tornar esta curva viva, vamos criar um objeto QThread de modo a não bloquear a aplicação, o que nos permitirá criar um sinal sinusoidal que evoluirá ao longo do tempo. Em cada iteração, enviaremos uma atualização utilizando o sinal 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 da aplicação PyQt
Para apresentar a curva numa aplicação, criaremos um gráfico PlotWidget num QWidget, que instanciará o QThread e traçará a curva. A curva será actualizada sempre que o sinal changeData for recebido, utilizando a função setData.
- função 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)
- sinal changeData
self.th.changeData.connect(self.setData)
Código de visualização do sinal de hora 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, 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())
Graças ao PyQtGraph, podemos ver uma janela aparecer com o sinal a deslocar-se através da interface PyQt, tal como num osciloscópio.
Configurar o estilo do PlotWidget
Existem inúmeras opções para configurar o estilo do gráfico (cor, legenda, etiqueta, etc.).
- estilo da curva self.pen = mkPen()
- definir a cor de fundo self.graphWidget.setBackground
- adicionar um título self.graphWidget.setTitle
- adicionar etiquetas aos eixos self.graphWidget.setLabel
- mostrar a grelha self.graphWidget.showGrid
- adicionar uma legenda 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')
Bónus: Configurar o sinal a partir de QThread
É possível modificar os parâmetros do sinal gerido pela QThread diretamente a partir da interface. Neste exemplo, vamos modificar a frequência, a amplitude e a amostragem do sinal.
Para o efeito, vamos criar três campos e um botão que nos permitirão configurar o sinal
Nota: cada campo está ligado à função setParam pelo sinal returnPressed, que detecta a tecla “enter”.
#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)
Quando uma configuração é alterada, a função setParam é executada. A função envia o sinal changeParam com um dicionário 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)
O sinal changeParam liga-se à função setParam do QThread() na definição de SignalContainer
self.th.changeData.connect(self.setData) #reception self.changeParam.connect(self.th.setParam) #emission
No objeto QThread, adicionamos uma função setParam que actualiza os parâmetros do sinal
@pyqtSlot(dict) def setParam(self,param): self.amp=param["amp"] self.freq=param["freq"] self.samp=max(0.0001,param["samp"])
Podemos então modificar o sinal a partir da interface PyQt e exibi-lo usando 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, 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())