본문 바로가기

InfoGraph/파이썬 matplotlib 애니메이션

02. [예제]사인 곡선(사인 그래프, sine graph) 그리기

반응형

 

 

첫 번째 예제이기에, 상세히 코드를 설명할 것이고, 작성된 코드를 이용해서 mp4 및 gif로 만드는 방법도 소개할 것이다.  이다음 예제부터는 애니메이션 핵심 코드를 중심으로만 설명할 예정.

사인 곡선을 그리는 애니메이션을 만들어볼 것이다. 

 

그런데, 애니메이션 코드로 바로 들어가기 전에, 일반적인 정지되어 있는 사인 곡선을 먼저 그려보겠다. 그것이 애니메이션 코드를 바로 소개하는 것보다, 이해가 더 잘될 거 같다. 

 

사인 곡선을 그리는 코드의 핵심은, 1)$x$에 해당하는 값들을 생성하고, 2) 그 $x$ 값들의 sine값을 구해서 $y$에 저장하고, 3) plot 하는 것.

 

1) x = np.linspace(0, 2*np.pi, 100) 

2) y = np.sin(x)

3) plt.plot(x,y)

 

주피터 노트북에서 작성된 실제 코드는 아래와 같다. 설명이 필요한 부분은 소스 코드 옆에 주석을 달아놨으니, 그것을 참조하면 쉽게 이해될 수 있을 것이다.

 

import numpy as np
import matplotlib.pyplot as plt

fig, ax = plt.subplots(figsize=(10,6))  # subplots를 호출하면서, 그림 크기 지정도 가능함 
ax.set_xlim(0, 2*np.pi) # x축 범위
ax.set_ylim(-1.2, 1.2)  # y축 범위

# x축의 눈금을 라이언 형태로 표출하기 위한 기법임. 이것을 지정하지 않으면 그냥 실수값으로 나옴
ax.set_xticks([0,np.pi/2,np.pi,(3*np.pi)/2,2*np.pi])
ax.set_xticklabels(['0',r'$\dfrac{\pi}{2}$',r'$\pi$',r'$\dfrac{3\pi}{2}$',r'$2\pi$']) #dfrac에 유의. 폰트가 큰 분수

ax.grid(True) # 그래프의 눈금 격자 표시

x = np.linspace(0, 2*np.pi, 100) # [0,2PI] 사이 수 100개 생성. 0과 2PI 포함
y = np.sin(x)  # 여기서 x는 numpy의 array이고, np.sin(x)는 이 x 배열값 각각에 대한 값 계산

plt.plot(x,y) # 디폴트 라인을 가지고 (x,y)에 대한 라인그래프를 그림
plt.show()

 

위 코드가 실행되는 모습은,

 

 

 


이제 위와 같은 정지 그래프를, 다이내믹하게 그래프가 그려지는 동영상을 만들어보자.

matplotlib.animation 패키지의 FuncAnimation이라는 함수를 이용할 것이다. 

 

matplotlib의 FuncAnimation애니메이션 작성방법 요약

matplotlib의 animation.FuncAnimation을 이용해서 동영상을 작성할 건데, 이의 사용법을 요약하면 다음과 같다.

 

  1. fig 생성: plt의 ax 및 fig 생성
  2. init 함수 작성: 동영상으로 만들 그래프의 초기값 세팅을 하는 init 함수 작성
  3. update 함수 작성: 실제 동영상이 될 그래프의 변환 루틴이 들어가는 update 함수 작성
  4. FuncAnimation 호출: 위 1 ~ 3에서 작성한 것 및 다른 파라미터를 지정해서 FuncAnimation 함수 호출

위의 순서대로 작성해 보자.

 

먼저 필요한 패키지들은 아래와 같이 import 한다.

import numpy as np
import matplotlib.pyplot as plt

from matplotlib.animation import FuncAnimation
from matplotlib import rc

1. fig 생성

fig, ax = plt.subplots(figsize=(10,6))
ax.grid()

x, y = [], []  # x,y 값을 저장할 리스트 미리 생성. 각 함수에서 사용할 것이기에 이렇게 함수 밖에서 생성
line, = plt.plot([], [], )  # 비어 있는 데이터를 이용해서 plot을 해서, 그래프의 원형 생성.

코드에서 x, y를 미리 만들어두고, 비어있는 데이터를 이용해서 plot을 하는 것이 특징이다.

