Pythonで脱力系グラフを描き、各種フォーマットで保存する(ラズパイ、macOS)


2021-11-24 追記
最近のmatplotlib/fontmanagerでは、フォントキャッシュクリアができないことが判明。「環境」と「フォント関係」をアップデートしました。あと、Windows 10/11環境でも検証しました。ちなみに、あとしばらくは、Python 3.7.9の使用をオススメ。2022年になったらそろそろ移行を考えるのが良いでしょう。2021年8月14日にPython 3.9標準インストールでDebian 11 / Bullseyeが出たので、次はPython 3.9です。2年に一度の大移動。

成果物

日本語入り脱力系グラフを描いて、各種フォーマットで保存。

  • 画像(PNG、JPEG、TIFF、SVG、PDF)
  • GIFアニメ、MPEG4動画
  • リアルタイム描画

すぐに試してみたい人は解説をパス


これは、50%スケールで出力したアニメGIFです。

やったこと (ラズパイ、macOS)

  1. Pythonのグラフ描画ライブラリ、matplotlibを使って手書き風グラフを描く。
  2. 日本語フォントを使えるようにして、手書き風フォントにする。
  3. LaTeX表記で数式を書き込む。(日本語フォントは使えない。)
  4. アニメーションのようにリアルタイムに描画する。
  5. 最終結果を画像(PNG、JPEG、TIFF、SVG、PDF)として保存する。
  6. 途中経過をMPEG4動画として保存する。
  7. 途中経過をGIFアニメとして保存する。

環境 (2021-01-14)

ラズパイ:Raspberry Pi 3B、Raspbian Stretch、Python 3.5.3
ラズパイ:Raspberry Pi 4B、Raspbian Buster、Python 3.7.3
macOS:Macbook Air 13inch 2017、Mojave、Python 3.7.9

matplotlib 3.0.3 から 3.3.3、numpy 1.17.0 から 1.18.2
ffmpeg 1.4

かなり昔から同じコードで動いていたので、環境のバージョン依存性は低いです。
おそらく、Windows環境でも割と普通に動くでしょう。

2021-11-14 追記
Windows環境で検証
Windows 10/11 / Python3.7.9 / matplotlib 3.5 / numpy 1.21.4 / ffmpeg 1.4
フォントキャッシュのクリアを新しい方式でやれば動く。

手書き風グラフ? なんなの?

