
💻 이전 글 참고: https://velog.io/@ckstn0779/Sensors-Accelerometer
저번 시간 글이 너무 길어져서 MPU-6050 센서 모듈의 데이터 출력을 파이썬 그래프로 출력하는 내용을 이어서 작성해보도록 하겠습니다.
파이썬에서 시각화하면 대표적으로 사용되는 라이브러리, Matplotlib에 대해 알아보겠습니다.
Matplotlib는 Python에서 정적, 애니메이션 및 상호작용 방식의 시각화를 생성하기 위한 포괄적인 라이브러리입니다. 데이터 분석가와 과학자들이 가장 널리 사용하는 라이브러리 중 하나이며, MATLAB과 유사한 명령형 인터페이스를 제공하여 사용이 쉽습니다.
💻 공식 문서 참고: https://matplotlib.org/
✍️ 당연히 MATLAB을 사용하고 싶지만... 개인이 사용하기에는 너무 비싸기 때문에... (학생 때 많이 쓰자)
주요 특징 및 사용 목적은 다음과 같습니다.
핵심 모듈은 다음과 같습니다.
Matplotlib는 데이터를 시각화하는 데 있어 계층적인 객체 모델을 따릅니다.
사용자가 plt.plot()와 같은 함수를 호출할 때, Matplotlib는 이 계층 구조에 따라 Figure와 Axes 객체를 자동으로 생성하고 데이터 포인트를 추가합니다.
Matplotlib를 사용하여 간단한 그래프를 그리는 가장 기본적인 방법은 pyplot 모듈을 활용하는 것입니다. 기본적인 꺾은선 그래프(Line Plot)를 만드는 방법을 설명해 드릴게요.
import matplotlib.pyplot as plt
# 1. 데이터 준비
x = [1, 2, 3, 4, 5]
y = [10, 15, 7, 20, 12]
# 2. 그래프 그리기
plt.plot(x, y, label='데이터 시리즈 1', color='red', linestyle='--') # 선 스타일 지정 가능
# 3. 그래프 꾸미기
plt.title("간단한 꺾은선 그래프 예시")
plt.xlabel("시간 (Time)")
plt.ylabel("값 (Value)")
plt.legend() # 범례 표시 (label을 설정했을 때 필요)
plt.grid(True) # 그리드(격자) 표시
# 4. 그래프 출력
plt.show()
실행 결과는 다음과 같습니다. 간단하죠?

참고로, Mac에서 Matplotlib 사용 시 한글 깨짐 문제가 있다면 아래 코드를 실행해 주세요. 이는 Matplotlib의 기본 폰트가 한글을 지원하지 않기 때문입니다.
plt.rcParams['font.family'] = 'AppleGothic' # 맥 기본 한글 서체
plt.rcParams['axes.unicode_minus'] = False # 마이너스 기호 깨짐 방지
이전의 간단한 예시는 pyplot 모듈의 기본 함수들을 사용하여 Figure와 Axes 객체를 자동으로 생성하도록 했기 때문에 명시적으로 Figure와 Axes 객체를 다루는 부분이 없었어요. Matplotlib를 능숙하게 사용하고 복잡한 그래프(예: 여러 개의 서브플롯, 실시간 업데이트)를 만들 때는 이 두 객체를 직접 생성하고 제어해야 합니다.
Matplotlib의 핵심 구조는 Figure (전체 캔버스) 안에 Axes (실제 그림 영역)가 포함되는 계층 구조입니다. 이 객체들을 직접 다루는 것을 객체 지향 인터페이스라고 합니다.
plt.subplots()를 이용한 동시 생성하는 것은 가장 흔하고 효율적인 방법입니다. Figure와 Axes를 한 번에 생성하고, 이를 변수에 할당받아 사용합니다.
import matplotlib.pyplot as plt
# 1. Figure(fig)와 Axes(ax) 객체 생성
# fig: 전체 창 (Figure 객체)
# ax: 그림이 그려지는 영역 (Axes 객체)
fig, ax = plt.subplots()
# 2. Axes 객체를 사용하여 데이터 플롯
x = [1, 2, 3, 4]
y = [10, 5, 20, 15]
ax.plot(x, y, label='단일 축')
# 3. Axes 객체의 메서드를 사용하여 꾸미기
ax.set_title("Axes 객체를 사용한 제어")
ax.set_xlabel("X축")
ax.set_ylabel("Y축")
ax.legend()
plt.show()

