๐ŸŽผ Project/๐ŸงŠ Monitoring System

[Monitoring System] 4.1 ์„ธ์ƒ์— ๋” ๋น ๋ฅธ ๋ฐฉ๋ฒ•์ด - pyqtgraph

darly213 2023. 4. 27. 15:47
728x90

 

 

[Monitoring System] 3. UDP ํ†ต์‹ ์œผ๋กœ ์‹ค์‹œ๊ฐ„ ์ง„๋™ ๋ฐ์ดํ„ฐ Plotting(feat. matplotlib)

์ด๋ฒˆ ํฌ์ŠคํŒ…์—์„œ๋Š” ์ด์ „ mqtt์—์„œ ์‚ฌ์šฉํ•œ ์˜ˆ์ œ ๋ฐ์ดํ„ฐ๋ณด๋‹ค 2๋งŒ ๋ฐฐ ์ •๋„ ๋งŽ์€ ๋ฐ์ดํ„ฐ๋ฅผ DAQ์—์„œ ๋ฐ›์•„์˜ค๋Š” UDP ์˜ˆ์ œ๋ฅผ ๋งŒ๋“ค ๊ฒƒ์ด๋‹ค. ๊ตฌํ˜„ํ•˜๋ ค๋Š” ์›น ์„œ๋น„์Šค์— ์˜จ๋„ ๋ฐ์ดํ„ฐ ๋ง๊ณ  ์ง„๋™ ๋ฐ์ดํ„ฐ๋„ ๋„ฃ์–ด๋ณด

dnai-deny.tistory.com

 

[Monitoring System] 4. UDP ํ†ต์‹ ์œผ๋กœ ์‹ค์‹œ๊ฐ„ ์ง„๋™ ๋ฐ์ดํ„ฐ FFT / STFT์‹œ๊ฐํ™”(numpy, tensorflow, pytorch)

๊ฒฐ๋ก ๋ถ€ํ„ฐ ๋งํ•˜๋ฉด stft๋Š” ๋ถ€ํ•˜๊ฐ€ ๋„ˆ๋ฌด ์‹ฌํ•ด์„œ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์‹œ๊ฐํ™”ํ•˜๋Š” ๊ฒƒ์€ ๋ฌด๋ฆฌ๋ฌด๋ฆฌ๋ฌด๋ฆฌ์ด๋‹ค. ์ผ๋‹จ matplotlib ์ž์ฒด์—์„œ ๋‹จ์ˆœ ์‹œ๊ฐํ™”๋ฅผ ํ•˜๊ธฐ์—๋„ ๋ฐ์ดํ„ฐ ์–‘์ด ๋„ˆ๋ฌด ๋งŽ๊ธฐ ๋•Œ๋ฌธ์— ์ผ์ข…์˜ ๋…นํ™”๊ธฐ๋Šฅ์„

dnai-deny.tistory.com

3~4์— ๊ตฌํ˜„ํ•œ matplotlib ๊ธฐ๋ฐ˜ ์‹ค์‹œ๊ฐ„ ๊ทธ๋ž˜ํ”„ ์‹œ๊ฐํ™”๋Š” ์•ฝ 1์ดˆ์— ํ•œ ๋ฒˆ ์ •๋„ ์—…๋ฐ์ดํŠธ ๋˜์—ˆ๊ณ , ์ด๋Š” ๊ฐ๊ด€์ ์œผ๋กœ ๋ดค์„ ๋•Œ ๋น ๋ฅด์ง€ ์•Š์•˜๋‹ค. 1์ดˆ์— 25000๊ฐœ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•„์˜ค๋Š” ๊ฒƒ์„ ๊ฐ์•ˆํ–ˆ์„ ๋•Œ 10๋งŒ ๊ฐœ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ํ•œ ๋ฒˆ์— ๋ณด์—ฌ์ฃผ๋ฏ€๋กœ ์•ฝ 4๋ฒˆ ์—…๋ฐ์ดํŠธํ•˜๋ฉด ๋‹ค๋ฅธ ๊ทธ๋ž˜ํ”„๊ฐ€ ๋˜์—ˆ๊ณ , ๋š๋š ๋Š๊ธฐ๋Š” ๊ฒƒ๋„ ์ฒด๊ฐ์ด ๋˜์—ˆ๋‹ค. 