``plt.plot( )`` 을 하게 되면 그 리턴 값으로 ``.Line2D`` 객체들이 리스트 형태로 리턴된다. 이 좌푯값들을 이용해서 실제 화면에 라인들이 그려지게 되는 거다. 여기서는 리턴되는 Line2D 객체 중 제일 첫 번째 것을 ``line`` 변수로 받은 것이다. 

 

즉, ``line, = plt.plot([], [],)``은 `` line = plt.plot([], [],)[0]``과 같은 표현이다. 생성된 Line2D 리스트 중에서 첫 번째 리스트를 line변수에 할당한 것.

 

나중에 이 line변수의 값을 변경시키면, 그 변경된 값으로 화면에 표출되는 것이다. 이러한 변경은 ``line.set_data( )``에 의해 이루어지고, 이러한 호출이 ``update``함수에서 이루어질 것이다.

 

이 처럼 plt.plot()의 리턴값을 line으로 받고, 이 line의 값이 바뀌면 화면상의 그래프가 바뀌는 방식이 좀 낯설 수 있다.
다음과 같이 생각하면 이해가 잘 될 것이다.

- plt.plot()에 의해서 그래프로 그릴 '라인 객체'들이 생성되고, line변수는 그 객체들의 주소를 가리키는 것이 됨
- 따라서, line 변수의 값을 바꾸면(=line변수가 가리키는 값들을 바꾸면), 그래프가 그려질 때 '그 곳'에 있는 객체를 참조해서 그리기에, 바뀐 데이터를 기준으로해서 그래프가 그려짐

 

2. init 함수 작성

def init():    
    ax.set_xlim(0, 4*np.pi)  # x축 범위 설정
    ax.set_ylim(-1.2, 1.2)  # y축 범위 설정
    
    # X축 단위 표시를 pi 단위로 표시
    ax.set_xticks([0,np.pi,2* np.pi,3*np.pi,4*np.pi])
    ax.set_xticklabels(['0',r'$\pi$',r'$2\pi$',r'$3\pi$',r'$4\pi$']) 
          
    return line,    

init함수에는, 그래프를 그리기 위한 초기 설정값들을 넣으면된다. 축의 범위(최소~최대), 축 눈금 단위 등

 

위 예제에서는 x축 단위를 $\pi$로 하기 위해서, 수동으로 눈금 표시를 하고(set_xticks( ) 이용), 수동으로 눈금마다의 라벨을 달았다. 

 

함수의 마지막 줄에는, 그래프에서 그릴 line 객체를 리턴했다. 그냥 line이 아니고 콤마가 포함된 'line , '임에 유의한다. 이 init함수가 animation.FuncAnimation함수의 'init_func'에 사용될 것이고, 이 'init_func'이 'iterable'한 리턴 값이 요구되기 때문이다. 쉽게 얘기하면 tuple이나 list 타입으로 리턴해야 한다는 것이다. 해서 "line"이 아니라 "line ,"과 같이 리턴한 것이다. 비록 line 하나만 사용될 것이지만.

 

3. update 함수 작성

def update(i):    
    x.append(i)    
    y = np.sin(x) 
    
    line.set_data(x, y)
    
    return line,

update는, 동영상에서 반복되서 실행되는 함수라고 생각하면 된다. 

 

FuncAnimation 함수에서 (interval=100, frames=range(0,10))으로 지정했다면, 100ms마다 한 번씩 10번 update 함수가 호출되고, 이 update에서 바뀐 내용(line 객체의 내용이 바뀌는 것)이 한 화면이 돼서, 전체 동영상이 된다.

100ms 마다 정지영상이 하나씩 생성되고, 이러한 영상이 10개 합쳐져서, 1초(=1000ms) 짜리 동영상이 되는 거다.

그리고, update의 파라미터 i로는, frames의 0~9까지의 값이 차례로 전달될 것이다.

 

실제로, 이 예제에서 사용할 FuncAnimation 코드는 아래와 같다.

ani = FuncAnimation(fig=fig, func=update, frames=np.linspace(0, 4*np.pi, 100),
                    init_func=init, interval=40, blit=True)

 

frames은 0에서 $4\pi$까지 100개의 값으로 구성된 리스트이고, 이 값들이 update함수가 실행될 때마다 차례로 전달될 것이다. 그리고 update함수의 실행 간격은 40ms이고.(왜냐면 interval=40)

 