이를 이용하면 하나의 창에 여러 개의 그래프를 나란히 표시할 수 있습니다.
import matplotlib.pyplot as plt
import numpy as np
# Figure 하나에 2행 1열의 Axes 2개 생성
fig, (ax_accel, ax_gyro) = plt.subplots(2, 1, sharex=True)
# 1. 가속도 그래프 (ax_accel)
t = np.linspace(0, 10, 100)
ax_accel.plot(t, np.sin(t), color='blue', label='가속도 X')
ax_accel.set_title('가속도계 데이터')
ax_accel.set_ylabel('g')
ax_accel.legend()
# 2. 각속도 그래프 (ax_gyro)
ax_gyro.plot(t, np.cos(t), color='red', label='자이로 Y')
ax_gyro.set_title('자이로스코프 데이터')
ax_gyro.set_xlabel('시간 (s)')
ax_gyro.set_ylabel('deg/s')
ax_gyro.legend()
# Figure 객체를 사용해 전체 창의 레이아웃을 조정
fig.tight_layout()
plt.show()

애니메이션을 사용하지 않고 그래프를 실시간으로 업데이트 시키는 방법이 있습니다.
import matplotlib.pyplot as plt
import numpy as np
import time
plt.rcParams['font.family'] = 'AppleGothic'
plt.rcParams['axes.unicode_minus'] = False
plt.ion() # interactive mode ON
# 1. Figure와 Axes 객체 생성 (객체 지향 인터페이스)
fig, ax = plt.subplots()
# 2. 초기 데이터 및 선 객체 준비
# 데이터 저장 리스트 (최대 100개 포인트만 표시)
x_data = [] # 시간
y_data = [] # 데이터
MAX_POINTS = 100
# 선 객체(line)를 미리 생성하고, 이를 업데이트할 것임
line, = ax.plot(x_data, y_data)
# 초기 축 범위 설정
ax.set_xlim(0, MAX_POINTS)
ax.set_ylim(-1.1, 1.1)
ax.set_title("실시간 그래프 예제: 스크롤링 사인파")
ax.grid(True)
start_time = time.time()
while True:
# 새로운 데이터 생성
t = time.time() - start_time # 0초부터 시작한 시간
new_y = np.sin(t)
# 데이터 추가
x_data.append(t)
y_data.append(new_y)
# MAX_POINTS 넘어가면 자동 스크롤: 오래된 데이터 제거
if len(x_data) > MAX_POINTS:
x_data.pop(0)
y_data.pop(0)
# x축 범위 = 최신 구간만 보이도록 업데이트 (가장 오래된 데이터 ~ 최신 데이터)
# 단, 데이터가 1개인 경우, x축의 시작점과 끝점과 일치해서 그래프를 그릴 수 없어 경고가 발생한다
if len(x_data) > 1:
ax.set_xlim(x_data[0], x_data[-1])
# 선 업데이트
line.set_data(x_data, y_data)
# 화면 갱신
fig.canvas.draw()
fig.canvas.flush_events()
time.sleep(0.02) # 약 50 Hz
코드를 보면 화면에 보여 줄 최대 데이터는 100개로 설정했습니다. 그리고 나서 시간에 따라 x_data, y_data 배열에 데이터를 계속 추가해줍니다. 그리고 그래프(선)을 업데이트, 화면 갱신을 해줍니다. 데이터가 100개가 넘어가면 오래된 데이터를 제거해주고, 새로운 데이터를 추가해줍니다. 큐 구조인거죠. 이렇게 하면 최대 데이터는 100개를 유지한 채로 그래프는 계속 업데이트되면서 실시간 느낌을 줄 수 있겠죠?
해당 코드에서 주요 함수에 대해 설명 드리겠습니다.
실시간 스크롤링 느낌으로 잘 동작합니다.