์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•  ๋ฐฉ์•ˆ์„ ์ฐพ๋˜ ์ค‘, ๋” ๋น ๋ฅธ plotting ๋ฐฉ์‹ ๋ช‡ ๊ฐ€์ง€๋ฅผ ์ถ”์ฒœ๋ฐ›์•˜๋Š”๋ฐ ๊ทธ ์ค‘ ํ•˜๋‚˜๊ฐ€ ๋ฐ”๋กœ pyqtgraph์ด๋‹ค.

 

PyQtGraph — pyqtgraph 0.13.4.dev0 documentation

New to PyQtGraph? Check out the getting started guides. Content here includes introductions to PyQtGraph concepts and capabilities, as well as basic tutorials.

pyqtgraph.readthedocs.io

๊ฐ€์ง€๊ณ  ์žˆ๋Š” ๋‹ค๋ฅธ daq ์žฅ์น˜ ์ค‘ SpectraDAQ-200์ด ์žˆ๋Š”๋ฐ, ๊ฐ™์€ ํšŒ์‚ฌ์—์„œ ์ง€์›ํ•˜๋Š” Spectra PLUS ๋ชจ๋‹ˆํ„ฐ๋ง ์‹œ์Šคํ…œ๊ณผ ๊ฑฐ์˜ ๋น„์Šทํ•œ ์ˆ˜์ค€์˜ ์‹œ๊ฐํ™” ๋Šฅ๋ ฅ์„ ๋ณด์—ฌ์ฃผ์—ˆ๋‹ค! 3๋ฒˆ ๊ธ€์—์„œ ๊ตฌํ˜„ํ•œ ๋ถ€๋ถ„์„ pyqtgraph ๊ธฐ๋ฐ˜์œผ๋กœ ๋‹ค์‹œ ๋งŒ๋“ค์—ˆ๋‹ค. ํ•œ ๋ฒˆ ๋ณด๋„๋ก ํ•˜๊ฒ ๋‹ค.

import sys
from threading import Lock, Thread
import csv, socket, array

import pyqtgraph as pg

from PyQt5.QtWidgets import *
from PyQt5.QtCore import pyqtSlot, QTimer
from PyQt5.QtGui import QPalette, QColor, QFont

import torch
import numpy as np

from stft import show_result

pyqtgraph๋Š” PyQt5~6์„ ๊ธฐ๋ฐ˜์œผ๋กœ ๋™์ž‘ํ•˜๋ฏ€๋กœ PyQt5 ์„ค์น˜๊ฐ€ ํ•„์ˆ˜์ ์ด๋‹ค. windows์˜ ๊ฒฝ์šฐ pip๋กœ ์„ค์น˜ํ•˜๋ฉด ๋˜๊ณ , Ubuntu ๋“ฑ์—์„œ๋Š” ์ข€ ๋” ๋ณตํ•ฉ์ ์ธ ๋ฐฉ๋ฒ•์ด ํ•„์š”ํ•œ๋ฐ ๊ทธ ๋ถ€๋ถ„์€ ๋งํฌ ์ฐธ์กฐ.

 

ubutu 18.04 : pyqt5 ์„ค์น˜

1. ํ„ฐ๋ฏธ๋„์„ ์‹ฑํ–‰ํ•˜๊ณ  ์•„๋ž˜์™€ ๊ฐ™์€ ๋ช…๋ น์–ด๋ฅผ ์ž…๋ ฅํ•˜์—ฌ pyqt5๋ฅผ ์„ค์น˜ํ•ฉ๋‹ˆ๋‹ค. sudo apt-get install python-pyqt5 2. python3 ํ™˜๊ฒฝ์—์„œ ์‚ฌ์šฉํ•  pyqt5๋ฅผ ์„ค์น˜ํ•ฉ๋‹ˆ๋‹ค. sudo apt-get install python3-pyqt5 3. ๋””๋ฐ”์ด์Šค ํˆด์„ ๋‹ค์Œ

makingrobot.tistory.com

 

1. UI ๊ตฌ์„ฑ

