【matplotlib】0を含んだ画像をlogスケールで白塗りにならずに表示する方法

# はじめに
matplotlibで0が含まれ画像を対数(log)スケールで表示するとき、以下の図の左側のように0の値が白塗りされてしまう事態に遭遇したことはないだろうか。0値にカラーをつけるなら、右側のような色になってほしいですよね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1599258/2c28e8d0-c1b2-36bc-bc9a-413001ba1ee6.png)

そこで、本記事では、**白塗りにならないで正しく表示する方法**を紹介します。

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

## 各種インポート
```Python:各種インポート
import cv2
import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import multivariate_normal
from scipy.signal import convolve
```
実行時の各種バージョン

| Name | Version |
|:-:|:-:|
| numpy | 1.21.6 |
| matplotlib | 3.2.2 |
| scipy | 1.7.3 |


## サンプル画像
```python:
def sample_img(px=25, sigma=1):
    half_px = px // 2
    x, y = np.mgrid[-half_px:half_px+1:, -half_px:half_px+1:]
    pos = np.dstack((x, y))
    mean = np.array([0, 0])
    cov = np.array([[sigma**2, 0], [0, sigma**2]])
    rv = multivariate_normal(mean, cov)
    img = rv.pdf(pos)
    img[img < 1e-10] = 0  # 本記事用に小さい値を0に置換しておく
    return img
```

ガウシアンを使います。ただし、本記事では0値がほしいので適当に``1e-10``より小さい値を``0``に置換しています。

## まずはそのままで見てみよう
```python:何も加工しない場合
img = sample_img()

fig, ax = plt.subplots()
cbar = ax.imshow(img, norm=colors.LogNorm())
fig.colorbar(cbar)
plt.show()
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1599258/0eade9de-8758-1df9-61a0-debaa5eb9d8f.png)

0値が白塗りされているのがわかります。

## 方法
白塗りになる原因は、単純にlogスケールで0を表現できないためです。そのため、0値をカラーバーの最小値に置換すれば解決できます。ここでは、カラーバーの最小値を設定しない場合と設定した場合の両方のケースで白塗りにならない表示方法を紹介します。

### カラーバーの範囲がオートの場合
```python:カラーバーの範囲がオートの場合
img = sample_img()

# 1番小さい値(0)を2番目に小さい値に置換
second_min = np.unique(img)[1]
mask_img = np.copy(img)
mask_img[img < second_min] = second_min

fig, ax = plt.subplots()
cbar = ax.imshow(mask_img, norm=colors.LogNorm())
fig.colorbar(cbar)
plt.show()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1599258/c4e1a629-0c4c-ddd1-e9bf-3c19de0898f5.png)

説明
* [np.unique()](https://numpy.org/doc/stable/reference/generated/numpy.unique.html)を使うことで、画像の値を重複を除き昇順でソートされます。
* その中には、0値が最小値として含まれていて、その一つ大きい値(``np.unique(img)[1]``)は0以外なので、その値を``second_min``に代入します。
* ``mask_img[img < second_min] = second_min``で、``second_min``より小さい値を``second_min``に置換します。
* 後は、``colors.LogNorm())``とすることで、オートで``mask_img``の最小値・最大値でカラーをつけてくれます。

### カラーバーの範囲を任意で指定する場合
```python:カラーバーの範囲を任意で指定する場合
img = sample_img()

# color_minに任意の範囲を指定
color_min = 1e-6
mask_img = np.copy(img)
mask_img[img < color_min] = color_min

fig, ax = plt.subplots()
cbar = ax.imshow(mask_img, norm=colors.LogNorm(vmin=color_min, vmax=None))
fig.colorbar(cbar)
plt.show()
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1599258/078160b8-fec8-abc2-a11f-5a031b0d6d06.png)

説明
* ``color_min``にカラーバーの最小値を指定します。
* ``mask_img[img < color_min] = color_min``で、``color_min``より小さい値を``color_min``に置換します。
* 後は、``colors.LogNorm(vmin=color_min, vmax=None)``とすることで、最小値の範囲を指定しカラーをつけてくれます。(``vmax``には、``None``でなく、適当な値を入れることもできます。)

# まとめ
matplotlibで、0値をlogスケールにしたいけど、白塗りになってしまうという問題を解決する方法を紹介しました。
``np.unique()``は、0が暗黙に含まれて、カラーバーをオートでつけるときに役に立つのでぜひ使ってみてください。
また、単純にカラーの最小値を指定する場合は、やはり、普通にその最小値になるように置換するのが良いかなと思います。

matplotlibで0が含まれた画像の対数表示は、少し慎重に置換しながら解決していきましょう。

# 参考資料
https://numpy.org/doc/stable/reference/generated/numpy.unique.html