画像の正規化に関して

# はじめに
よく、画像の前処理で[0~1]にスケーリングとか[-1,1]にスケーリングとか出てきます。その処理を行うコードも情報によっては書き方が違ったりするし、また画像の正規化や標準化などの言葉も出てきたり、ちょっと混乱してきたのでまとめてみました。

# 正規化とか標準化って何？
そもそもデータセットの特徴量間でスケールが違うことがほとんどです。
身長と体重を例に挙げると、成人男性であれば大体ですけど

身長： 150cm~190cmくらい
体重: 40kg~100kgくらい

の範囲の値になる人ばかりです。単位と値の範囲が大きく異なるわけです。
これを考慮せず、モデルに学習させても、比較的取りうる値の大きい身長の方に引っ張られて、うまく学習できません。
そのため特徴量間でスケール（範囲）を揃えてあげる必要があるわけですね。

その揃える方法として**正規化**と**標準化**の2つがあります。

# 正規化とは
正規化は特徴量の値の範囲を一定の範囲内に収めるように変換する処理のこと。
主に0から1,もしくは-1から1にすることが多い。
元データxを正規化する式は以下。
$$ x_{norm} = \frac{x - x_{min}}{x_{max} - x_{min}} $$ 
$$(x_{norm}: x を正規化した値, x_{min}: x の最小値, x_{max}: x の最大値)$$

「元の値から最小値を引いて、最大値と最小値の差（値の範囲）で割る」という処理をしている。


# 標準化とは
標準化は特徴量の平均を0,分散が1になるように変換する処理のこと。
元データxの標準化を式にすると以下。

$$x_{std}=\frac{x−μ}{σ}$$

$$(x_{std}:x を標準化した値, μ:xの平均,σ:xの標準偏差)$$

# どっちを使えばいいの？
基本的には標準化。正規化だと外れ値の影響を受けやすいため。

ただ画像データであれば**正規化**を行うことがほとんどです。
というのも画像データは取りうる値が決まっているからですね。

カラーの画像データであれば3原色である赤、緑、青の色の強さををそれぞれ0~255までの離散整数値で表すことで、画像として表示されています。
要は赤が0~255までの256段階、緑、青も同様ということで、256の3乗＝約1677万通りの色を表現できます。

実際に見てみた方が早いので画像をndarray形式で読み込んでみましょう。
以下の桜の画像を使いましょう。
![aj-McsNra2VRQQ-unsplash.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/890361/2870aca0-41c6-b29b-f547-66470b8968c8.jpeg)

```python
import numpy as np
import cv2

img = cv2.imread('aj-McsNra2VRQQ-unsplash.jpg')
img
```

```python:結果
array([[[193, 172, 205],
        [193, 172, 205],
        [193, 172, 205],
        ...,
        [250, 234, 197],
        [250, 234, 197],
        [250, 234, 197]],

       [[193, 172, 205],
        [193, 172, 205],
        [193, 172, 205],
        ...,
        [250, 234, 197],
        [250, 234, 197],
        [250, 234, 197]],

```
193とか250とかの値がズラーと並んでますね。0から255までの整数値の組み合わせで画像が表されているわけです。


何度も言いますが、白黒、カラー関わらず、画像データは取りうる値の範囲が決まっています。すなわち最小値は0,最大値は255です。どんな画像データでも大体そうです。
大体と言ったのは、全部黒の画像とかになると最小値0、最大値0になるような例もあるからです。

だた一般的な画像データであれば最小値は0,最大値255と考えていて問題ありません。そのため外れ値に引っ張られることもないので、**正規化**を行うことがほとんどかと思います。

# 画像の正規化の種類
基本は2種類。

1. 0から1の範囲の正規化
2. -1から1の範囲の正規化