class Main(QWidget):
    def __init__(self):
        super().__init__()

        self.paused = False
        self.label = QLabel("Real-Time Graph")
        self.label.setFont(QFont("Arial", 20))

        # define plot
        self.orig = pg.PlotWidget(title="Original")
        self.orig.plotItem.setLabels(bottom='Samples', left="IEPE (mV)")
        self.orig.plotItem.getAxis('bottom').setPen(pg.mkPen(color='#000000'))
        self.orig.plotItem.getAxis('left').setPen(pg.mkPen(color='#000000'))
        self.orig.setBackground('w')
        self.orig.setStyleSheet("border: 1px solid black; padding-left:10px; padding-right:10px; background-color: white;")
        
        self.fft = pg.PlotWidget(title="FFT")
        self.fft.plotItem.setLabels(bottom="Frequency (Hz)", left="Amplitude")
        self.fft.plotItem.getAxis('bottom').setPen(pg.mkPen(color='#000000'))
        self.fft.plotItem.getAxis('left').setPen(pg.mkPen(color='#000000'))
        self.fft.setBackground('w')
        self.fft.setStyleSheet("border: 1px solid black; padding-left:10px; padding-right:10px; background-color: white;")

        # plot control button
        self.pause_button = QPushButton()
        self.pause_button.setText("Pause/Restart")
        self.pause_button.clicked.connect(self.toggle_event)
        self.pause_button.setMinimumSize(300, 50)
        self.pause_button.setStyleSheet("background-color: white; padding-left:10px; padding-right:10px; font-size: 15px; font-family: Arial;")

        # start record button
        self.record_button = QPushButton()
        self.record_button.setText("Start Recording")
        self.record_button.clicked.connect(self.start_recording)
        self.record_button.setMinimumSize(300, 50)
        self.record_button.setStyleSheet("background-color: white; font-size: 15px;font-family: Arial;")
        
        # data save button
        self.save_button = QPushButton()
        self.save_button.setText("Save Data")
        self.save_button.clicked.connect(self.save_data)
        self.save_button.setMinimumSize(300, 50)
        self.save_button.setStyleSheet("background-color: white; font-size: 15px;font-family: Arial;")

        # show entire data analysis button
        self.show_button = QPushButton()
        self.show_button.setText("Show Analysis")
        self.show_button.clicked.connect(show_result)
        self.show_button.setMinimumSize(300, 50)
        self.show_button.setStyleSheet("background-color: white; font-size: 15px;font-family: Arial;")

        buttons = QHBoxLayout()
        buttons.addWidget(self.pause_button)
        buttons.addWidget(self.record_button)
        buttons.addWidget(self.save_button)
        buttons.addWidget(self.show_button)

        box = QVBoxLayout()
        box.addWidget(self.label)
        box.addWidget(self.orig)
        box.addWidget(self.fft)
        box.addLayout(buttons)
        box.setSpacing(30)
        box.setContentsMargins(50,50,50,50)
        self.setLayout(box)

        # set Window
        self.setGeometry(800, 300, 1500, 1000)
        self.setWindowTitle("Real Time Data Graphs")

๋Œ€๋ถ€๋ถ„ style sheet ์ ์šฉ๊ณผ ๊ด€๋ จ๋œ ๋‚ด์šฉ์ด๋ฏ€๋กœ ์ž์„ธํžˆ ๋ด์•ผํ•  ๊ฒƒ์€ ์ด๊ฒƒ ๋ฟ์ด๋‹ค.

self.orig = pg.PlotWidget(title="Original")
self.fft = pg.PlotWidget(title="FFT")

self.pause_button = QPushButton()
self.pause_button.setText("Pause/Restart")
self.pause_button.clicked.connect(self.toggle_event)

pg.PlotWidget ์€ matplotlib ์˜ ์ผ์ข…์˜ subplot ๊ฐ™์€ ์—ญํ• ์„ ํ•œ๋‹ค. ๋ฒ„ํŠผ์€ ๊ฑฐ์˜ ์œ ์‚ฌํ•˜๋ฏ€๋กœ ๋„˜์–ด๊ฐ„๋‹ค.

๊ทธ๋ฆฌ๊ณ  HBox Layout์— ๋ฒ„ํŠผ๋“ค์„ ๊ฐ€๋กœ๋กœ ์ญ‰ ๋„ฃ์–ด์ฃผ๊ณ , VBox Layout์— ๊ทธ๋ž˜ํ”„์™€ ๋ฒ„ํŠผ๋“ค์„ ์‹น ๋„ฃ์–ด์ฃผ๋ฉด ์ด๋Ÿฐ ๊ทธ๋ž˜ํ”„๊ฐ€ ์ƒ๊ธด๋‹ค.

์ œ๋ฒ• ๊ทธ๋Ÿด๋“ฏํ•˜๋‹ค(๋ฟŒ๋“ฏ)