따라서, 동영상의 전체 수행시간은 40ms * 100 = 4000ms = 4초가 될 것이다.

 


위 예제의 update 함수에는, i값으로 0~$4\pi$까지의 100개 값이 차례로 전달될 것이다. 그리고 이렇게 전해진 $i$값들이 차곡차곡 리스트 $x$에 들어갈 것이다. 그러고 나서 지금까지 들어간 $x$값들에 대해서 y = np.sin(x) 코드에 의해 사인 곡선 값이 되는 $y$가 생성될 것이다.

 

이렇게 바뀐 y값으로 line 값을 업데이트해주면, 사이 곡선이 서서히 그려지는 애니메이션이 된다. 

 

즉, 이 update 함수는 40ms 마다 실행될 것이고, 그때마다 조금씩 더 자라는 사인 곡선이 line값으로 지정되고, 결과적으로 사인 곡선이 그려지게 되는 것이다.

 

이 update 함수의 리턴값도 init 함수와 마찬가지로 iterable 한 값이어야 하기에 콤마(,)를 붙여줘서 튜플 형태가 되게 리턴해준다. 비록 지금은 하나의 line객체가 사용되지만 튜플 형태로 리턴해주는 것이다.

 

4. FuncAnimation 호출

ani = FuncAnimation(fig=fig, func=update, frames=np.linspace(0, 4*np.pi, 100),
                    init_func=init, interval=40, blit=True)

FuncAnimation 함수를 호출하는 한 줄짜리 코드로 동영상이 완성된다. 위 쪽에서 설정했던 fig, init, update 함수는 모두 이 FuncAnimation 함수를 호출하기 위한 준비과정이었다.

 

FuncAnimation함수를 이해하려면,  사용되는 파라미터들을 알면 된다.

 

fig matplotlib의 Figure 객체를 지정해준다. 코드의 앞 부분에서 plt.subplots 함수를 이용해서 Figure 객체를 만들고, 이 Figure의 크기, 축 범위, 눈금 등을 지정한 후, fig에다가 그 객체를 지정해주면 된다.
func 위에서 만들어진 update함수를 지정하면된다. frames에 지정한 횟수만큼 이 update가 실행될 것이다. 

이 func의 인자(argument)로는 frames의 next 값이 전달된다. 만약 frames=raange(0,10)이면 0에서 9까지의 값이 차례로 func가 호출될 때마다 첫번 째 인자로 전달된다. func에 추가적인 인자를 전달하고자한다면 'fargs' 파라미터를 사용하면 된다.
frames func에 전달될 인자를 지정한다. 
쉽게 얘기하면, 이 frames안에 들어 있는 값 개수 만큼 func가 실행되고, 그 func의 첫번 째 인자로 frames의 값들이 차례로 전달되는 것이다.

frames의 값으로는
  - 리스트 같은 iterable 객체가 주어지는 것이 일반적이고, 
  - a라는 int 값으로 주어지면 range(a)와 같은 효과이고
  - generator function으로 주어질 수도 있고(다른 예제에서 설명하겠다.)
  - call 메서드를 가지고 있는 클래스가 주어질 수도 있다.
init_func 동영상의 맨 처음 프레임을 만들 때 호출되는 함수이다. 이 함수의 리턴값에 의해 첫번째 프레임의 그래프가 만들어진다.
만약 init_func가 지정되지 않으면, func의 첫번째 프레임이 동영상의 첫번째 프레임이 된다.
interval 프레임간의 간격. 단위는 ms. 디폴트 값은 200ms
여기에 지정된 시간 간격마다 func 함수가 호출되어 하나의 영상프레임이 만들어진다.

따라서, 전체 동영상 재생 시간은 interval * len(frames) 
blit 디폴트값은 False
만약 True로하면, 그래프의 축이나 격자, 백그라운드는 변하지 않고, 실제 그래프만 변하게 됨. 따라서 애니메이션 생성 시간이 짧음.

False로 하면, 그래프의 모든 부분을 다시 그리는 것이기에 애니메이션 생성시간이 길어짐

 

FuncAnimation 함수에 의해 애니메이션 객체가 만들어지고(위 예제에서는 이를 'ani' 변수로 받았다.), 이 객체를 이용해서 mp4로 만들어 재생시키거나 gif로 만들면 되는 것이다.


