はじめに
matplotlib で作成するグラフについて、次のように プロット領域の大きさ(幅・高さ)をピクセル単位で指定するにはどうすればいいのか についての解説です。
実行環境(Google Colab.)
- python 3.6.9
- matplotlib 3.1.2
注意
Jupyter環境(Google Colab.環境)の場合、plt.show(...)
により実行結果セルに表示されるPNG画像と、plt.savefig(...)
でファイル出力されるPNG画像では、その大きさが違ってきます。
具体的には、実行結果セルに表示されるPNG画像は、**周囲の余白部分が自動的にトリミングされたもの**になります。
例えば、次のコードを Google Colab.環境で自動した場合、出力セルに表示される画像は $348\times 270$ (px)、一方で、test.png
として保存される画像は $400\times 300$ (px) となります。
import numpy as np
import matplotlib.pyplot as plt
x = np.linspace(0,20,100) # 描画用サンプルデータ
y = x ** 0.5 # 描画用サンプルデータ
fig = plt.figure(dpi=100, figsize=(4,3))
plt.plot(x,y)
plt.savefig('test.png') # ファイルに出力される 400 x 300 px
plt.show() # 出力セルに表示される 348 x 270 px
-
figsize=(4,3)
では、インチ単位で画像の「幅」と「高さ」を指定しています。 -
dpi=100
では「Dot per Inch(=1インチあたりのドット数(ピクセル数)」を指定しています。 - 出力画像サイズは、幅が $4,\mathrm{inch}\times 100,\mathrm{dpi}=400,\mathrm{pixel}$、高さが $3,\mathrm{inch}\times 100,\mathrm{dpi}=300,\mathrm{pixel}$ になる・・・はずなのですが、出力セルに表示される画像は、周囲余白がトリミングされてひとまわり小さくなったものになります。
以降は、plt.savefig(...)
でファイル出力される画像を対象に話を進めます。
画像サイズではなく(正味の)プロット領域のサイズを求めたい
画像サイズ(ピクセル単位)は、上述したように、インチ単位で指定される figure
の「サイズ」と「DPI」から求めることができました。これは、figure
のインスタンス(fig
)から次のように計算できます。
fig_w_px = int(fig.get_figwidth() * fig.get_dpi())
fig_h_px = int(fig.get_figheight() * fig.get_dpi())
print(f'■ Fig size {fig_w_px} x {fig_h_px} [px]')
まず、ここでは、この画像の全体サイズではなく、次のように赤枠で囲まれた(正味の)プロット領域の大きさ(ピクセル単位)が、どのようになっているのか求めていきます。
プロット領域の大きさ(ピクセル単位)は、次のように、Axesオブジェクト(ax
)から値を取得して計算することができます。なお、図内に日本語を使っているので、Google Colab.環境などでは、先に !pip install japanize-matplotlib
を実行してライブラリをインストールしておいてください。
import numpy as np
import japanize_matplotlib
import matplotlib.pyplot as plt
x = np.linspace(0,20,100) # 描画用サンプルデータ
y = x ** 0.5 # 描画用サンプルデータ
fig = plt.figure(dpi=100, figsize=(4,3))
ax = plt.gca()
ax.plot(x,y)
ax.set_xlabel('入力',fontsize=12)
ax.set_ylabel('出力',fontsize=12)
plt.savefig('foo.png')
# Figure(画像全体)のサイズ
fig_w_px = int(fig.get_figwidth() * fig.get_dpi())
fig_h_px = int(fig.get_figheight() * fig.get_dpi())
print(f'■ Fig size {fig_w_px} x {fig_h_px} [px]')
# Axis(プロット領域)のサイズ
ax_size_inch = ax.figure.get_size_inches()
ax_w_inch = ax_size_inch[0] * (ax.figure.subplotpars.right - ax.figure.subplotpars.left)
ax_h_inch = ax_size_inch[1] * (ax.figure.subplotpars.top - ax.figure.subplotpars.bottom)
ax_w_px = int( ax_w_inch * fig.get_dpi() )
ax_h_px = int( ax_h_inch * fig.get_dpi() )
print(f'■ Axis size {ax_w_px } x {ax_h_px } [px]')
実行結果は、次のようになります。また、ファイル出力された foo.png を画像編集ソフト等で開き、プロット領域の大きさを確認してみると、確かに下記の結果とも一致します。
■ Fig size 400 x 300 [px]
■ Axis size 310 x 226 [px]
プログラムの解説
-
ax.figure.get_size_inches()
で、Axesオブジェクトの全体の大きさ(=各軸の目盛やラベル(「入力」や「出力」などの文字)も含んだ領域の大きさ)をインチ単位で取得しています。 -
ax.figure.subplotpars.left
では、Axesオブジェクトの幅全体を $1.0$ としたときの プロット領域の左端位置 を表す値($0.0$~$1.0$)です。またax.figure.subplotpars.right
は、プロット領域の右端位置 を表す値です。- ここで、両者の差分
ax.figure.subplotpars.right - ax.figure.subplotpars.left
は、Axesオブジェクトの幅全体を $1.0$ としたときに、そこでプロット領域の幅が占める割合を表したものになります。
- ここで、両者の差分
-
プロット領域の幅をインチ単位で求めるために、Axesオブジェクトの全体幅
ax_size_inch[0]
(インチ単位)と、プロット領域の幅(割合)ax.figure.subplotpars.right - ax.figure.subplotpars.left
を掛け、ax_w_inch
に格納しています。 -
インチ単位を、ピクセル単位に変換するために、
ax_w_inch
に、解像度fig.get_dpi()
をかけて、ax_w_px
に格納しています。
プロット領域のサイズをピクセル単位で指定してグラフを描きたい
プロット領域のサイズをピクセル単位で求めることはできました。
今度は、逆に、プロット領域のサイズをピクセル単位で指定してグラフを描いていきます。次のコードは、プロット領域の大きさが、$400\times 300$ (px) になるようにグラフを描画するサンプルです。
- 参考:Demo Fixed Size Axes @ matplotlib.org
なお、原因は不明なのですが、プログラムで最後で、プロット領域の大きさを再計算して出力すると Axis size 387 x 302 [px]
となってしまいます。ただ、実際に出力された画像(foo.png)についてプロット領域の大きさを測ってみると、正しく $400\times 300$ (px) となっています。
import numpy as np
import japanize_matplotlib
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1 import Divider, Size # 追加
from mpl_toolkits.axes_grid1.mpl_axes import Axes # 追加
x = np.linspace(0,20,100) # 描画用サンプルデータ
y = x ** 0.5 # 描画用サンプルデータ
ax_w_px = 400 # プロット領域の幅をピクセル単位で指定
ax_h_px = 300 # プロット領域の高さをピクセル単位で指定
# サイズ指定のための処理 ↓↓ ここから ↓↓
fig_dpi = 100
ax_w_inch = ax_w_px / fig_dpi
ax_h_inch = ax_h_px / fig_dpi
ax_margin_inch = (0.5, 0.5, 0.5, 0.5) # Left,Top,Right,Bottom [inch]
fig_w_inch = ax_w_inch + ax_margin_inch[0] + ax_margin_inch[2]
fig_h_inch = ax_h_inch + ax_margin_inch[1] + ax_margin_inch[3]
fig = plt.figure( dpi=fig_dpi, figsize=(fig_w_inch, fig_h_inch))
ax_p_w = [Size.Fixed(ax_margin_inch[0]),Size.Fixed(ax_w_inch)]
ax_p_h = [Size.Fixed(ax_margin_inch[1]),Size.Fixed(ax_h_inch)]
divider = Divider(fig, (0.0, 0.0, 1.0, 1.0), ax_p_w, ax_p_h, aspect=False)
ax = Axes(fig, divider.get_position())
ax.set_axes_locator(divider.new_locator(nx=1,ny=1))
fig.add_axes(ax)
# サイズ指定のための処理 ↑↑ ここまで ↑↑
ax.plot(x,y)
ax.set_xlabel('入力',fontsize=12)
ax.set_ylabel('出力',fontsize=12)
plt.savefig('foo.png')
# Figure(画像全体)のサイズ
fig_w_px = int(fig.get_figwidth() * fig.get_dpi())
fig_h_px = int(fig.get_figheight() * fig.get_dpi())
print(f'■ Fig size {fig_w_px} x {fig_h_px} [px]')
# Axis(プロット領域)のサイズ
ax_size_inch = ax.get_figure().get_size_inches()
ax_w_inch = ax_size_inch[0] * (ax.figure.subplotpars.right - ax.figure.subplotpars.left)
ax_h_inch = ax_size_inch[1] * (ax.figure.subplotpars.top - ax.figure.subplotpars.bottom)
ax_w_px = int( ax_w_inch * fig.get_dpi() )
ax_h_px = int( ax_h_inch * fig.get_dpi() )
print(f'■ Axis size {ax_w_px } x {ax_h_px } [px]')
# 上記の print出力では、400 x 300 にならないが、
# 実際の画像('foo.png')ではプロット領域の大きさが 400 x 300 になっている