original signal graph์™€ fft graph ๋ชจ๋‘ ์„ ๊ทธ๋ž˜ํ”„ ์ด๋ฏ€๋กœ ์•„๋ž˜์™€ ๊ฐ™์ด ์„ ์–ธํ•ด์ค„ ์ˆ˜ ์žˆ๋‹ค.

# set line graph
self.orig_p = self.orig.plot(pen='b')
self.fft_p = self.fft.plot(pen='r')

pyqtgraph์—์„œ matplotlib์˜ animation๊ณผ ์œ ์‚ฌํ•œ ๊ฒฐ๊ณผ๋ฅผ ๋‚ด๋„๋ก ํ•ด์ฃผ๋Š” ๊ฒƒ์ด ๋ฐ”๋กœ Timer์ด๋‹ค. ์„ค์ •ํ•œ ์‹œ๊ฐ„๋งˆ๋‹ค ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•ด์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐฑ์‹ ํ•˜๋„๋ก ํ•˜๊ณ , ์ด ๊ฐฑ์‹ ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ทธ ํ•จ์ˆ˜์—์„œ ๊ทธ๋ž˜ํ”„๋ฅผ ๊ทธ๋ฆฌ๋„๋ก ๋‹ค๋ฅธ ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•ด์„œ ๊ทธ๋ž˜ํ”„๋ฅผ ๊ฐฑ์‹ ํ•œ๋‹ค.

# set timer - animation controller
self.mytimer = QTimer()
self.mytimer.start(25)  
self.mytimer.timeout.connect(self.get_data)

self.draw_chart()
self.show()

 

2. ๋ฐ์ดํ„ฐ ๊ฐฑ์‹  / ๊ทธ๋ž˜ํ”„ ๊ทธ๋ฆฌ๊ธฐ