만들어진 애니메이션 객체를 출력하는 2가지 방법을 설명하겠다. (다른 방법도 있는데, 얘기가 길어지니, 여기서는 꼭 필요한 2가지만)

 

하나는 Jupyter Notebook에서 볼 수 있는 html5 형태와 gif로 만드는 방법

 

1. Jupyter Notebook에서 볼 수 있는 html5 형태로 변환

 

이 방법을 사용하기 위해서는, 미리 ffmpeg이 설치되어 있어야 한다. 안되어 있다면, 앞 글에 가이드되어 있는 방법으로 ffmpeg를 설치하거나(ImageMagic을 설치하면서 같이 깔게 했다.), 아니면 직접 ffmpeg 사이트에 가서 다운로드하여 설치하면 된다. 

 

 

이 방법은 간단하다. 아래와 같은 코드를 치면 끝이다.

rc('animation', html='html5')
ani

여기서 ani는 FuncAnimation을 실행해서 얻은 값이다. 

 

Jupyter Notebook에서 이렇게 코드를 치고 실행하면 몇 초 후에 애니메이션이 실행된다.

 

혹은 위와 같이 rc 함수를 사용하지 않고, 아래와 같이 해도 똑같이 실행된다.

from IPython.display import HTML

HTML(ani.to_html5_video())

 

실행되는 애니메이션을 mp4로 저장도 가능한데, 이는 실행되는 애니메이션 위에 마우스 커서를 위치하고 마우스 우클릭해서 "동영상을 다른 이름으로 저장' 메뉴를 선택한 후, mp4로 저장하면 된다.

 

 

2. gif로 변환

gif 파일로도 만들 수 있다. 

 

이를 위해서는 ImageMagic이라는 프로그램을 미리 깔아야 한다. (이에 대한 설치방법은 앞 글에서 설명했다. )

 

  앞 글: 01. matplotlib를 이용해서 애니메이션 그래프(Animation Graph) 만들기

 

ani = FuncAnimation(fig=fig, func=update, frames=np.linspace(0, 4*np.pi, 100),
                    init_func=init, interval=40, blit=True)

ani.save('a.gif', writer='imagemagick', fps=25, dpi=100)

 

gif 파일을 만들 때 주의할 점은 fps값을 지정할 때이다. FuncAnimation에서 만든 프레임들을 가지고 gif를 만들기에, FuncAnimation에 의해 만들어진 프레임들이 몇 초마다 한 개씩인지를 잘 봐야 한다. 

 

위 예제에서는 interval=40ms로 했기에, 1초에 25개 프레임이 만들어진다. (1000ms/40ms = 25개)

따라서, gif를 만들 때 fps=25로 해야, FuncAnimaion에서 의도한 시간길이 만큼의 gif가 만들어진다. 예제에서 FuncAnimation에 의한 동영상은 4초짜리였고, gif에서도 fps를 똑같이 맞춰놨기에 4초짜리 gif가 만들어질 것이다.

 

그런데 만약, fps=50으로 한다면, 2초짜리 gif가 만들어질 것이고, fps=12나 13정도로 하면 8초 정도 되는 gif가 만들어진다. 

 


위 설명에 사용된 전체 소스코드 및 실행되는 모습은 아래와 같다.

 

import numpy as np
import matplotlib.pyplot as plt

from matplotlib.animation import FuncAnimation
from matplotlib import rc

fig, ax = plt.subplots(figsize=(10,6))
ax.grid()

x, y = [], []
line, = plt.plot([], [], )

def init():    
    ax.set_xlim(0, 4*np.pi)
    ax.set_ylim(-1.2, 1.2)
    
    # X축 단위 표시를 pi 단위로 표시
    ax.set_xticks([0,np.pi,2* np.pi,3*np.pi,4*np.pi])
    ax.set_xticklabels(['0',r'$\pi$',r'$2\pi$',r'$3\pi$',r'$4\pi$']) 
          
    return line,

def update(i):    
    x.append(i)    
    y = np.sin(x) 
    
    line.set_data(x, y)
    
    return line,

ani = FuncAnimation(fig=fig, func=update, frames=np.linspace(0, 4*np.pi, 200),
                    init_func=init, interval=20, blit=True)

rc('animation', html='html5')
ani

 

-끝-

반응형