반응형

본 글은 다음 글을 주로 참고하여 작성했습니다.

파이썬 예제 (Matplotlib, 실시간 챠트), https://oceancoding.blogspot.com/2019/12/matplotlib.html

 

필요한 패키지

설치한다.

pip install matplotlib
pip install pypubsub

여기서 사용하는 stop_watch.py 가 있는데 그건 여기서 가져다 쓴다.

[PYTHON] 시간 간격을 알려주는 스탑워치 클래스 만들기, https://stella47.tistory.com/448

 

구현 내용 설명

1. Tester에서 버튼으로 Figure를 생성, 정지, 재시작, 종료를 조작하고, 그리는 시간을 표시해준다.

2. 생성된 LiveFigure는 내부 타이머를 통해 업데이트 되는 값을 animation.Funcanimation을 통해 실시간으로 그려준다.

 

미구현 내역

1. 정지/재시작 시에 그래프를 새로 그리는데, 기존 그래프가 남아있었으면 좋겠다.

2. blitting을 통해서 그리는 속도를 더 빠르게 하고 싶다. [2]

 

결과물

생각 외로 잘 된다.

100Hz 로 설정하면 20Hz 정도가 나오는 것을 확인했다.

하지만 데이터와 subplot이 늘어나면 더 느려지므로 blitting을 해봐야겠다.

 

지금 세팅으로 interval과 실제 실행 시간을 비교하면 다음과 같다.

animation.Funcanimation(interval) real interval
10 ms 55ms
25 ms 55ms
40 ms 60 ~ 75ms
50 ms 75 ~ 90ms
100 ms 125 ~ 135 ms
200 ms 210 ~ 225 ms

이 세팅으로 최소 주기는 20Hz 가 최대이고

그림그리는데 10~35ms 정도를 소모하는 것 같다.

그림 그리는 것(self.animate())만 측정하면 대략 20~25ms 였다.


코드 구현

크게 코드는 Tester 클래스와 FigureBase 클래스로 나뉘어 있다.

FigureBase 클래스는 다른 클래스가 상속받아서 다음을 해볼 수 있을 것이다.

  1. subplot 추가
  2. self._time, self._data 에 데이터를 추가하고, 해당 데이터를 선택적으로 특정 subplot에 추가/제거하기

더보기를 누르면 코드를 볼 수 있다.

더보기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
import sys
from PyQt5.QtCore import Qt, QTimer
from PyQt5.QtWidgets import QPushButton, QLabel
from PyQt5.QtWidgets import QMainWindow, QApplication, QWidget, QVBoxLayout
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar
 
from pubsub import pub
 
import time
import math
 
from stop_watch import StopWatch
 
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)
 