def draw_chart(self):
    """
    Update plot data and calculate FFT.
    """
    # update original signal graph
    self.orig_p.setData(self.graph_data)  

    # update fft graph
    Fs = 25600.0
    n = len(self.graph_data)

    # pytorch fft
    graph_data = torch.Tensor(self.graph_data)
    graph_data.to("cuda:0")
    fft_data = torch.fft.rfft(graph_data) / n
    frequency = torch.arange(0.0, Fs/2.0, Fs/n)

    self.fft_p.setData(frequency[:len(fft_data)//2], np.abs(fft_data[:len(fft_data)//2]))


@pyqtSlot()
def get_data(self):
    """
    Separate plotting data from entire data.
    """
    data_lock.acquire()
    if len(entire_data) > 110000:
        self.graph_data = entire_data[len(entire_data) - 100000:]
    else:
        self.graph_data = entire_data
    data_lock.release()
    self.draw_chart()

์ด ๋ถ€๋ถ„์€ matplotlib์œผ๋กœ ๊ตฌํ˜„ํ–ˆ์„ ๋•Œ์™€ ๊ฑฐ์˜ ๊ฐ™๋‹ค. ๊ทธ๋•Œ๋Š” set์ด์—ˆ๋‹ค๋ฉด ์ด๋ฒˆ์—๋Š” setData๋ฅผ ์ผ๋‹ค๋Š” ์ •๋„์˜ ์ฐจ์ด๋งŒ ์žˆ๋‹ค.

@pyqtSlot()์€ signal์„ ์ˆ˜์‹ ํ•˜์˜€์„ ๋•Œ ์‹คํ–‰๋˜๋Š” signal controller๋ผ๋Š” ๋œป์ด๋‹ค. ์œ„์—์„œ timer๋ฅผ ์„ ์–ธํ•  ๋•Œ timeout์ด ๋˜์—ˆ๋‹ค๋Š” ์‹œ๊ทธ๋„์ด ๋ฐœ์ƒํ•˜๋ฉด get_data ํ•จ์ˆ˜๊ฐ€ ๋ถˆ๋ฆฌ๋Š” ์‹์ด๋‹ค.

3. ๋ฒ„ํŠผ ํ•จ์ˆ˜ ์„ ์–ธ

def toggle_event(self):
    """
    Control animation. Pause/Restart.
    """
    if self.paused:
        self.mytimer.start()
    else:
        self.mytimer.stop()
    self.paused = not self.paused

def start_recording(self) -> None:
    """
    Set start point of saving data.
    """
    data_lock.acquire()
    self.pos = len(entire_data) - 1
    data_lock.release()
    print("Recording started.")

def save_data(self) -> None:
    """
    Save entire voltage data in csv.
    During save data, graph plotting will be paused.
    Using lock.
    """
    self.mytimer.stop()
    print("Saving...")

    data_lock.acquire()
    f = open("data.csv", "w")
    target = csv.writer(f)
    target.writerow(entire_data[self.pos:])
    data_lock.release()

    print("Saved {0} Data. Restart plotting.".format(len(entire_data)))
    self.mytimer.start()

animation.stop์ด timer์˜ stop๊ณผ start๋กœ ๋ณ€ํ•œ ๊ฒƒ์„ ์ œ์™ธํ•˜๋ฉด ๋‹ฌ๋ผ์ง„ ์ ์ด ์—†๋‹ค. ์š”๊ฒƒ๋„ ๋„˜์–ด๊ฐ„๋‹ค. ๋‹น์—ฐํžˆ UDP ํ†ต์‹ ์„ ํ•˜๋Š” Thread ์ฝ”๋“œ๋„ ๋‹ฌ๋ผ์ง„ ๊ฒƒ์ด ์—†๋‹ค.

def run() -> None:
    """
    UDP data receive function.
    Using lock.
    """
    print("Connected", flush=True)
    while True:
        data = serverSocket.recv(32768)
        signals = array.array('d')
        signals.frombytes(data)
        data_lock.acquire()
        entire_data.extend(signals)
        data_lock.release()

 

4. Main ํ•จ์ˆ˜ ์ž‘์„ฑ

if __name__ == "__main__":
    app = QApplication(sys.argv)
    pal = app.palette()
    pal.setColor(QPalette.Window, QColor(245,245,245))
    app.setPalette(pal)

    ex = Main()

    # define socket setting and bind
    server = ('YOUR_IP_ADDRESS', 2001)
    serverSocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    serverSocket.bind(server)

    # set data / data lock
    entire_data = [0, 0]
    data_lock = Lock()

    # start UDP in another thread
    t = Thread(target=run, args=())
    t.start()

    sys.exit(app.exec_())

PyQt ์ฐฝ์„ ์‹คํ–‰์‹œํ‚ค๊ธฐ ์œ„ํ•œ ์ฝ”๋“œ๋งŒ ์กฐ๊ธˆ ์ถ”๊ฐ€๋˜์—ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ๋ฐ”๊ฟ”์ฃผ๋ฉด ๋! ์‚ฌ์‹ค matplotlib์—์„œ pyqt๋กœ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋งŒ ๊ฐˆ์•„ํƒ„ ๊ฑด๋ฐ๋„ ์„ฑ๋Šฅ์ฐจ์ด๊ฐ€ ์—„์ฒญ๋‚˜๊ฒŒ ๋‚ฌ๋‹ค. ์ง€์—ฐ์‹œ๊ฐ„์ด ์ฒด๊ฐ 20๋ฐฐ๋Š” ์ค„์–ด๋“  ๋А๋‚Œ... ์ง„์งœ ์‹ค์‹œ๊ฐ„์ด๋ผ๋Š” ๋А๋‚Œ์ด ๊ฐ•ํ•ด์กŒ๋‹ค.

STFT๋ฅผ ๊ณ„์‚ฐํ•ด์„œ ์ €์žฅํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ์‹œ๊ฐํ™”ํ•˜๋Š” ๋ถ€๋ถ„์€ matplotlib์œผ๋กœ ๋‚จ๊ฒจ๋’€๋‹ค. ๊ทธ์ชฝ์ด ๋” ๋ณด๊ธฐ๋Š” ์ข‹๊ธฐ๋„ ํ•˜๊ณ , ์„ฑ๋Šฅ๋„ ์ค‘์š”ํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. 

์—ฌ๋‹ด์œผ๋กœ pyqtgraph๋ฅผ ์‚ฌ์šฉํ•˜๋‹ˆ STFT๋„ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์‹œ๊ฐํ™”๊ฐ€ ๊ฐ€๋Šฅํ•ด์กŒ๋Š”๋ฐ, ๋ฐ์ดํ„ฐ๊ฐ€ ๋ฐ์ดํ„ฐ๋‹ค ๋ณด๋‹ˆ ๋‹น์žฅ์€ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ณธ๋‹ค๊ณ  ๋‹ฌ๋ผ์ง€์ง€ ์•Š์„ ๊ฒƒ ๊ฐ™์•„ ์ ์šฉํ•˜์ง€ ์•Š์•˜๋‹ค.

 

์•„๋ฌดํŠผ ์™„์„ฑ๋ณธ์€ ์š”๋ ‡๊ฒŒ~ 

728x90