🎯 참고. ax.set_xlim vs. ax.relim(), ax.autoscale_view()
ax.set_xlim(min, max) 대신 ax.relim(), ax.autoscale_view() 라는 함수가 있길래 사용하면 되지 않을까 했지만 좀 다른 거 같더군요. relim()은 현재 그려진 데이터의 실제 범위를 재계산하는 함수이고, autoscale_view()는 그 계산된 범위를 축에 실제로 적용하는 함수입니다. relim()이 "계산", autoscale_view()가 "적용"이라서 보통 두 개를 세트로 사용합니다.
음... 그니까 relim/autoscale_view 는 데이터 범위를 미리 알 수 없을 때, matplotlib이 모든 데이터를 자동으로 분석해서 최적의 범위를 찾아서 보여줄 때 사용하면 좋을 거 같고요. x축 실시간 스크롤링/고정된 윈도우 크기를 구현하려면 ax.set_xlime 이 맞는거 같네요. (헷갈리네...그냥 무시하십쇼...)
Matplotlib에서 애니메이션을 구현하는 가장 일반적이고 강력한 방법은 matplotlib.animation 모듈의 FuncAnimation 클래스를 사용하는 것입니다. 이 클래스는 정해진 시간 간격(interval)마다 특정 함수를 반복적으로 호출하여 그래프의 데이터를 갱신하고, 이를 통해 움직이는 듯한 효과를 만듭니다.
FuncAnimation은 애니메이션을 구동하는 핵심 클래스이며, 객체를 생성할 때 다음과 같은 필수 인수를 필요로 합니다.
간단한 사인파(Sine Wave) 실시간 애니메이션 코드입니다. 애니메이션을 사용하지 않은 코드랑 거의 유사하죠?
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import numpy as np
import time
plt.rcParams['font.family'] = 'AppleGothic'
plt.rcParams['axes.unicode_minus'] = False
# 1. Figure와 Axes 객체 생성 (객체 지향 인터페이스)
fig, ax = plt.subplots()
# 2. 초기 데이터 및 선 객체 준비
# 데이터 저장 리스트 (최대 50개 포인트만 표시)
x_data = []
y_data = []
MAX_POINTS = 100
# 선 객체(line)를 미리 생성하고, 이를 업데이트할 것임 (고성능 핵심)
line, = ax.plot(x_data, y_data)
# 초기 축 범위 설정
ax.set_ylim(-1.1, 1.1)
ax.set_title("FuncAnimation 예제: 스크롤링 사인파")
ax.grid(True)
start_time = time.time()
# 3. 데이터 갱신 함수 정의 (콜백 함수)
def animate(i):
"""interval마다 호출되어 그래프 데이터를 갱신하는 함수"""
# 새로운 데이터 추가
t = time.time() - start_time
new_y = np.sin(t)
x_data.append(t)
y_data.append(new_y)
# 데이터 포인트 수 제한 (스크롤링 효과)
if len(x_data) > MAX_POINTS:
x_data.pop(0) # 가장 오래된 X 데이터 제거
y_data.pop(0) # 가장 오래된 Y 데이터 제거
line.set_data(x_data, y_data)
# x축 범위 = 최신 구간만 보이도록 업데이트 (가장 오래된 데이터 ~ 최신 데이터)
if len(x_data) > 1:
ax.set_xlim(x_data[0], x_data[-1])
return line, # FuncAnimation에 업데이트된 객체를 반환
# 4. FuncAnimation 객체 생성 및 실행
# interval=50ms (20Hz)
ani = animation.FuncAnimation(
fig,
animate,
interval=50,
blit=True, # 최적화 옵션: 변경된 부분만 다시 그립니다.
cache_frame_data=False # 실시간 데이터 스트리밍 시 필요 없음
)
plt.show()
실시간 그래프에서 성능을 최적화하고 깜빡임 없이 부드러운 애니메이션을 구현하는 핵심은 데이터 갱신입니다.
여기서 한가지 문제가 있다면 x축이 업데이트되지 않는 다른 겁니다. blit=True는 “변경된 부분만 다시 그리기” 기능인데, 축(ax)의 변화는 업데이트 대상에 포함되지 않습니다. 그래서 선은 움직이는데 x축은 그대로인 것처럼 보입니다.
blit=False로 하면 매 프레임 전체를 다시 그려서 ax.set_xlim() 도 적용되어 x축이 자연스럽게 스크롤처럼 움직입니다.

