画像処理のトーンカーブについての話しをしよう ~LUTはすごいぞ~

#はじめに
Pythonで`OpneCV`を使い始めて間もないのですが
いろいろ試してつまずいたところなどを
つらつら書いていこうと思います.

掻い摘んで話すと
**`LUT`ってすごいんだな!**
って思ったので
**`LUT`使ってみました!**
っていう内容です.

(結果的に)自作の色調変換関数との性能比較も行いました.

>
※様々なトーンカーブを扱った記事は[こちら>>][関連1]

#動作環境
```
端末：Windows 10
コンソール：cmd(コマンドプロンプト)
python：3.6.8
仮想環境：venv
```
cmdを快適にしたい方は[こちらへ>>](https://qiita.com/RoaaaA/items/694ae4ccfa4a69fc8286)

#導入
私は大学の授業(ペーパーワーク)でComputer Vision(CV)について学びました.
そこではじめて「**トーンカーブ**」というものに出会いました.
トーンカーブとは[^1]

>
ディジタル画像の各画素は, その濃淡を表す値(画素値)を持っている. 画像の濃淡を変化させるためには, 入力画像の画素値に対し, 出力画像の画素値をどのように対応づけるかを指定すればよい. そのような対応関係を与える関数のことを諧調変換関数(gray-level transformarion function), また, それをグラフで表したものをトーンカーブ(tone curve)とよぶ.

ネットをみると, カメラ好きの方たちにはおなじみのものみたいです.

教科書に出てきたトーンカーブの中で, 以下のような二つがありました.
(※もちろんほかにもたくさん出てきました)

横軸が**入力画素値**, 縦軸が**出力(変換後の)画素値**となります.

| ![toneCurve12.png][t1] | ![toneCurve22.png][t2] |
|:---:|:---:|
| 図１ | 図２ |

簡単に言うと
**コントラストを上げる変換**と
**コントラストを下げる変換**です.


この部分の授業なんて半年前に, とっくに終わっているのですが
このトーンカーブを実際に画像へ適用してみたくなりました.

#関数化
関数で表すと以下のようになります( xは入力画素値 0 ~ 255).

図1の関数:

```math
f(x) = \begin{cases} 2 \cdot x & (x < 128) \\ 255 & (otherwise)
  \end{cases}
```

図2の関数:

```math
f(x) = \begin{cases} 0 & (x < 128) \\ 2 \cdot x - 255 & (otherwise)
  \end{cases}
```
 
一般化できそうですが, そのことはひとまず置いておきましょう.

#失敗1
まずは, 図1のコードを書こうと思いました.
が...早速詰まりました.

私は, 上の関数を思いついた時点で
単純に元の画素値の配列の要素値を二倍にすればいいと思いました.

```py:変換関数
def toneCurve(frame):
    return 2 * frame

frame = cv2.imread('.\\image\\castle.jpg')
cv2.imwrite('cas.jpg', toneCurve(frame))
```
`frame`は画素値の格納されている配列

しかしうまくいきませんでした.
出力した画像は以下のようになりました.

| 元画像 | 変換画像 |
|:----:|:----:|
| ![元画像][cas1] | ![失敗画像1][cas2] |

どうやら`cv2.imread()`で取得した画素値を単純に二倍すると
255(最大値)より大きなものは255になる補完されるのではなく
サイクル的に値を変換して返されてしまうようでした.
(ex:256=0, 260=4)

とても禍々しくてこれはこれで好きですが
目的はこの画像ではないので改良します.

#改良1(失敗2)
調べてみると, `cv2.add()`を使えば
255以上の値を255として扱ってくれるみたいです.

試してみました.

##図1の改良
自分に自身の画素値を足しこむ = 定数倍
ということになるので
変換関数は以下のようになりました.

```python:変換関数
def toneCurve11(frame, n = 1):
    tmp = frame
    if n > 1:
        for i in range(1,n):
            tmp = cv2.add(tmp, frame)
    return tmp
```
`frame`は画素値の格納されている配列
`n`は画素値を何倍, つまりaddする回数を表します.

それでは結果です.

| 元画像 | 変換画像 |
|:----------------------:|:----------------------:|
| ![元画像][cas1] | ![失敗画像2][cas3] |

そう! 求めていたのはこれです! 
コントラストを上げた画像を出力してくれました.
(グレイスケールの画像の方がよかったですか?)

しかし, この足していくだけの方法でどうやって図2の変換を実現し
画像を出力するのか?

**パっとひかって思いつきました**:fireworks:

##図2の改良
何を思いついたかというと, こういうことです.

**`ネガポジ反転` > `cv2.add()` > `ネガポジ反転`**

図に表すとこうなります.

| ![ori][ori] | ![negapoei][np] |
|:-:|:-:|:-:|:-:|
|1.無変換|2.ネガポジ反転|
| ![add][add] | ![renegaposi][renp] |
|3.cv2.add()で倍化|4.ネガポジ反転|

変化は1, 2, 3, 4の順です.

変換用コードは以下のようになりました.

```py:変換関数
def negaPosi(frame):
    return 255 - frame


def toneCurve21(frame, n = 1):
    if n > 1:
        return negaPoso(toneCurve11(negaPosi(frame), n))
    return frame
```
先ほど同様に
`frame`は画素値の格納されている配列
`n`は画素値を何倍, つまりaddする回数を表します.

それでは, 結果です.

| 元画像 | 変換画像 |
|:----------------------:|:----------------------:|
| ![元画像][cas1] | ![失敗画像3][cas4] |

無事コントラストを下げた画像を出力してくれました.

#小休止
以上のようにやっても, まあうまくいくんですけど
**オーダー**について問われそうな気がし, ほかの方法を調べることにしました.

すると, cv2には**`LUT(Look Up Table)`**なる便利なものがあるようです.
しかも, オーダーもかなり小さくできると...

#LUT
上でいくつかのアプローチをしてきたわけですが, 結局のところ
取得した画素値を別の画素値に置き換えれば, 変換できるわけですね.

ここで, 画素値の取りうる値は 0 ~ 255 の**高々256通り**です.
ならば, ある画素値に対する変換先の値をあらかじめ登録した対応表を持っておき
画素値を参照したとき, 画素値自体を計算するのではなく
単純に置き換えるだけにすれば計算コストを抑えることができる.

これがLUTの簡単な説明です.

例えば以下のような対応表が考えられます.

| 参照画素 |  | 変更先 |
|:-:|:-:|:-:|
| 0 |  -> | 0 |
| 1 | ->  | 2 |
|  ... |   | ...  |
| 127  | ->  |  254 |
| 128  | ->  |  255 |
| ...  |   |  ... |
|  255 |  -> |  255 |

そうです, この表は図1のトーンカーブの対応表です.

もし, frame[90][90] の画素値が
`[12, 245, 98]`だったとすると
LUT適用後は
`[24, 255, 196]`になります.


#改良2
LUTを実際に用いていきます.

##図1の改良(Re:)

```py:変換関数
def toneCurve12(frame, n = 1):
    look_up_table = np.zeros((256,1), dtype = 'uint8')
    for i in range(256):
        if i < 256 / n:
            look_up_table[i][0] = i * n
        else:
            look_up_table[i][0] = 255
    return cv2.LUT(frame, look_up_table)
```
`frame`は画素値の格納されている配列
`n`は画素値を何倍にするかを表しています.

##関数の説明
```py
    look_up_table = np.zeros((256,1), dtype = 'uint8')
```
の部分ですが, 変換用の対応表(256 × 1)を作成します.

現段階ではまだ以下のような値が格納されています.

```py
>>> look_up_table = np.zeros((256,1), dtype = 'uint8')
>>> look_up_table
array([[0],
       [0],
       [0],
       ...,
       [0],
       [0]], dtype=uint8)
>>> len(look_up_table)  
256
```
次の行からこの配列に参照する値を格納していきます.


```py
    for i in range(256):
        if i < 256 / n:
            look_up_table[i][0] = i * n
        else:
            look_up_table[i][0] = 255
```
の部分ですが, 先ほど作成した変換用の対応表に, 画素値を登録していきます.

また

```py
        if i < 256 / n:
```
の部分は条件式を一般化し, 画素値がn倍であっても動作するようになっています.
(ex: n=3[画素値3倍])

```py
    return cv2.LUT(frame, look_up_table)
```
の部分ですが, 先ほど登録したLUTによって対応関係をとり, 
変換された画像データを返します.

それでは, 結果です.

| 元画像 | 改良1 | 改良2 |
|:--:|:--:|:--:|
| ![元画像][cas1] | ![失敗画像2][cas3] | ![成功画像1][cas5] |
目的の画像を得ることができています.

実行時間(ms)についてみてみましょう.

|  | n = 2 | n = 3 | n = 4 |
|:-:|:-:|:-:|:-:|
| 改良1  | 4.9848556| 9.9725723  | 15.921831  |
| 改良2  | 4.9870014| 4.9834251  | 4.9870014  |
処理が速くなりました.

同様にして, 図2を改良していきます.

##図2の改良(Re:)

```py:変換関数
def toneCurve22(frame, n = 1):
    look_up_table = np.zeros((256,1), dtype = 'uint8')
    for i in range(256):
        if i < 256 - 256 / n :
            look_up_table[i][0] = 0
        else:
            look_up_table[i][0] = i * n - 255 * (n - 1)
    return cv2.LUT(frame, look_up_table)
```
やっていることはほぼ変わらないので, 詳しい説明は省きますが, 

```py
        if i < 256 - 256 / n :
```
の部分は, `n = 2` の時の関数部分で `x < 128` だったものを一般化したものです.
n = 2とすると
 `x < 256 - 256 / n = 128` つまり,
`x < 128` に変形できます.

それでは, 結果です.

| 元画像 | 改良1 | 改良2 |
|:--:|:--:|:--:|
| ![元画像][cas1] | ![失敗画像2][cas4] | ![成功画像1][cas6] |
目的の画像を得ることができています.

実行時間(ms)についてみてみましょう.

|  | n = 2 | n = 3 | n = 4 |
|:-:|:-:|:-:|:-:|
| 改良1  | 25.915145| 32.911539  | 36.406755  |
| 改良2  | 4.9862861| 4.9872398  | 4.9846172  |

やっぱり, 処理の早いものがいいですね.

#おわりに
今回は, トーンカーブについてだらだらと話してきました.

OpenCVってすごく便利ですね.
先人達には頭が上がりません...
しかも, Pythonならコマンド一つで使えるようになります.

この記事だれか見てくれるのだろうか?
とか, 色々不安になりながらも
これは備忘録だと, 保険をかけて
いつも投稿しています.

こんな記事もいつか誰かの役に立つといいですね.

さいごに今回用いたソースコードを張って終了とします.
次は何をしようかな?

それでは:wave:

#ソースコード
カレントディレクトリ下の
imageというディレクトリ内に
castle.jpgという画像ファイルが存在します.
このコードを使用される場合は, 各自変更してください.

また, 以下でライブラリをインストールしてください.

```console:コンソール
$ pip install opencv-python

$ pip freeze
numpy==1.18.4
opencv-python==4.2.0.34
```

```py:ex.py
import cv2
import numpy as np


def toneCurve(frame):
    return 2 * frame


def negaPosi(frame):
    return 255 - frame


def toneCurve11(frame, n = 1):
    tmp = frame
    if n > 1:
        for i in range(1, n):
            tmp = cv2.add(tmp, frame)
    return tmp


def toneCurve12(frame, n = 1):
    look_up_table = np.zeros((256, 1), dtype = 'uint8')
    for i in range(256):
        if i < 256 / n:
            look_up_table[i][0] = i * n
        else:
            look_up_table[i][0] = 255
    return cv2.LUT(frame, look_up_table)


def toneCurve21(frame, n = 1):
    if n > 1:
        return negaPosi(toneCurve11(negaPosi(frame), n))
    return frame


def toneCurve22(frame, n = 1):
    look_up_table = np.zeros((256, 1), dtype = 'uint8')
    for i in range(256):
        if i < 256 - 256 / n :
            look_up_table[i][0] = 0
        else:
            look_up_table[i][0] = i * n - 255 * (n - 1)
    return cv2.LUT(frame, look_up_table)


def main():
    img_path = '.\\image\\castle.jpg'
    img = cv2.imread(img_path)
    cv2.imwrite('.\\image\\tone00.jpg', toneCurve(img))
    cv2.imwrite('.\\image\\tone11.jpg', toneCurve11(img, 2))
    cv2.imwrite('.\\image\\tone12.jpg', toneCurve12(img, 2))
    cv2.imwrite('.\\image\\tone21.jpg', toneCurve21(img, 2))
    cv2.imwrite('.\\image\\tone22.jpg', toneCurve22(img, 2))

if __name__ == '__main__':
    main()
```

#参考
- [コンピュータ グラフィックス][cv]
- [Optie研](http://optie.hatenablog.com/entry/2018/03/03/141427#221-LUT%E3%81%A8%E3%81%AF) (はてなブログ)
- [Pythonの学習の過程とか](https://peaceandhilightandpython.hatenablog.com/entry/2016/02/05/004445) (はてなブログ)
- [画像取得元](https://pixabay.com/ja/photos/%E5%9F%8E-%E3%82%B9%E3%82%B3%E3%83%83%E3%83%88%E3%83%A9%E3%83%B3%E3%83%89-3619698/) (Pixabay)

#関連記事
- [Python & OpenCVで画像処理 [ Tone Curve編 ]][関連1]

[^1]: [コンピュータ グラフィックス][cv](p262)より抜粋

[cv]:https://www.amazon.co.jp/%E3%82%B3%E3%83%B3%E3%83%94%E3%83%A5%E3%83%BC%E3%82%BF%E3%82%B0%E3%83%A9%E3%83%95%E3%82%A3%E3%83%83%E3%82%AF%E3%82%B9-%E6%94%B9%E8%A8%82%E6%96%B0%E7%89%88/dp/4903474496
[t1]:https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/8defb57b-54a5-2ef9-5a12-bcf8c0c0cb12.png
[t2]:https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/debd9f7f-6380-67f1-542d-e504bb032208.png
[cas1]:https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/e2b1b898-8be2-4b57-8de7-1c9a176975e0.jpeg
[cas2]:https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/d7e6b375-fe26-ee78-653f-21aae60604de.jpeg
[cas3]:https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/ccaca514-ce2f-574f-8a23-0f299b6d0198.jpeg
[ori]:https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/30ce4739-d90d-a5ef-2145-f155fc959b1b.png
[np]:https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/70dada7a-f6db-80c3-abbf-6e773afe9f1a.png
[add]:https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/f4e508cf-6182-6c20-a779-1d299c331872.png
[renp]:https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/ae641d99-3255-5d99-4cb6-2ea890f1deef.png
[cas4]:https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/d1064732-05dc-5cce-2ad8-24e50bb6547a.jpeg
[cas5]:https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/40dd01ef-b4b2-1e2f-4309-8133a6bc65df.jpeg
[cas6]:https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/b9bbeaff-4c6a-f652-5cdc-9a0540349a3e.jpeg

[関連1]:https://qiita.com/RoaaaA/items/723b2b9409a6fc8bb781