다음 글에 이어서 연재하는 글이다.
[PyQt5] 새 창을 띄워서 matplotlib.animation으로 실시간 그래프 그리기, https://stella47.tistory.com/445
앞선 글에서 plot을 그리는데 20~25ms 가 소요되는 것을 확인했다.
이건 괭장히 가벼운 상태인도 불구하고 연산이나 다른 프로그램을 끼게 되면 훨씬 더 느려진다.
더 빠르게 그림을 그릴 수 있는 방법이 있다 하니, 한번 보자.
Blitting 설명 요약
blitting 은 상호작용하는 figure의 성능을 비약적으로 높일 수 있는 표준화된 기법이라고 한다. 그래서 이미 animation 이나 widgets 등 에서 이미 사용하고 있다고 한다.
Blitting의 전략은 매번 렌더링할 때 변하지 않는 그래픽 요소들을 배경으로 그려서 반복적 그리기 작업을 빠르게 한다.
그래서 단계는 다음과 같다.
1. 일정한 배경을 준비한다.
- 움직인다고 하는 것들을 제외하고 그려준다.
- RBGA 버퍼 복사본을 저장한다.
2. 개별 이미지들을 렌더링한다.
- RBGA 버퍼 복사본을 불러온다.
- 움직이는 것들을 다시 그려준다.
- 결과 이미지를 화면에 뿌린다.
이미 animation에서 사용한다고 하나, 테스트해본 바로는 너무 느리다.
공식 문서[1]에 나와있는 것을 이용하여 테스트 해봤을 때, 1000번 기준으로 그리는 시간이 평균 2.754 ms, 표준 편차 0.621 ms 였다. 앞선 글에서 계측한 시간에 10배 가량 빠른 속도이다.
측정한 코드는 다음과 같다. 더보기를 누르면 코드를 볼 수 있다.
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
|
import matplotlib.pyplot as plt
import numpy as np
from stop_watch import StopWatch
class BlitManager:
def __init__(self, canvas, animated_artists=()):
"""
Parameters
----------
canvas : FigureCanvasAgg
The canvas to work with, this only works for subclasses of the Agg
canvas which have the `~FigureCanvasAgg.copy_from_bbox` and
`~FigureCanvasAgg.restore_region` methods.
animated_artists : Iterable[Artist]
List of the artists to manage
"""
self.canvas = canvas
self._bg = None
self._artists = []
for a in animated_artists:
self.add_artist(a)
# grab the background on every draw
self.cid = canvas.mpl_connect("draw_event", self.on_draw)
def on_draw(self, event):
"""Callback to register with 'draw_event'."""
cv = self.canvas
if event is not None:
if event.canvas != cv:
raise RuntimeError
self._bg = cv.copy_from_bbox(cv.figure.bbox)
self._draw_animated()
def add_artist(self, art):
"""
Add an artist to be managed.
Parameters
----------
art : Artist
The artist to be added. Will be set to 'animated' (just
to be safe). *art* must be in the figure associated with
the canvas this class is managing.
"""
if art.figure != self.canvas.figure:
raise RuntimeError
art.set_animated(True)
self._artists.append(art)
def _draw_animated(self):
"""Draw all of the animated artists."""
fig = self.canvas.figure
for a in self._artists:
fig.draw_artist(a)
def update(self):
"""Update the screen with animated artists."""
cv = self.canvas
fig = cv.figure
# paranoia in case we missed the draw event,
if self._bg is None:
self.on_draw(None)
else:
# restore the background
cv.restore_region(self._bg)
# draw all of the animated artists
self._draw_animated()
# update the GUI state
cv.blit(fig.bbox)
# let the GUI event loop process anything it has to do
cv.flush_events()
if __name__ == "__main__":
stop_watch = StopWatch()
dts = np.zeros([10000,1])
# make a new figure
fig, ax = plt.subplots()
# add a line
x = np.linspace(0, 2 * np.pi, 100)
(ln,) = ax.plot(x, np.sin(x), animated=True)
# add a frame number
fr_number = ax.annotate(
"0",
(0, 1),
xycoords="axes fraction",
xytext=(10, -10),
textcoords="offset points",
ha="left",
va="top",
animated=True,
)
bm = BlitManager(fig.canvas, [ln, fr_number])
# make sure our window is on the screen and drawn
plt.show(block=False)
plt.pause(.1)
for j in range(10000):
stop_watch.click()
# update the artists
ln.set_ydata(np.sin(x + (j / 100) * np.pi))
fr_number.set_text("frame: {j}".format(j=j))
# tell the blitting manager to do its thing
bm.update()
stop_watch.click()
dts[j] = stop_watch.get_dt()*1000.0
# print('dt : {:.3f} ms'.format(stop_watch.get_dt()*1000.0))
print("dt mean {:.3f} ms".format(np.mean(dts)))
print("dt std.dev {:.3f} ms".format(np.std(dts)))
|
cs |
blit으로 여러 그래프 같이 넣기 [3]
hold 도 없고 어떻게 여러개를 넣나 싶었는데 plot을 여러개 넣으면 되는거였다.
레퍼런스 코드는 [3]을 참고한다.
코드의 꺽쇄부분이 실제 그리는 부분이다.
더보기를 누르면 코드를 볼 수 있다.
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
|
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(0, 2 * np.pi, 100)
fig, ax = plt.subplots()
(ln,) = ax.plot(x, np.sin(x), animated=True)
plt.show(block=False)
plt.pause(0.1)
# Save background
bg = fig.canvas.copy_from_bbox(fig.bbox)
ax.draw_artist(ln)
fig.canvas.blit(fig.bbox)
for j in range(100):
fig.canvas.restore_region(bg)
#vvvvvvvvvvvvvvvvvvvvvvv#
ln.set_ydata(np.sin(x + (j / 100) * np.pi))
ax.draw_artist(ln)
#^^^^^^^^^^^^^^^^^^^^^^^#
fig.canvas.blit(fig.bbox)
fig.canvas.flush_events()
|
cs |
내가 수정한 코드는 다음과 같다.
더보기를 누르면 코드를 볼 수 있다.
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
|
import matplotlib.pyplot as plt
import numpy as np
import time
from stop_watch import StopWatch
import pprint
def _fig_config(ax, legends):
ax.set_xlim(0,10)
ax.set_ylim(-5,5)
ax.grid(True)
ax.legend(legends,loc='upper right')
# Data Initial Settings
xs = list()
ys = list()
lines = list()
ys_len = 4
xs.append(0)
for i in range(0,ys_len):
data = {"NAME":"data{}".format(i), "DATA":list()}
ys.append(data)
ys[i]['DATA'].append(0)
# Figure Initial Settings
number_of_figure = 3
cfg = list()
cfg.append(list([0,1,2]))
cfg.append(list([0]))
cfg.append(list([1]))
fig, axs = plt.subplots(number_of_figure,1)
fig.set_figwidth(6)
fig.set_figheight(8)
fig.tight_layout()
figures = list()
for ax_idx, ax in enumerate(axs):
figure = {"AX":ax, "LINE":dict(), "LEGEND":list()}
for data_idx in cfg[ax_idx]:
figure['LINE'][data_idx] = None
figures.append(figure)
# Plot Initialization
for ax_idx in range(len(axs)):
# print('Fig.{} - '.format(ax_idx), figures[ax_idx]['LINE'].keys())
figure = figures[ax_idx]
for data_idx in figure['LINE'].keys():
(line, ) = figure['AX'].plot(xs, ys[data_idx]['DATA'], animated=True)
figure['LINE'][data_idx] = line
figure['LEGEND'].append("data.{}".format(data_idx))
_fig_config(figure['AX'], figure['LEGEND'])
plt.show(block=False)
plt.pause(0.1)
# Preparing data
x = 0
for j in range(5000):
x = x + 0.01
xs.append(x)
for i in range(ys_len):
ys[i]['DATA'].append(float(i+1)*np.sin(x))
# Background Image
bg = fig.canvas.copy_from_bbox(fig.bbox)
for ax_idx in range(len(axs)):
figure = figures[ax_idx]
for line in figure['LINE'].values():
figure['AX'].draw_artist(line)
fig.canvas.blit(fig.bbox)
stop_watch = StopWatch()
stop_watch_data = StopWatch()
# Animate Figures
for j in range(1, 5000):
# Load background
fig.canvas.restore_region(bg)
# Update figure
for ax_idx in range(len(axs)):
figure = figures[ax_idx]
ax = figure['AX']
for data_idx in figure['LINE'].keys():
line = figure['LINE'][data_idx]
line.set_xdata(xs[:j])
line.set_ydata(ys[data_idx]['DATA'][:j])
ax.draw_artist(line)
ax.set_xlim(xs[j]-10, xs[j])
ax.set_ylim(-5, 5)
stop_watch.click()
#
fig.canvas.blit(fig.bbox)
fig.canvas.flush_events()
# Figure가 바로 꺼져서 Pause 화면을 하나 만든다.
# plt.close()는 아예 창을 꺼버린다.
# plt.clf() # 차라리 clear figure가 낫다.
for ax_idx in range(len(axs)):
figure = figures[ax_idx]
ax = figure['AX']
for data_idx in figure['LINE'].keys():
ax.plot(xs,ys[data_idx]['DATA'])
ax.grid(True)
_fig_config(figure['AX'], figure['LEGEND'])
plt.show()
|
cs |
클래스로 만들면 다음과 같다.
더보기를 누르면 코드를 볼 수 있다.
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
|
import matplotlib.pyplot as plt
import numpy as np
import time
class FigureMaker():
def __init__(self):
self._ys_len = 4
self._number_of_figure = 3
self._cfg = list()
self._cfg.append(list([0,1,2]))
self._cfg.append(list([0]))
self._cfg.append(list([1]))
self._figures = list()
self._idx = 0
self._init_data()
self._init_figure()
self._init_plot()
self._init_background()
self._update_before_drawing()
def _init_data(self):
self._xs = list()
self._ys = list()
self._xs.append(0)
for i in range(0,self._ys_len):
data = {"NAME":"data{}".format(i), "DATA":list()}
self._ys.append(data)
self._ys[i]['DATA'].append(0)
def _init_figure(self):
fig, axs = plt.subplots(self._number_of_figure,1)
fig.set_figwidth(6)
fig.set_figheight(8)
fig.tight_layout()
self._fig = fig
self._axs = axs
def _init_plot(self):
for ax_idx, ax in enumerate(self._axs):
figure = {"AX":ax, "LINE":dict(), "LEGEND":list()}
for data_idx in self._cfg[ax_idx]:
figure['LINE'][data_idx] = None
self._figures.append(figure)
def _init_background(self):
# Plot Initialization
for ax_idx in range(len(self._axs)):
# print('Fig.{} - '.format(ax_idx), figures[ax_idx]['LINE'].keys())
figure = self._figures[ax_idx]
for data_idx in figure['LINE'].keys():
(line, ) = figure['AX'].plot(self._xs, self._ys[data_idx]['DATA'], animated=True)
figure['LINE'][data_idx] = line
figure['LEGEND'].append("data.{}".format(data_idx))
self._fig_config(figure['AX'], figure['LEGEND'])
plt.show(block=False)
# Background Image
self._bg = self._fig.canvas.copy_from_bbox(self._fig.bbox)
for ax_idx in range(len(self._axs)):
figure = self._figures[ax_idx]
for line in figure['LINE'].values():
figure['AX'].draw_artist(line)
self._fig.canvas.blit(self._fig.bbox)
def _update_before_drawing(self):
# Preparing data
x = 0
for j in range(10000):
x = x + 0.01
self._xs.append(x)
for i in range(self._ys_len):
self._ys[i]['DATA'].append(float(i+1)*np.sin(x))
def _background_setting(func):
def wrapper(self, *args, **kwargs):
# Load background
self._fig.canvas.restore_region(self._bg)
# Drawing
func(self, *args, **kwargs)
# Save and flush
self._fig.canvas.blit(self._fig.bbox)
self._fig.canvas.flush_events()
return wrapper
@_background_setting
def update(self):
j = self._idx
self._idx += 1
# Update figure
for ax_idx in range(len(self._axs)):
figure = self._figures[ax_idx]
ax = figure['AX']
for data_idx in figure['LINE'].keys():
line = figure['LINE'][data_idx]
line.set_xdata(self._xs[:j])
line.set_ydata(self._ys[data_idx]['DATA'][:j])
ax.draw_artist(line)
ax.set_xlim(self._xs[j]-10, self._xs[j])
ax.set_ylim(-5, 5)
def pause(self):
# Figure가 바로 꺼져서 Pause 화면을 하나 만든다.
# plt.close()는 아예 창을 꺼버린다.
# plt.clf() # 차라리 clear figure가 낫다.
# plt.cla()
j = self._idx
time.sleep(1.0)
for ax_idx in range(len(self._axs)):
figure = self._figures[ax_idx]
ax = figure['AX']
ax.cla()
for data_idx in figure['LINE'].keys():
ax.plot(self._xs[:j], self._ys[data_idx]['DATA'][:j])
ax.grid(True)
self._fig_config(figure['AX'], figure['LEGEND'])
plt.show(block=False)
plt.pause(3)
def _fig_config(self, ax, legends):
j = self._idx
ax.set_xlim(self._xs[j]-10, self._xs[j])
ax.set_ylim(-5,5)
ax.grid(True)
ax.legend(legends,loc='upper right')
if __name__ == '__main__':
plt.close()
blit = FigureMaker()
print("Real-time Drawing")
for i in range(0,1000):
blit.update()
print("Pause")
blit.pause()
print("Real-time Drawing")
for i in range(0,1000):
blit.update()
|
cs |
아쉬운 점은 x, y tick의 라벨을 바꾸기 어렵다는 점이다.
정말 그래프의 선만 바꿀 수 있다.
그리고 hold가 안되서 그리면 grid 가 안쳐진다..
[1] "Faster rendering by using blitting", https://matplotlib.org/stable/tutorials/advanced/blitting.html
[2] "Making Animations Quickly with Matplotlib Blitting" https://alexgude.com/blog/matplotlib-blitting-supernova/
[3] https://matplotlib.org/stable/tutorials/advanced/blitting.html#minimal-example
EOF
'SW' 카테고리의 다른 글
[PyQt5] 새창 띄워서 matplotlib & blit을 이용한 더 빠른 실시간 그래프 그리기 (0) | 2023.08.08 |
---|---|
[PyQt5] 선택한 figure 창에 그래프 그려주는 프로그램 (0) | 2023.08.07 |
[PYTHON] 시간 간격을 알려주는 스탑워치 클래스 만들기 (0) | 2023.08.06 |
[PyQt5] 새 창을 띄워서 matplotlib.animation으로 실시간 그래프 그리기 (0) | 2023.08.06 |
[PyQt5] Color Switch (0) | 2023.08.06 |