🎯 참고. blit 옵션에 대한 자세한 설명
blit 옵션은 Matplotlib 애니메이션 성능을 크게 좌우하는 중요한 개념입니다. 이 옵션은 배경을 한 번 저장해두고, 업데이트되는 요소(선, 점 등)만 그 위에 다시 그려서 비용을 줄이는 최적화 기법입니다. 전체 화면을 매번 다시 그리는 대신 변경된 영역만 업데이트하므로, 실시간 애니메이션의 성능이 크게 향상됩니다.
위 예시에서는 '선'만 업데이트 되는거죠. 선 전체를 매번 다시 그리지만, 변하지 않는 축/그리드 등은 캐시된 배경 이미지를 재사용해서 빠른 겁니다. (업데이트는 개별 데이터 포인트가 아니라 그래픽 객체 단위로 작동!)
Matplotlib 에 대해 대략적으로 알아봤으니 이제 이를 이용해 측정 데이터를 실시간 시각화를 해보도록 하겠습니다.
#1. 초기 설정 및 라이브러리
시리얼 통신을 위한 라이브러리, matplotlib 라이브러리를 불러옵니다. 시리얼 통신 설정과 연결을 시도합니다. 그래프에 표시할 데이터의 최대 개수 및 데이터 저장소를 만듭니다.
import serial
import time
import re
import matplotlib.pyplot as plt
import matplotlib.animation as animation
# -- 설정값 --
SERIAL_PORT = 'COM4'
BAUD_RATE = 115200
MAX_POINTS = 100 # 그래프에서 표시할 최대 데이터 포인트 수
NUM_AXES = 6 # 가속도 3축 + 자이로 3축
# -- 데이터 저장소 --
xs = [] # 시간축 (X축) 데이터
ys = [[] for _ in range(NUM_AXES)] # 가속도 및 자이로스코프 데이터 (Y축) - 2차원 리스트
# -- 시리얼 포트 초기화
try:
ser = serial.Serial(SERIAL_PORT, BAUD_RATE)
time.sleep(0.5)
ser.reset_input_buffer() # 🔥 기존 입력 버퍼 전체 삭제!
print(f"시리얼 포트 {SERIAL_PORT} 연결 성공.")
except serial.SerialException as e:
print(f"시리얼 포트 연결 실패: {e}")
print("포트 번호가 올바른지, ESP32가 연결되어 있는지 확인하세요.")
exit()
여기서 한 가지 중요한 점은 시리얼 포트는 이전에 ESP32가 출력한 데이터가 PC 버퍼에 그대로 남아 있기 때문에, 파이썬이 실행되면 그 “남아 있던 오래된 데이터”부터 읽어옵니다.
ESP32 재부팅 전/후, 파이썬 코드 실행 전, 출력된 모든 문자열이 Windows의 시리얼 버퍼에 쌓여 있고, 파이썬의 ser.readline()이 그걸 처음부터 차례대로 읽습니다.
따라서 코드에서 시리얼 포트를 연 직후 시리얼 버퍼를 비워주면(flush) 해결됩니다. 안정성을 위해 ESP32와 연결 된 이후 0.5초 정도 기다린 뒤 지워줍니다.
#2. matplotlib 설정, 그래프 틀
# -- matplotlib 설정 --
fig, (ax1, ax2) = plt.subplots(2, 1, sharex=True)
fig.suptitle('MPU-6050 ACC, GYRO')
# 첫 번째 그래프: 가속도 (ACC)
ax1.set_title('ACC (g)')
ax1.set_ylabel('ACC (g)')
lines_accel = [ax1.plot([], [], label=f'ACC {axis}')[0] for axis in ['X', 'Y', 'Z']]
ax1.legend(loc='upper right')
ax1.grid(True)
# 두 번째 그래프: 각속도 (GYRO)
ax2.set_title('GYRO (deg/s)')
ax2.set_xlabel('Time (s)')
ax2.set_ylabel('GYRO (deg/s)')
lines_gyro = [ax2.plot([], [], label=f'GYRO {axis}')[0] for axis in ['X', 'Y', 'Z']]
ax2.legend(loc='upper right')
ax2.grid(True)
#3. 데이터 파싱(data_pattern, read_and_parse_data)
이 부분이 시리얼로 들어오는 텍스트를 숫자 데이터로 변환하는 핵심입니다.
# -- 정규 표현식 패턴 --
# "ACC: X=..., Y=..., Z=... | TEMP: ... | GYRO: X=..., Y=..., Z=..." 형식에서
# 숫자 값만 추출하는 패턴 (온도는 제외)
# 그룹 1~3: ACC X, Y, Z
# 그룹 4~6: GYRO X, Y, Z
data_pattern = re.compile(
r"ACC: X=([\d.-]+), Y=([\d.-]+), Z=([\d.-]+) \| TEMP: [\d.-]+ C \| GYRO: X=([\d.-]+), Y=([\d.-]+), Z=([\d.-]+)"
)
# -- 데이터 읽기 및 파싱 함수
def read_and_parse_data():
try:
# 시리얼 버퍼에서 한 줄 읽기
line = ser.readline().decode('utf-8').strip()
if not line:
return None
# 정규 표현식으로 데이터 추출
match = data_pattern.match(line)
if match:
# 6개 데이터를 float으로 반환하여 리스트로 반환
return [float<(match.group(i)) for i in range(1, NUM_AXES + 1)]
except Exception as e:
print(f"데이터 처리 중 오류: {e}")
return None
#4. 그래프 애니메이션 (animate 함수)
animate 함수는 FuncAnimation에 의해 지정된 간격(예. 100ms)마다 반복 호출되어 그래프를 갱신합니다.
# --- 그래프 업데이트 함수 ---
start_time = time.time()
def animate(i):
global start_time
# 1. 시리얼 데이터 읽기 및 파싱
data_values = read_and_parse_data()
if data_values:
current_time = time.time() - start_time
# 2. 데이터 저장소 업데이트
xs.append(current_time)
for idx in range(NUM_AXES):
ys[idx].append(data_values[idx])
# 데이터 포인트가 MAX_POINTS를 초과하면 가장 오래된 데이터 제거
if len(xs) > MAX_POINTS:
xs.pop(0)
for y_list in ys:
y_list.pop(0)
# 3. 그래프 데이터 업데이트 및 스케일 조정
# 가속도 (ACCEL: ys[0]~ys[2])
for i in range(3):
lines_accel[i].set_data(xs, ys[i])
# 각속도 (GYRO: ys[3]~ys[5])
for i in range(3):
lines_gyro[i].set_data(xs, ys[i+3])
# X축 스케일 조정
ax1.set_xlim(xs[0], xs[-1] if xs else 1)
# Y축 스케일 조정 (데이터에 맞춰 동적 조정)
if ys[0]:
all_ys = [val for y_list in ys for val in y_list]
min_y = min(all_ys) - 0.1
max_y = max(all_ys) + 0.1
# 가속도 축 (ax1)
ax1.set_ylim(min_y, max_y)
# 각속도 축 (ax2)
ax2.set_ylim(min_y, max_y)
# 🔥 blit=True 를 위한 필수 return
return lines_accel + lines_gyro
#5. 애니메이션 시작
ESP32에서 출력하는 데이터 속도와 맞추면 됩니다. 처음에는 100Hz(1초에 100개)를 시도했다가 열리지도 않았던가...? 그래서 10Hz로 바꿨습니다.
# --- 애니메이션 시작 ---
# interval=100: 100ms마다 animate 함수 호출 시도 (10Hz 목표)
ani = animation.FuncAnimation(
fig,
animate,
interval=100,
blit=True,
cache_frame_data=False
)
plt.show()
# --- 종료 시 시리얼 포트 닫기 ---
ser.close()
MPU6050을 여러 방향으로 뒤집어보면 가속도와 각속도가 (거의) 실시간으로 변하는 것을 알 수 있습니다. 다만 시간이 가면 갈수록 반응이 늦어져서 실시간 느낌이 나지 않습니다.