xkcdとは?? 理系、あるいはオタク系ウェブコミックです。( → wikipedia
いかにも理論的、科学的に見えるけど、ブラックジョーク風味。
作者はRandall Munroe

xkcd風のグラフは、脱力系で、こんな感じ( → 画像検索)

で、このxkcd風のグラフを描く機能がグラフ描画ライブラリmatplotlibに正式に組み込まれています。
→ けっこう、やる気満々で脱力しているmatplotlib公式ギャラリー
(/xkcd/ を加えたURLに対して、PNG画像を差し替えているだけですが。)

この脱力系のグラフは、プレゼン/授業のスライドなどに入れると、なかなかいい味を出してくれて、アニメーションして描くとさらに注意をひくことができます。

脱力系の描き方解説

すぐに試してみたい人はパス

xkcd風にするだけなら、非常に簡単。plt.xkcd()を追加するのみ。各種パラメーターを脱力方向に調整してくれます。

from matplotlib import pyplot as plt

plt.xkcd()
# 以降、脱力系

部分的にxkcd風にする場合は、withによるブロック化を使います。

# ブロック外だと、真面目なグラフ。
with plt.xkcd():
    # このブロック内で描くと、脱力系。
    #
# ブロック外だと、真面目なグラフ。

リアルタイムにアニメーション

描いている様子をリアルタイムに見せたいときは、plt.ion()によるインタラクティブモードで描きます。

plt.ion()     # インタラクティブモード開始
#
# グラフを描く
#
plt.ioff()    # インタラクティブモード終了
plt.show()    # 描き終わってからもウィンドウを残す。

各種フォーマットで保存

ボタンを押して画像保存

グラフを描くときに出てくるウィンドウに保存ボタン(今どきの若者にはピンとこない例のアイコン)が付いています。これを押すと、PNGで保存することができます。

プログラムで各種フォーマット保存

PNG、JPEG、TIFF、SVG、PDFで問題なく保存できました。

macOSの場合、バックエンドがデフォルトのMacOSXでは保存できず真っ白画像に。GUI無しバックエンドのAggを選ぶ必要があった。未解決。
ラズパイの場合、デフォルトバックエンドのPyQt5でオッケー。画面も出しつつ、画像保存もできる。

# 複数種類、保存できます。
    plt.savefig("heart.png")
    plt.savefig("heart.jpeg")
    plt.savefig("heart.tiff")
    plt.savefig("heart.svg")
    plt.savefig("heart.pdf")

# PS, EPSは、軸などが直線になってしまいました。
    plt.savefig("heart.ps")
    plt.savefig("heart.eps")
# バックエンドを指定する場合は、pyplotのインポートより前で指定すること。
import matplotlib
matplotlib.use('Agg')
from matplotlib import pyplot as plt
# 指定されているバックエンドの確認
print("current backend:", plt.get_backend())

MPEG4動画、あるいはGIFアニメとして保存する

この2つは似ていて、writerというものを切り替える。

mp4バージョン

FFMpegWriter = manimation.writers['ffmpeg']
metadata = dict(title='Heart Curve', artist='Matplotlib',
        comment='Movie support!')
writer = FFMpegWriter(fps=15, metadata=metadata)


with writer.saving(fig, "heart_curve.mp4", 100):
    writer.grab_frame()

GIFアニメバージョン

import matplotlib.animation as manimation
from matplotlib.animation import PillowWriter

PillowWriter = manimation.writers['pillow']
fig.set_dpi(50)  # バックエンドによって効き方が違う


with writer.saving(fig, "heart_curve.gif", dpi=None):
    writer.grab_frame()

描いている様子をリアルタイムに見せる

なにか描く度にplt.pause(0.5)として、一時停止すれば良い。

フォント関係

フォントをインストールしたときは、matplotlibのフォントキャッシュを再構築する必要があります。

import matplotlib.font_manager as fm
fm._rebuild()

2021-11-24 追記
上記の方式では、動かなくなってました。_rebuild()が無くなった。。下記の方式でキャッシュディレクトリを見つけて消去すればいけました。macOSでは、/Users/username/.matplotlib、Windowsでは、C:\Users\username.matplotlibにあるので、直接ゴミ箱に捨ててもよし。ラズパイだと~/.cache/matplotlibです。

import shutil
import matplotlib

shutil.rmtree(matplotlib.get_cachedir())

フォント名だけ抜き出してリスト表示するコード

このようにフォントファミリー名を指定する時に必要になります、
plt.rcParams["font.family"] = "Natsume"

import matplotlib.font_manager as fm

font_names=[]
for font_path in fm.findSystemFonts():
    try:
        font_name = fm.FontProperties(fname=font_path).get_name()
    except Exception as e:
        print(font_path)
        print("ERROR::: can't get the font name from the file above.  ", e)
        print("********")
    finally:
        font_names.append(font_name)

    font_names2 = list(set(font_names))
    font_names2.sort()

for font_name in font_names2:
    print(font_name)

参考: 手書きフォント情報

本家xkcdフォント [xkcd] [xkcd Script]
https://github.com/ipython/xkcd-font
コミックの作者の手書き文字からサンプリングしたらしい。当然、アルファベットのみ。

日本語手書きフォント [font.familyの指定キーワード]

851マカポップ [851MkPOP]
851手書き雑フォント [851tegakizatsu]
なつめもじ/なつめもじ抑 [Natsume] [Natsumemozi-o]
あんずもじ/あんずもじ等幅 [APJapanesefont] [APJapanesefontT]
あんずもじ奏/あんずもじ湛 [APJapanesefontK] [APJapanesefontT]
あんずもじ始/あんずもじ始等幅 [APJapanesefontH] [APJapanesefontHT]
ラノベPOP [07LightNovelPOP]
ガガガガ FREE版(カタカナ) [GAGAGAGA]

パス!

長い話は置いといて。

フォントをインストールして、実際に試す

手書きフォントを持っていない人は、とりあえず「なつめもじ」をインストールしましょう。フォント指定行をコメントアウトすれば、脱力線だけなら試せますが、魅力半減以下なので、ぜひインストールしましょう。上記フォント関係のところを読んでください。

京風子(きょうこ)さんの「なつめもじ」をおすすめします。
http://www8.plala.or.jp/p_dolce/

好きなフォントでやりたい場合は、フォント名だけ抜き出してリスト表示するコードを利用してフォント名を調べてください。

ラズパイの場合、インストールしたあと、
システムのフォントキャッシュを再構築。
fc-cache -v

matplotlibのフォントキャッシュ再構築
# matplotlib 3.3.3 あたりでは使えたメソッド。
import matplotlib.font_manager as fm
fm._rebuild()

# 最近はこっちで。
import shutil
import matplotlib

shutil.rmtree(matplotlib.get_cachedir())

成果物とコード

コードはターミナルでipython3を動かし、全行コピペ一発で動かせます。

その他、いずれもiPythonですが、mu-editor、Jupyter Notebook、などのREPLもオッケー。VS CodeのJupyterがイマドキかな。ただし、VS CodeのJupyter環境はリアルタイム系のが遅いようなので要注意。

もちろん、.pyファイルとして保存して実行しても良いです。

脱力グラフ

from matplotlib import pyplot as plt
import numpy as np

plt.xkcd()

#plt.rcParams["font.family"] = "Gen Shin Gothic P"
plt.rcParams["font.family"] = "Natsume"
#plt.rcParams["font.family"] = "GenEi LateGo"


plt.plot(np.sin(np.linspace(0, 10)))
plt.title('いぇい! 日本語の手書き風フォントもつかえるよ')
plt.show()

脱力パイチャート

from matplotlib import pyplot as plt
import numpy as np


plt.xkcd() 
plt.rcParams["font.family"] = "Natsume"

x = np.array([0.2, 0.4, 0.15, 0.25])

#plt.rcParams["font.family"] = "Natsume"
plt.rcParams["font.family"] = "851MkPOP"
plt.rcParams["font.size"] = 24

labels = ['メロン', 'バナナ', 'ぶどう', 'アップル']
colors = ['yellowgreen', 'gold', 'darkviolet', 'hotpink']
plt.pie(x, autopct='%d%%', labels=labels, colors=colors)
plt.axis('equal')
plt.title('好きな果物アンケート結果', fontname="Natsume")
plt.tight_layout()

plt.show()

脱力はぁと関数

from matplotlib import pyplot as plt
from numpy import arange, pi, sin, cos

plt.xkcd() 
plt.rcParams["font.family"] = "Natsume"

STEP = 0.1  # 0.01から0.1で速度/精度調整


def draw_graph(x, y, title, color):
    plt.title(title, fontsize=24)
    plt.gca().set_aspect('equal', adjustable='box')
    plt.plot(x, y, color=color, linewidth=8)
    plt.show()


def draw_heart():
    intervals = arange(0, 2 * pi, STEP)
    x = []
    y = []
    for t in intervals:
        x.append(16 * sin(t) ** 3)
        y.append(13 * cos(t) - 5 * cos(2 * t) - 2 * cos(3 * t) - cos(4 * t))
    draw_graph(x, y, title='はぁと関数', color='#FF6597')


draw_heart()

脱力はぁと関数 リアルタイム描画版 TeXのデモ付き

これは、Jupyterのページ内描画だと各コマを上書きせずにコマ送りで描いていき、とても時間がかかります。マシンパワーによりますが、VS Codeではやらないほうがいいかも。ターミナルからiPythonに食わせましょう。

# realtime drawing version
import matplotlib

from matplotlib import pyplot as plt
from numpy import arange, pi, sin, cos

plt.xkcd()
plt.rcParams["font.family"] = "Natsume"
plt.rcParams["font.size"] = 12
plt.rcParams['xtick.direction'] = 'in'
plt.rcParams['ytick.direction'] = 'in'

plt.xticks(arange(-20, 21, 10))
plt.yticks(arange(-20, 21, 5))

plt.title('はぁと関数', fontsize=24)
plt.gca().set_aspect('equal', adjustable='box')

STEP = 0.05  # 0.01から0.1で速度/精度調整


def draw_heart():
    intervals = arange(0, 2 * pi, STEP)
    x = []
    y = []
    plt.ion()

    for t in intervals:
        x.append(16 * sin(t) ** 3)
        y.append(13 * cos(t) - 5 * cos(2 * t) - 2 * cos(3 * t) - cos(4 * t))
        plt.plot(x, y, color='#FF6597', linewidth=8)
#        plt.draw()
        plt.pause(0.00001)


draw_heart()
#plt.close()

plt.text(0.0, 2.0, r"$\mathrm{x = 16sin^3\theta}$", horizontalalignment='center')
plt.pause(0.5)
plt.text(0.0, 0.0, r"$\mathrm{y = 13cos\theta - 5cos2\theta - 2cos3\theta - cos4\theta}$", horizontalalignment='center')
plt.pause(0.5)
plt.text(0.0, -2.0, r"$\mathrm{0\leqq\theta\leqq2\pi}$", horizontalalignment='center')
plt.pause(0.5)

plt.ioff()
plt.show()

参考
JupyterLabでのアニメーション
https://qiita.com/fhiyo/items/0ea94b0de6cd5c76d67a
https://github.com/matplotlib/jupyter-matplotlib