読者です 読者をやめる 読者になる 読者になる

drilldripper’s blog

機械学習とソフトウェア開発を頑張ってます

手書き風グラフを支える技術 -Matplotlibのxkcd関数とFIRフィルタ-

Pythonのグラフ描画ライブラリのMatplotlibには、手書き風のグラフを描画するための関数xkcd()があります。以下のソースコードを実行すると、手書き風のsinグラフが描画されます。

from matplotlib import pyplot as plt
import numpy as np
plt.xkcd()  # この関数を実行すると、次のグラフが手書き風グラフになる
plt.plot(np.sin(np.linspace(0, 10)))
plt.title('Handwriting sin graph')
plt.show()

f:id:drilldripper:20170324202314p:plain

温かみがあっていい感じですね。

このような手書き風グラフは、関数の名前にも使われているxkcdというサイトの画像をリスペクトしたものです。

xkcd: Color Pattern

理系的なネタが多く、エンジニアの間でもよく話題になります。また海外版空想科学読本といったテイストの本、ホワット・イフが翻訳されて話題になりました。*1

ホワット・イフ?:野球のボールを光速で投げたらどうなるか

ホワット・イフ?:野球のボールを光速で投げたらどうなるか

さて、このような手書き風グラフはコンピュータでどのように描画しているのでしょうか?

 乱数生成とローパスフィルタ

現在Matplotlibに実装されているxkcd()関数の原案となったブログ記事を参考にしながら読み解いて行きます。

XKCD-style plots in Matplotlib | Pythonic Perambulations

記事のxkcd_line()に注目してください。この関数ではグラフを描くための配列(x, y)を与えたときに、その間を補間するための点を生成します。補完する点にノイズ(ゆらぎ)を加えることによって、手書きのようなガタガタとした線を表現することができます。

この線を描画するために、補間する点数の制御やB-Spline曲線での近似などの様々な工夫がされていますが、肝となる部分はFIRフィルターのローパスフィルタによるノイズ制御と言って問題ないと思います。*2

xkcd_line()中の次のコードを見てみましょう。

# create a filtered perturbation
coeffs = mag * np.random.normal(0, 0.01, len(x_int) - 2) # mag=10
b = signal.firwin(f1, f2 * dist_tot, window=('kaiser', f3)) # f1=30, f2=0.05, f3=15
response = signal.lfilter(b, 1, coeffs)

1行目のコードは線に加えるノイズの大きさを決めています。magを変化させることによって乱数のスケールが変化し、線のガタツキの強弱が変わります。

2行目のコードではFIRフィルタでローパスフィルタの設計を行っています。(FIRフィルターについては後述します)ローパスフィルタは低周波成分のみを通過させて、高周波成分をカットするフィルタです。すなわち1行目で生成した乱数の中で、しきい値を超えたものを0にする処理を行います。これにより本来のグラフから極端に外れた点を抑制することができるので、適度なガタツキを表現することができます。

3行目のコードでは、2行目で設計したローパスフィルターを実際に適応しています。

FIRフィルター

FIRフィルターは有限インパルス応答フィルタ(Finite Impulse Response Filter)とも呼ばれるディジタルフィルタの一種です。FIRフィルターの回路は次の図と式で表されます。

f:id:drilldripper:20170324205455p:plain

{} $$ y = \sum_{k=0}^{N-1} (b[k]x[n-k])
= b[0]x[n-0] + b[1]x[n-1] + b[2]x[n-3] +… b[N-1]x[n-(N-1)] $$

x[n]を入力信号、y[n]を出力、b[n]をフィルタ係数とします。

このとき上式のNを一般にタップ数と呼びます。このフィルタに入力x[n]を入れたときに必要のない周波数成分を取り除くように回路を設計することによって、ローパスフィルタやハイパスフィルタを実現することができます。またこの式から、タップ数を増やすことで入力に対して大きな応答を得ることができることがわかります。

xkcd_line()内で使われいるscipyに実装されているFIRフィルターsignal.firwin()は、第1引数にカットオフ周波数、第2引数にタップ数、第3引数に窓関数を指定する仕様になっています。*3

実験

xの入力を[-3, -2, -1, 0, 1, 2, 3]、yの入力を[9, 4, 1, 0, 1, 4, 9]として、タップ数とカットオフ周波数を変更していきながらグラフの結果を見ていきます。これは{}y = x2のプロットです。

タップ数の変更

(左から順にタップ数の係数0.05, 0.1, 0.2) タップ数の係数を大きくするに従って波の振動が細かくなっていることがわかります。

ここで注意したいのは変更した値はタップ数の「係数」で、タップ数そのものではありません。あらかじめ計算しておいた補間する点の2点間距離に係数をかけています。

カットオフ周波数の変更

(左から順にカットオフ周波数1, 30, 200) カットオフ周波数を大きくするにしたがって、波の振動が抑えられていることがわかります。

参考

SciPyのFIRフィルタの使い方 - 人工知能に関する断創録

*1:私もこの本を読みましたが、とてもおもしろかったです

*2:他にもフォントの変更など、Matplotlibに実装されているxkcd()関数はもっと凝った処理をしています

*3:窓関数の特性に応じて、信号の解析することのできる範囲を変更することができます。詳しい説明は省きますので、気になる方は各自お調べください