（matplotlib）ピクセル単位でサイズを指定したグラフを描きたい

# はじめに

matplotlib で作成するグラフについて、次のように **プロット領域の大きさ（幅・高さ）をピクセル単位で指定するにはどうすればいいのか** についての解説です。

![top.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152805/2b9a18f8-4d2e-8eac-4dc4-045bbbc25a13.png)

## 実行環境（Google Colab.）
 - python 3.6.9
 - matplotlib 3.1.2 

# 注意

Jupyter環境（Google Colab.環境）の場合、`plt.show(...)`により**実行結果セルに表示されるPNG画像**と、`plt.savefig(...)`で**ファイル出力されるPNG画像**では、<font color='red'>**その大きさが違ってきます**</font>。

具体的には、**実行結果セルに表示されるPNG画像**は、**<font color='red'>周囲の余白部分が自動的にトリミングされたもの</font>**になります。

例えば、次のコードを Google Colab.環境で自動した場合、出力セルに表示される画像は $348\times 270$ (px)、一方で、`test.png` として保存される画像は $400\times 300$ (px) となります。

```py:
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}$ になる・・・はずなのですが、<font color='red'>出力セルに表示される画像は、周囲余白がトリミングされてひとまわり小さくなったもの</font>になります。

以降は、`plt.savefig(...)`で**ファイル出力される画像を対象**に話を進めます。

# 画像サイズではなく（正味の）プロット領域のサイズを求めたい

画像サイズ（ピクセル単位）は、上述したように、インチ単位で指定される `figure` の「サイズ」と「DPI」から求めることができました。これは、`figure` のインスタンス（`fig`）から次のように計算できます。

```py:画像サイズ（ピクセル単位）を計算
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]')
```

まず、ここでは、この画像の全体サイズではなく、次のように<font color='red'>**赤枠**</font>**で囲まれた（正味の）プロット領域の大きさ**（ピクセル単位）が、どのようになっているのか求めていきます。

![top2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152805/5e8a146d-3bde-1d68-d040-d68e1b3a2ec7.png)


プロット領域の大きさ（ピクセル単位）は、次のように、Axesオブジェクト（`ax`）から値を取得して計算することができます。なお、図内に日本語を使っているので、Google Colab.環境などでは、先に `!pip install japanize-matplotlib` を実行してライブラリをインストールしておいてください。

```py:プロット領域サイズ（ピクセル単位）を計算
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](https://matplotlib.org/gallery/axes_grid1/demo_fixed_size_axes.html) @ matplotlib.org

なお、原因は不明なのですが、プログラムで最後で、プロット領域の大きさを再計算して出力すると `Axis size  387 x 302 [px]` となってしまいます。ただ、実際に出力された画像（foo.png）についてプロット領域の大きさを測ってみると、正しく $400\times 300$ (px) となっています。

```py:プロット領域の大きさをピクセル単位で指定してグラフ出力
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 になっている
```