音響信号ソフトウェアでは、録音している音信号の大きさやピッチ(基本周波数)といった情報をリアルタイムで表示したいことが多々あります。
ところで、Pythonではグラフを表示するようなGUIのライブラリの選択肢はあまり多くありません。
そこで、今回はグラフライブラリとして最もメジャーであるmatplotlib
を使ってリアルタイムで折れ線グラフをプロットしてみました、
matplotlib でリアルタイムプロットのポイント
方針としては、
- 信号データが更新されるたびに、グラフ全体を新たに描画しなおす
- 信号データが更新されるたびに、プロットするデータのみ入れ替える
という2つがあります。
今回は、より効率的だと思われる後者の方法を採用しました
参考:Matplotlib でプロットの更新を自動化する方法
この方法での実装の流れは以下の通りです。
plt.ion()
を宣言し、インタラクティブモードにする- ベースとなるプロットを行う
line = plt.plot(xdata, ydata)
- 最初に取得した
Line2D
オブジェクトline
より、新しい配列をセットするline[0].set_ydata(ydata_new)
plt.pause(time)
で再描画- 3~4 を繰り返す
Interactive Figures — Matplotlib 3.4.2 documentation
なお、matplotlib
でリアルタイムプロットをする場合、いくつか注意点があります
。
- プロットの更新間隔(FPS)は10 ~30 Hz 程度が限界
- 更新タイミングは不安定・かなりバラつく
- なるべくプロット処理を軽くする方がよい
- プロットする配列の要素数を減らす
plt.figure(figsize=figsize, dpi=dpi)
でfigsize
やdpi
を小さくする
以下の記事もご参照ください。
ソースコード
# -*- coding: utf-8 -*- import sys import time import numpy as np import matplotlib.pyplot as plt class RealtimePlot1D(): def __init__( self, x_tick, length, xlabel="Time", title="RealtimePlot1D", label=None, color="c", marker='-o', alpha=1.0, ylim=None ): self.x_tick = x_tick self.length = length self.color = color self.marker = marker self.alpha = 1.0 self.ylim = ylim self.label = label self.xlabel = xlabel self.title = title self.line = None # プロット初期化 self.init_plot() def init_plot(self): self.x_vec = np.arange(0, self.length) * self.x_tick \ - self.length * self.x_tick self.y_vec = np.zeros(self.length) plt.ion() fig = plt.figure(figsize=(10,10)) ax = fig.add_subplot(111) self.line = ax.plot(self.x_vec, self.y_vec, self.marker, color=self.color, alpha=self.alpha) if self.ylim is not None: plt.ylim(self.ylim[0], self.ylim[1]) plt.xlabel(self.xlabel) plt.title(self.title) plt.grid() plt.show() self.index = 0 self.x_data = -self.x_tick self.pretime = 0.0 self.fps = 0.0 def update_index(self): self.index = self.index + 1 if self.index < self.length-1 else 0 def update_ylim(self, y_data): ylim = self.line[0].axes.get_ylim() if y_data < ylim[0]: plt.ylim(y_data*1.1, ylim[1]) elif y_data > ylim[1]: plt.ylim(ylim[0], y_data*1.1) def compute_fps(self): curtime = time.time() time_diff = curtime - self.pretime self.fps = 1.0 / (time_diff + 1e-16) self.pretime = curtime def update(self, y_data): # プロットする配列の更新 self.x_data += self.x_tick self.y_vec[self.index] = y_data y_pos = self.index + 1 if self.index < self.length else 0 tmp_y_vec = np.r_[self.y_vec[y_pos:self.length], self.y_vec[0:y_pos]] self.line[0].set_ydata(tmp_y_vec) if self.ylim is None: self.update_ylim(y_data) plt.title(f"fps: {self.fps:0.1f} Hz") plt.pause(0.01) # 次のプロット更新のための処理 self.update_index() self.compute_fps() if __name__ == "__main__": x_tick = 0.1 # 時間方向の間隔 length = 100 # プロットする配列の要素数 realtime_plot1d = RealtimePlot1D(x_tick, length) for e in range(0, 500): #time.sleep(1.0/30) y_data = np.random.normal(0.0, 1.0) # 正規分布に従う乱数 realtime_plot1d.update(y_data)