반응형

다음 글에 이어서 연재하는 글이다.

[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(02 * np.pi, 100)
    (ln,) = ax.plot(x, np.sin(x), animated=True)
    # add a frame number
    fr_number = ax.annotate(
        "0",
        (01),
        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
 
= np.linspace(02 * 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
= 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(15000):
    # 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(-55)
    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]-10self._xs[j])
            ax.set_ylim(-55)
 
    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]-10self._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

728x90

+ Recent posts