class FigureBase(QWidget):
    def __init__(self):
        super().__init__()
        
        # Default Figure Setting
        self._number_of_figures = 1
        self._time_scale = 10
        self.xs = list()
        self.ys = list()
        # sub-class will store data at this lists
        self._time = list()
        self._data = list()
 
        # To check running time
        self._stop_watch_draw = StopWatch()
        self._stop_watch_module = StopWatch()
        
        self._init_draw()
        self._init_UI()
        self.show()
 
    def __del__(self):
        plt.close()
 
    # override figure close event from QWidget
    def closeEvent(self, event):
        self.close()
 
    def _init_UI(self):
        vbox = QVBoxLayout()
        vbox.addWidget(self.toolbar)
        vbox.addWidget(self.canvas)
        self.setLayout(vbox)
        self.setGeometry(400,300,400,600)
 
    def _init_draw(self):
        # for PyQt embedding
        self.fig = plt.Figure()
        self.canvas = FigureCanvas(self.fig)
        self.toolbar = NavigationToolbar(self.canvas, self)
 
        # make figure list
        self._plots = list()
        for idx in range(0self._number_of_figures):
            self._plots.append({"FIGURE":self.fig.add_subplot(self._number_of_figures,1,idx+1), 'INDEX':list()})
            self._plots[idx]['FIGURE'].clear()
 
        # 애니메이션 생성하고 그리기
        self.ani = animation.FuncAnimation(fig=self.fig, func=self.animate, interval=10, blit=False, save_count=100)
        self.canvas.draw()
 
    # 그리기 전에 아무거나 계산
    def _before_drawing(self):
        self._stop_watch_module.click()
        x = self._stop_watch_draw.get_time_since()
        self.xs.append(x)
        self.ys.append(math.sin(x))
        pub.sendMessage("draw_dt",msgData=self._stop_watch_module.get_dt())
 
    # Sub-class must override this function
    def animate(self, event):
        self._before_drawing()
 
        min_idx = self._find_x_lim()
        for item in self._plots:
            item['FIGURE'].clear()
            item['FIGURE'].grid(True)
            if(len(self.xs) >= 2):
                minmax = [self.xs[-1]-self._time_scale, self.xs[-1]]
                item['FIGURE'].set_xlim(minmax[0], minmax[1])
            # Put each data @ sub-class
            # for idx in item['INDEX']
            #     item['FIGURE'].plot(self._time[-min_idx:], self._data[idx][-min_idx:])
            item['FIGURE'].plot(self.xs[-min_idx:], self.ys[-min_idx:])
 
    # 보여줄 크기에 맞는 인덱스를 찾는다.
    def _find_x_lim(self):
        min_idx = 0
        for idx in range(len(self.xs)):
            if(self.xs[-1- self.xs[-idx-1> self._time_scale):
                min_idx = idx-1
                break
        return min_idx
 
    def pause(self):
        # stop 은 멈추는데
        # start 가 아예 새로 시작함
        # self.ani.event_source.stop()
        self.ani.pause()
 
    def resume(self):
        # start나 resume이랑 다를 바 없이 다 지우고 시작한다.
        # self.ani.event_source.start()
        self.ani.resume()
 
    def close(self):
        self.ani.event_source.stop() # 애니메이션 정지
        # self._timer.stop() # 데이터 업데이트 정지
        # plt.close() # 이거 해봤자 창이 안꺼짐.
        super().close() # 이걸 해야 위젯이 꺼진다.
 
        
class Tester(QMainWindow):
 
    def __init__(self):
        super().__init__()
        self.w = None  # No external window yet.
        self.button = QPushButton("Create",self)
        self.button.setGeometry(10108030)
        self.button.clicked.connect(self.create_plot)
 
        self.button = QPushButton("Close",self)
        self.button.setGeometry(10508030)
        self.button.clicked.connect(self.close_plot)
        
        self.button = QPushButton("Pause",self)
        self.button.setGeometry(100108030)
        self.button.clicked.connect(self.pause_plot)
 
        self.button = QPushButton("Resume",self)
        self.button.setGeometry(100508030)
        self.button.clicked.connect(self.resume_plot)
 
        self.label = QLabel("dt :   ms"self)
        self.label.setGeometry(1010020050)
        font = self.label.font()
        font.setPointSize(10)
        self.label.setFont(font)
 
        pub.subscribe(self._cb_dt, 'draw_dt')
        self.dt = 0
 
        self.setGeometry(100,300,300,300)
        self.show()
 
    def create_plot(self):
        if self.w is not None:
            if (self.w.isVisible() == False): # 우측 상단 X으로 끄면 visible이 False가 된다.
                self.close_plot()
        self.w = FigureBase() # 아무튼 초기화 했으니 켠다.
    def pause_plot(self):
        if self.w is not None:
            self.w.pause()
 
    def resume_plot(self):
        if self.w is not None:
            self.w.resume()
 
    def close_plot(self):
        if self.w is not None:
            self.w.close()
            self.w.__del__()
            self.w = None
 
    def _cb_dt(self, msgData):
        self.dt = msgData
        self.label.setText("dt : {:10.3f} ms".format(self.dt))
        
if __name__ == "__main__":
    app = QApplication(sys.argv)
    w = Tester()
    app.exec()
 
cs

 

 

다음으로 이어지는 글이 있습니다.

[PYTHON] matplotlib의 빠른 애니메이션 그리기 : blitting, https://stella47.tistory.com/449

[PyQt5] 선택한 figure 창에 그래프 그려주는 프로그램, https://stella47.tistory.com/447

 

Reference

[1] "파이썬 예제 (Matplotlib, 실시간 챠트)", https://oceancoding.blogspot.com/2019/12/matplotlib.html

[2] "Faster rendering by using blitting", https://matplotlib.org/stable/tutorials/advanced/blitting.html

 

 

728x90

+ Recent posts