1초에 10개 출력를 출력하는 대신, 5개만 출력하고 animation interval를 200으로 늘려도 지연이 생기는건 여전하고요. Blit=True 설정해도 느리긴 마찬가지였습니다. (그나마 나은거 같기도 하고요. 비록 x축 값이 변하진 않지만요.)
문제의 원인이 뭘까요? blit=True를 적용해도 매 프레임미다 그래프는 다시 그려야 하고요. FuncAnimation 내부에서 결국에는 시리얼 읽고, 파싱하고, 그래프 렌더까지 모두 해야 하기 때문에 렌더가 느려지면 readline()도 밀리고 딜레이가 발생합니다.
matplotlib은 실시간 스트리밍에 적합한 라이브러리는 아니라고 하네요. matplotlib 대신 pyqtgraph를 사용한다면 30~100배 더 빠르다고 합니다.
🤔 matplotlib 대신 pyqtgraph를 사용하면 왜 더 빠를까요?
아... 목적 자체가 달라서 최적화를 하는 방향도 다른거네요.
"Scientific Graphics and GUI Library for Python" - 공식 문서 참고
PyQtGraph는 Python에서 빠르고 강력한 데이터 시각화 및 과학/공학용 그래픽을 위해 설계된 라이브러리입니다. 특히 대용량 데이터를 실시간으로 플로팅(Plotting)하거나, 복잡한 신호 처리 결과를 인터랙티브하게 보여줄 때 주로 사용됩니다.
PyQtGraph는 다음과 같은 특징 덕분에 과학 및 엔지니어링 분야에서 Matplotlib 대신 실시간 데이터 처리에 자주 활용됩니다.
이 두개를 간단하게 표로 비교해보면 다음과 같습니다.
| 특징 | Matplotlib | PyQtGraph |
|---|---|---|
| 주요 목적 | 정적, 출판용 고품질 그래프 생성 | 고성능, 실시간, 과학/엔지니어링 데이터 시각화 |
| 속도 | 느림 (특히 실시간 데이터 갱신 시) | 매우 빠름 (OpenGL 가속) |
| 기반 프레임워크 | 다양한 백엔드 지원 (Qt, Tkinter 등) | Qt (PyQt/PySide) 전용 |
| 상호작용성 | 기본 지원 기능이 적고 느림 | 매우 부드러운 확대/축소 및 이동 기본 지원 |
| 개발 난이도 | 간단한 플롯은 쉬움 | GUI 프레임워크(Qt)를 이해해야 함 |
데이터의 양이 많거나, 갱신 주기가 빠르거나(예: 100Hz 이상의 센서 데이터), 사용자가 그래프를 인터랙티브하게 조작해야 하는 환경이라면 PyQtGraph가 Matplotlib보다 훨씬 유리합니다.
PyQt와 PySide는 Python 언어를 사용하여 그래픽 사용자 인터페이스 (GUI) 기반의 데스크톱 애플리케이션을 개발할 수 있도록 해주는 프레임워크입니다. 두 프레임워크 모두 강력하고 크로스 플랫폼을 지원하는 GUI 툴킷인 Qt 라이브러리를 Python에서 사용할 수 있도록 래핑(Wrapping)한 것입니다.
Qt의 역할과 두 프레임워크의 관계는 다음과 같습니다.
기능적인 측면에서는 두 프레임워크 모두 Qt의 기능을 거의 완벽하게 지원하므로 큰 차이가 없습니다. 하지만 법적인 측면에서 중요한 차이가 있습니다.
PyQt/PySide를 사용하여 GUI 애플리케이션을 개발할 때 다루는 핵심 개념은 다음과 같습니다.
PyQtGraph와의 관계
PyQtGraph는 이름에서 알 수 있듯이, 이 PyQt/PySide (Qt 프레임워크) 위에서 구동되도록 설계된 라이브러리입니다.
저는 PyQt6 + PyQtGraph 조합을 사용해보도록 하겠습니다. 사용하기 전에 설치부터 해봅시다.
pip install pyqt6 pyqtgraph
주의할 점으로, 현시점 기준으로 최신 pyqt6은 python 3.13 버전까지만 지원합니다. 저는 python 3.14를 설치했는데... 귀찮게 python 3.13을 하나 더 설치해줍니다. ㅠㅠ
📌 예제 1 - 단일 그래프 한개 띄우기
PyQt6로 GUI 윈도우를 만들고, PyQtGraph의 PlotWidget을 중앙에 배치해서 그래프를 표시하는 코드입니다.
import sys
from PyQt6.QtWidgets import QApplication, QMainWindow
import pyqtgraph as pg
class Window(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("PyQtGraph + PyQt6 Example")
# 플롯 위젯 생성
plot_widget = pg.PlotWidget()
# 그래프 데이터 작성
x = [1, 2, 3, 4, 5]
y = [10, 20, 15, 30, 25]
plot_widget.plot(x, y, pen='r')
# 메인 윈도우에 플롯 위젯 배치
self.setCentralWidget(plot_widget)
app = QApplication(sys.argv)
win = Window()
win.show()
sys.exit(app.exec())
Window 클래스에서 x, y 데이터를 빨간색 선(pen='r')으로 그린 후, QApplication으로 GUI 앱을 실행합니다. PyQt의 위젯 시스템과 통합되어 더 복잡한 GUI 애플리케이션을 만들 수 있습니다.

참고로, QMainWindow은 필수는 아니고요. 간단한 그래프만 표시하려면 QWidget을 사용하거나, PlotWidget으로 직접 표시할 수도 있습니다.
import sys
from PyQt6.QtWidgets import QApplication
import pyqtgraph as pg
app = QApplication(sys.argv)
# 플롯 위젯 생성 및 설정
plot_widget = pg.PlotWidget()
plot_widget.setWindowTitle("PyQtGraph + PyQt6 Example")
# 그래프 데이터 작성
x = [1, 2, 3, 4, 5]
y = [10, 20, 15, 30, 25]
plot_widget.plot(x, y, pen='r')
# 위젯 표시
plot_widget.show()
sys.exit(app.exec())
QWidget은 PyQt의 가장 기본적인 UI 요소로, 모든 위젯의 부모 클래스입니다. 버튼, 텍스트박스, 레이블 등 모든 UI 요소가 QWidget을 상속받습니다.
PlotWidget은 QWidget을 상속받아 그래프 그리기 기능이 추가된 특수 위젯입니다. pyqtgraph 라이브러리에서 제공하는 것으로, 내부적으로 OpenGL 기반 플로팅 기능이 구현되어 있습니다.
📌 예제 2 - 단일 그래프 한개 띄우기
QVBoxLayout을 사용해서 2개의 PlotWidget을 수직으로 배치하는 코드입니다. 첫 번째는 빨간색, 두 번째는 파란색 선으로 표시됩니다.
import sys
from PyQt6.QtWidgets import QApplication, QWidget, QVBoxLayout
import pyqtgraph as pg
app = QApplication(sys.argv)
# 메인 윈도우 생성
window = QWidget()
window.setWindowTitle("PyQtGraph + PyQt6 Example")
layout = QVBoxLayout()
# 첫 번째 그래프
plot_widget1 = pg.PlotWidget()
x1 = [1, 2, 3, 4, 5]
y1 = [10, 20, 15, 30, 25]
plot_widget1.plot(x1, y1, pen='r')
layout.addWidget(plot_widget1)
# 두 번째 그래프
plot_widget2 = pg.PlotWidget()
x2 = [1, 2, 3, 4, 5]
y2 = [5, 15, 10, 25, 20]
plot_widget2.plot(x2, y2, pen='b')
layout.addWidget(plot_widget2)
# 레이아웃 설정 및 표시
window.setLayout(layout)
window.show()
sys.exit(app.exec())
QVBoxLayout + PlotWidget 방식은 Qt 위젯 레이아웃을 직접 관리하는 일반적인 GUI 구성 방식입니다. PyQtGraph의 PlotWidget을 일반 Qt 위젯처럼 사용합니다.
장점으로는 버튼, 라벨, 슬라이더 같은 다른 위젯과 섞어 쓰기 쉽습니다.

GraphicsLayoutWidget을 사용하면 더 간단하고 성능도 좋습니다! PyQt 레이아웃 없이 PyQtGraph 자체 레이아웃 시스템을 사용합니다.
import sys
from PyQt6.QtWidgets import QApplication
import pyqtgraph as pg
app = QApplication(sys.argv)
# GraphicsLayoutWidget 생성
win = pg.GraphicsLayoutWidget()
win.setWindowTitle("PyQtGraph + PyQt6 Example")
# 첫 번째 그래프 (0행 0열)
plot1 = win.addPlot(row=0, col=0)
x1 = [1, 2, 3, 4, 5]
y1 = [10, 20, 15, 30, 25]
plot1.plot(x1, y1, pen='r')
# 두 번째 그래프 (1행 0열)
plot2 = win.addPlot(row=1, col=0)
x2 = [1, 2, 3, 4, 5]
y2 = [5, 15, 10, 25, 20]
plot2.plot(x2, y2, pen='b')
win.show()
sys.exit(app.exec())
GraphicsLayoutWidget은 그리드 기반이라 row, col로 위치를 지정할 수 있고, 모든 플롯이 하나의 OpenGL 컨텍스트를 공유해서 더 빠릅니다. (확실히 더 깔끔한거 같네요)

📌 예제 3 - 실시간 그래프 (QTimer)
QTimer를 사용한 실시간 업데이트 예제입니다. QTimer가 20ms마다 update() 함수를 호출해서 사인파 데이터를 실시간으로 추가하고 그래프를 업데이트합니다.
import sys
from PyQt6.QtWidgets import QApplication
from PyQt6.QtCore import QTimer
import pyqtgraph as pg
import numpy as np
app = QApplication(sys.argv)
# 플롯 위젯 생성 및 설정
plot_widget = pg.PlotWidget()
plot_widget.setWindowTitle("PyQtGraph 실시간 예제")
plot_widget.setYRange(-1.5, 1.5)
# 초기 데이터
x_data = []
y_data = []
MAX_POINTS = 100
start_time = 0
# 선 객체 생성
line = plot_widget.plot(x_data, y_data, pen='r')
# 업데이트 함수
def update():
global start_time
# 새 데이터 생성
t = start_time
new_y = np.sin(t)
x_data.append(t)
y_data.append(new_y)
# 최대 포인트 수 제한
if len(x_data) > MAX_POINTS:
x_data.pop(0)
y_data.pop(0)
# 선 업데이트
line.setData(x_data, y_data)
start_time += 0.1
# QTimer 설정 (20ms마다 업데이트 = 50Hz)
timer = QTimer()
timer.timeout.connect(update)
timer.start(20)
# 위젯 표시
plot_widget.show()
sys.exit(app.exec())
PyQtGraph는 Matplotlib의 FuncAnimation처럼 interval을 사용하는 것이 아니라, Qt 프레임워크의 QTimer를 사용하여 주기적으로 데이터를 읽고 setData()로 갱신하는 방식을 주로 사용합니다.
코드는 Matplotlib을 사용했을 때와 비슷하죠? 구조가 되게 비슷해서 쉽게 교체할 수 있을 겁니다.

자, Matplotlib을 사용했던 코드를 PyQtGraph + PyQt6 을 사용하도록 수정해봅시다. 구조는 비슷해서 수정하기 간단합니다. 생략된 부분은 이전 코드를 참고하십쇼.
# PyQtGraph version of MPU6050 real-time visualizer
# COM4 115200, parses ACC X/Y/Z and GYRO X/Y/Z
import sys
import serial
import time
import re
from PyQt6.QtWidgets import QApplication
from pyqtgraph.Qt import QtCore
import pyqtgraph as pg
# 초기 설정 값
# ...
# 데이터 저장소
# ...
# 시리얼 포트 초기화
# ...
# 데이터 파싱(data_pattern, read_and_parse_data)
# ...
# GUI
app = QApplication(sys.argv)
win = pg.GraphicsLayoutWidget(show=True, title="MPU6050 ACC / GYRO Real-time Plot")
win.resize(900, 600)
# ACC plot
p1 = win.addPlot(title="ACC (g)")
p1.addLegend()
acc_colors = ['r', 'g', 'b']
acc_labels = ['ACC X', 'ACC Y', 'ACC Z']
acc_lines = [
p1.plot(pen=pg.mkPen(color=acc_colors[i], width=2), name=acc_labels[i])
for i in range(3)
]
win.nextRow()
# GYRO plot
p2 = win.addPlot(title="GYRO (deg/s)")
p2.addLegend()
gyro_colors = ['c', 'm', 'y']
gyro_labels = ['GYRO X', 'GYRO Y', 'GYRO Z']
gyro_lines = [
p2.plot(pen=pg.mkPen(color=gyro_colors[i], width=2), name=gyro_labels[i])
for i in range(3)
]
# --- 그래프 업데이트 함수
start_time = time.time()
def update():
global start_time
# 시리얼 데이터 읽기 및 파싱
data_values = read_and_parse_data()
if not data_values:
return
t = time.time() - start_time
xs.append(t)
for i in range(NUM_AXES):
ys[i].append(data_values[i])
# Trim
if len(xs) > MAX_POINTS:
xs.pop(0)
for y_list in ys:
y_list.pop(0)
# Update plots
for i in range(3):
acc_lines[i].setData(xs, ys[i])
for i in range(3):
gyro_lines[i].setData(xs, ys[i+3])
# Timer event (10Hz = 100ms)
timer = QtCore.QTimer()
timer.timeout.connect(update)
timer.start(200)
# Exit
if __name__ == '__main__':
sys.exit(app.exec())
실행 결과는 다음과 같습니다. 오... 좋습니다.

다만, 처음에는 역시 빠르지만 시간이 지나면 지연이 생기는 군요. 그래도 matplotlib 보다는 빠른 편입니다.
MPU6050으로 데이터를 받아서 실시간 그래프 그리는 것까지 해봤습니다. 앞으로 어떤 걸 해볼까요?
⭐ 1단계: 데이터 처리(필터링)
센서값은 노이즈가 많기 때문에 필터를 적용하면 훨씬 안정적인 동작이 가능합니다.
⭐ 2단계: 센서 융합(각도/기울기 추정)
가속도 + 자이로를 조합하면 기울기(roll/pitch) 를 정확하게 계산할 수 있습니다.
⭐ 3단계: 특성 분석 & 이벤트 감지
센서 값 패턴을 분석해서 특정 “동작”을 인식하는 프로젝트입니다. (AI야~ 해줘~...ㅠ)
⭐ 4단계: 고급 프로젝트(재미 최고)
여기부터는 실전 느낌이 강한 프로젝트들입니다.
일단은 “측정 → 처리 → 해석 → 제어” 단계로 확장하는 과정이 필요할 거 같네요.
먼저 해볼 건, Complementary Filter로 Pitch/Roll 계산. 즉, 기울기를 계산해보는 걸 해봐야 될 거 같습니다.