matplotlibで2次元配列の解像度に合わせた画像を保存する方法

# はじめに
matplotlibで、2次元配列を画像として保存する必要がある場合に、配列のサイズと同じ解像度の画像を保存する方法をいくつか紹介します。本手法により、データの解像度を保ったまま画像サイズを調整し、matplotlibの補間による劣化を防ぐことができます。特に、生のデータを扱う方にとって、1つの配列要素に1画素が対応する画像はありがたいと思います。

本記事の目標
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1599258/e57b8290-58aa-66ae-fbfb-0d727265e1ea.png)

# 実装
Google Colabで作成した本記事のコードは、[こちら](https://colab.research.google.com/drive/175F1_1Z7yxxIQ5-ympBWg2sE-vKDIk0b?usp=sharing)にあります。

## 各種インポート
```Python:各種インポート
import numpy as np
import matplotlib.pyplot as plt
```

## 方法
matplotlibで生成される図のピクセル数は、``figsize=(横, 縦)``とdpiを用いて、``(横 * dpi) × (縦 * dpi)``となります。このため、配列の縦横の個数と生成される図のピクセル数を一致させることで、配列の各要素をピクセル単位で表現することができます。本記事では**配列と同じサイズのカラーマップを作成する方法**の紹介します。

※本記事の実装コードは、配列以外の情報（例えばカラーバーや軸ラベルなど）を図に含めると、その情報も画像サイズに含めて生成されるため、配列をピクセル単位での表現が損なわれることになります。簡易的なコードで、配列をピクセル単位で正確に表現するためには、図に含める情報を配列のみに制限する必要があります。
もしそれらも含めて実装しようと思うと、例えば、配列が小さすぎる場合には、文字などが小さくなりすぎる（ドット絵のようになる）といった問題が発生し、データの損失がないようにするには、配列のサイズの整数倍に拡張したりする必要があります。しかし、すべてのケースで汎用的に実装することは困難なため、本記事ではこの問題については触れず、配列と同じサイズの画像を生成する方法の紹介に留めています。

### 方法1(plt.subplots(), tight_layoutを使う方法)
これが一番シンプルで美しい方法だと思います。

```python:plt.subplots(), tight_layoutを使う方法
image = np.random.rand(300, 500)

fig, ax = plt.subplots(figsize=image.shape[::-1], dpi=1, tight_layout=True)
ax.imshow(image)
ax.axis('off')

plt.savefig('image.png', dpi=1)
```

コードの説明

* ``figsize=image.shape[::-1]``は、``image.shape``は、(縦、横)の順なのに対し、``figsize``は(横、縦)なので、``image.shape[::-1]``で逆順にして指定します。
* ``dpi=1``は、``(横 * dpi) × (縦 * dpi)``がピクセルサイズとなり、``figsize``に配列のサイズを指定しているため、``dpi=1``にします。
* ``tight_layout=True``で、余白をなくします。配列のサイズと完全に同じサイズの画像サイズにするためです。
* ``ax.axis('off')``は、軸情報を消すために使います。軸情報が含まれると、配列をピクセル単位で表示できなくなるためこの部分も必要です。
* plt.savefig('image.png', dpi=1)は、``dpi=1``で保存します。（``plt.savefig``の``dpi``に何も指定しないと、``plt.subplots()``で指定した場合の``dpi``と同じものが使われるかもしれませんが、私自身調べてもよくわからなかったので、念の為ここでも``dpi``を指定しておきます。）

出力結果
縦 300 × 横 500 ピクセルで出力されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1599258/f88a2e84-0488-9df8-f2dc-67e770125ab8.png)


### 方法2(plt.subplots(), subplots_adjust()を使う方法)
方法1でいいと思います。
```python:plt.figure(), subplots_adjust()を使う方法
image = np.random.rand(300, 500)

fig, ax = plt.subplots(figsize=image.shape[::-1], dpi=1)
fig.subplots_adjust(left=0, bottom=0, right=1, top=1)
ax.imshow(image)
ax.axis('off')

plt.savefig('image.png', dpi=1)
```

コードの説明

``fig, ax = plt.subplots(figsize=image.shape[::-1], dpi=1)``だけでは、余白が含まれてしまいます。そこで、``fig.subplots_adjust(left=0, bottom=0, right=1, top=1)``で、画像の描画エリア全体を1とした比率で指定しており、この場合、縦横のエリアを余すことなく指定しています。そうすることで、ピクセル単位で表示できます。

### 方法3(plt.figure(), add_axes()を使う方法)
方法1でいいと思います。
```python:plt.figure(), add_axes()を使う方法
image = np.random.rand(300, 500)

fig = plt.figure(figsize=image.shape[::-1], dpi=1)
ax = fig.add_axes([0, 0, 1, 1])
ax.imshow(image)
ax.axis('off')

plt.savefig('image.png', dpi=1)
```
コードの説明

``fig.add_axes[x0, y0, width, height]``は、表示エリアの``[x0, y0]``を左下の座標として、幅と高さをそれぞれ全体を1とした比率で指定します。``ax = fig.add_axes([0, 0, 1, 1])``とすることで、描画エリアを縦横余すことなく使うことになり、ピクセル単位での表示を可能にしています。


# まとめ
matplotlibで、配列を同じサイズの画像で保存する方法を紹介しました。matplotlibは配列サイズに比べて画像のサイズが小さい場合に補完を行われ、データの表現が変化してしまうため、数ピクセル単位に関心がある際には注意が必要です。データの損失なく画像を見たい場合に、本記事を活用していただければ幸いです。

また、matplotlibの補完の振る舞いについて、私の記事で恐縮ですが以前紹介しているので、よろしければそちらもご覧ください。

https://qiita.com/yusuke_s_yusuke/items/65c7ccbdcc7271a6c3cf



# 参考資料

https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.subplots_adjust.html

https://matplotlib.org/stable/api/figure_api.html#matplotlib.figure.Figure.add_axes