正直自分もどっちがいいとかはわかりませんが、[コチラ](https://book.impress.co.jp/books/1120101017)の書籍によれば

>画像のピクセルごとのスケーリングでは、それらの中心を0に設定した上で、[-1,1]の範囲で尺度を取り直す方法も一般的である。実際のところ、通常はそれでうまくいく。

と書かれています。あまり深く考えず、どちらも「取りうる値の大きさを小さくすることで計算を安定させる」ために行っていると思えばいいかと。

個人的な解釈として、もし使い分けるならCNNモデルの最後の出力の値の範囲で使い分ければいいかと思います。つまり活性化関数で何を使うかで決めるということです。

例えばシグモイド関数であれば取りうる値の範囲は0から1なので入力も[0,1]の範囲に正規化した画像にするべきですし、tanh(双曲線正接関数）であれば-1から1の範囲の値を取るので[-1,1]に正規化すべきだと思います。

まぁ書籍とかだとMNISTの分類で最後の活性化関数がsoftmax関数（0から1の範囲の確率値)なのに[-1,1]に正規化してたりとかもしますが（それでもうまくいっていることの方が多い）
あまり厳密になる必要もないのかなと思ったり。

# 実装例
ここから正規化の実装例を思いつくだけ紹介します。

## [0,1]スケーリング
まず[0,1]スケーリングから、とは言ってもこれしかないですが、、、

```python
img = cv2.imread('aj-McsNra2VRQQ-unsplash.jpg')
img = img / 255
```

各ピクセル値を255で割ることで0から1の範囲になります。
たまーに、おそらく0から255で256諧調だからか 256で割っているコードも見かけますが、それだと最大値が1にならないので注意です。

正規化しても問題なく表示できます。

```python
import matplotlib.pyplot as plt
img = cv2.imread('aj-McsNra2VRQQ-unsplash.jpg')

# opencvはカラー画像をBGRモードで読み込むのでRGBに変換
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)

# 正規化

img = img / 255

plt.imshow(img)
```
3行目の処理に関して詳しく[コチラ](https://qiita.com/okateru/items/cc41ad91caf297817066)の記事にまとめてますので、良かったら見てください。

表示結果⇩
![download.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/890361/8d0b05fd-4cc6-3200-3a44-b4eca94febd7.png)

## [-1,1]スケーリング
次は[-1,1]スケーリング。とは言ってもそんなに種類ないです。

```python:例①
img = cv2.imread('aj-McsNra2VRQQ-unsplash.jpg')
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)

img = (img - 127.5) / 127.5
```

GANモデルとかだとよく見ます。255の半分の値の127.5を引いて[-127.5, 127.5]の範囲に。その後127.5で割ることで[-1,1]にしています。

```python:例②
img = cv2.imread('aj-McsNra2VRQQ-unsplash.jpg')
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)

img = ((img / 255) - 0.5) * 2
```

レアケース。まず255で割って[0,1]に。0.5を引いて[-0.5,0.5]。それを2倍することで[-1,1]にしています。

この2つくらいかな。

じゃ[-1,1]スケールした画像を表示してみます。

```python
img = cv2.imread('aj-McsNra2VRQQ-unsplash.jpg')
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)

img = (img - 127.5) / 127.5
plt.imshow(img)
```

```
Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers).
```
![download.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/890361/d6a14391-12d5-afa2-3769-7733a4eb1863.png)

エラーと共に赤色が強い画像が表示されました。

まぁ色の値にマイナスなんてものはないので当たり前ですね。
[-1,1]スケールにした画像を正しく表示したい場合は、float型の[0,1]スケールに戻すか、8ビット整数型の[0,255]スケールに戻す必要があります。

やり方は以下。

```python:[-1,1]→[0,1]
img = 0.5 * img + 0.5
```
0.5をかけて [-0.5, 0.5]。さらに0.5を足すことで[0,1]にしています。

```python:[-1,1]→[0,255]
img = 127.5 * img + 127.5
img = img.astype('uint8')
```
127.5をかけて[-127.5, 127.5]。さらに127.5を足して[0,255]にしています。
ただ[0,255]に戻す場合は、整数値にしなくてはいけませんので8ビット整数型に変える必要があります。

# まとめ
画像の正規化についてまとめて見ました。

画像の型と、取りうる値の範囲には常に意識を向けておくことが大事だと思います。