Python & OpenCVで画像処理 [ Tone Curve編 ]

#はじめに
この記事は, **Tone Curve**による画像処理の紹介がメインです.
教科書にTone Curveは載っているけど, コードは載っていない...
どんなコードで実現できるのか気になった人向けになるかもしれません.

Tone Curveを知らない方で, この記事を読んでくださる方は
簡単に[<この記事>][tc]の「導入」で知ってくださると, よいかと思います.

- **トーンカーブ**
- **出力画像(カラー, グレイスケール)**
- **変換関数**

の順に紹介します.

#動作環境
```
端末：Windows 10
コンソール：cmd(コマンドプロンプト)
python：3.6.8
仮想環境：venv
```

#目次
- [**ネガ・ポジ反転**](#ネガポジ反転)
- [**折れ線型トーンカーブ**](#折れ線型トーンカーブ)
    - [折れ線型トーンカーブ1](#折れ線型トーンカーブ1)
    - [折れ線型トーンカーブ2](#折れ線型トーンカーブ2)
- [**S字トーンカーブ**](#s字トーンカーブ)
- [**ガンマ変換のトーンカーブ**](#ガンマ変換のトーンカーブ)
- [**ソラリゼーション**](#ソラリゼーション)
- [**ポスタリゼーション**](#ポスタリゼーション)
- [**2値化**](#2値化)

#紹介
入力画像は以下の2つになります.

| カラー | グレイスケール |
|:-:|:-:|
| ![original.jpg][ori]  | ![grayScale.jpg][gray]  |
グレイスケール化に関してはOpenCVの`cv2.cvtColor()`を使いました.

カラー画像, グレイスケール画像がどのように変化するか
紹介していきます.

また, 関数からの返り値を `cv2.imwrite()` 関数などで読み込めば
保存できます.
##▷ネガ・ポジ反転
画素値をその名の通り反転させます.
(※これより, 数式中の *x* は画素値です.)

```math
f(x) = 255 - x
```
####トーンカーブ
|![nega_posi.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/14181ccb-3824-db0c-fe0a-1f57a237783f.png)|
|:-:|

####出力画像
- カラー

| input  | output  |
|:-:|:-:|
| ![input][ori] | ![output][ng1] |
- グレイスケール

| input | output |
|:-:|:-:|
| ![input][gray] | ![output][ng2]  |

####関数
```py:変換関数
def negaPosi(frame):
    return 255 - frame
```
##▷折れ線型トーンカーブ
以前紹介した[記事](https://qiita.com/RoaaaA/items/373f309f724c4b569749)と被ってしまいますが...
詳しく見たい方は, 記事を読んでくださるとありがたいです.

ここではn = 2についてのトーンカーブで出力します.
(※nは画素値を何倍するかの値です.)

###◆折れ線型トーンカーブ1
画像のコントラストを上げる変換です.

```math
f(x) = \begin{cases} 2 \cdot x & (x < 128) \\ 255 & (otherwise)
  \end{cases}
```
####トーンカーブ

|![tone_curve1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/66caff58-d168-e7a3-9228-9abb55df09e1.png)|
|:-:|

####出力画像
- カラー

| input  | output  |
|:-:|:-:|
| ![input][ori] | ![output][ore1c] |
- グレイスケール

| input | output |
|:-:|:-:|
| ![input][gray] | ![output][ore1g]  |

####関数
```py:変換関数
def toneCurve1(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)
```

###◆折れ線型トーンカーブ2
上に対してこちらはコントラストを下げる変換です.

```math
f(x) = \begin{cases} 0 & (x < 128) \\ 2 \cdot x - 255 & (otherwise)
  \end{cases}
```
####トーンカーブ
|![tone_curve2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/9a9909e8-7180-f112-b804-eebd99d8e94c.png)|
|:-:|

####出力画像
- カラー

| input  | output  |
|:-:|:-:|
| ![input][ori] | ![output][ore2c] |
- グレイスケール

| input | output |
|:-:|:-:|
| ![input][gray] | ![output][ore2g]  |

####関数
```py:変換関数
def toneCurve2(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)
```

##▷S字トーンカーブ
今回のS Tone Curveは, 明るいところはより明るく
暗いところはより暗く強調する変換です.

```math
f(x) = \frac {255}{2} \cdot \left\{\sin\left(\frac {x}{255} - \frac {1}{2}\right)\pi + 1\right\}
```
####トーンカーブ
|![s_tone_curve.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/570777dd-b891-b2ea-e9ea-0a99d3bd529a.png)|
|:-:|

####出力画像
- カラー

| input  | output  |
|:-:|:-:|
| ![input][ori] | ![output][sc] |
- グレイスケール

| input | output |
|:-:|:-:|
| ![input][gray] | ![output][sg]  |
画像がどこかクリアになりました.
####関数
```py:変換関数
def sToneCurve(frame):
    look_up_table = np.zeros((256,1), dtype = 'uint8')
    for i in range(256):
        look_up_table[i][0] = 255 * (np.sin(np.pi * (i/255 - 1/2)) + 1) / 2
    return cv2.LUT(frame, look_up_table)
```

##▷ガンマ変換のトーンカーブ
先ほどの折れ線型トーンカーブでは
一定値(0, 255)に変換され, 濃淡情報が失われてしまった部分があるが
ガンマ変換では, その部分の濃淡情報も残しつつ変換を行えます.

```math
f(x) = 255 \cdot \left(\frac {x}{255}\right)^\frac{1}{\gamma}
```
####トーンカーブ
いくつかの γ 値のパターンを示します.

|![gamma_curve.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/5e5ea88d-1b83-f9ef-a53d-3a225cd3ec82.png)|
|:-:|

####出力画像
- カラー

| input  | γ = 3  | γ = 2  |
|:-:|:-:|:-:|
| ![input][ori] | ![output][g3c] |![output][g2c]|
| **γ = 1**| **γ = 0.5**|**γ = 1 / 3**|
|![output][g1c]|![output][g05c]|![output][g033c]|
- グレイスケール

| input  | γ = 3  | γ = 2  |
|:-:|:-:|:-:|
| ![input][gray] | ![output][g3g] |![output][g2g]|
| **γ = 1**| **γ = 0.5**|**γ = 1 / 3**|
|![output][g1g]|![output][g05g]|![output][g033g]|
当たり前ですけど, γ = 1 とinputの画像は同じものになります.


####関数
```py:変換関数
def gammaCurve(frame, gamma = 1):
    look_up_table = np.zeros((256,1), dtype = 'uint8')
    for i in range(256):
        look_up_table[i][0] = pow(i / 255, 1 / gamma) * 255
    return cv2.LUT(frame, look_up_table)
```

##▷ソラリゼーション
ネガとポジの画像が混ざり合ったような画像になります.

私のソラリゼーションで用いる変換数式は以下のようになります.

```math
f(x) = \frac {255}{2} \cdot \sin\left\{3\pi\left(\frac {x}{255} - \frac {1}{2}\right)\right\}
```
####トーンカーブ
|![soralization.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/b95769ad-67fd-3b70-5dca-af1623c4645c.png)|
|:-:|

####出力画像
- カラー

| input  | output  |
|:-:|:-:|
| ![input][ori] | ![output][src] |
- グレイスケール

| input | output |
|:-:|:-:|
| ![input][gray] | ![output][srg]  |

####関数
```py:変換関数
def soralization(frame):
    look_up_table = np.zeros((256,1), dtype = 'uint8')
    for i in range(256):
        look_up_table[i][0] = (np.sin(3 * np.pi * (i / 255 + 1 / 2 )) + 1) * 255 / 2
    return cv2.LUT(frame, look_up_table)
```

##▷ポスタリゼーション
画素値を段階的に一定値化します.
絵画のような画像になります.

出力としては, 画素値を2, 3, 4段階に分けたパターンを出力します.

####トーンカーブ
*step* = 53 はおまけです(余白的な問題で).

| ![posterization2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/2ead4b77-3fce-ed81-860b-1c09a372fd44.png) | ![posterization3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/8da4f2f5-d8f2-35f1-4150-59b5bf73ad68.png) |
|:-:|:-:|
| ![posterization.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/7464c43e-fc40-d95c-d06a-e341be021b17.png) | ![posterization5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/5be897e2-4001-5b20-873d-dd8ddf86b37c.png) |


####出力画像
- カラー

| input  |step = 2 |
|:-:|:-:|
| ![input][ori] | ![output][p2c]  |
|  **step = 3** | **step = 4** |
|  ![output][p3c]  |![output][p4c]  |
- グレイスケール


| input  |step = 2 |
|:-:|:-:|
| ![input][gray] | ![output][p2g]  |
|  **step = 3** | **step = 4** |
|  ![output][p3g]  |![output][p4g]  |
ポストカードっぽくなりました.
####関数
```py:変換関数
def posterization(frame, step = 4):
    if 1 < step and step <= 256:
        look_up_table = np.zeros((256, 1), dtype = 'uint8')
        split = int(256 / (step - 1))
        up = int(256 / step)
        for i in range(256):
            if np.trunc(i / up) * split >= 255:
                look_up_table[i][0] = 255
            else:
                look_up_table[i][0] = np.trunc(i / up) * split
        return cv2.LUT(frame, look_up_table)
    else:
        return frame
```
多分このコードが思いつくのに一番苦労したきがします...
もっといい方法がございましたら, ご教授ください.

##▷2値化
閾値 `n` より
**小さいときの値を0(最小値)**
**大きいときの値を255(最大値)**にする変換です.

今回は使っていませんが, cv2ライブラリの中に[二値化の関数](http://labs.eecs.tottori-u.ac.jp/sd/Member/oyamada/OpenCV/html/py_tutorials/py_imgproc/py_thresholding/py_thresholding.html)があります.
`cv2.adaptiveThreshold()`, `cv2.threshold()` があります.

```math
f(x) = \begin{cases}
       0 & (x < n) \\ 255 &(otherwise)
\end{cases}
```
####トーンカーブ
今回, 閾値 n = {50, 128, 200} の場合について出力します.

|![thre_50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/d5bf549c-1c05-6ba6-5568-5bfce86ebe2e.png)|![thre_128.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/02182aa8-1933-6d29-6f98-26b5bdcdac8c.png)|![thre_200.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/afb76c37-20f2-af0b-8371-d6d4a497d810.png)|
|:-:|:-:|:-:|

####出力画像
- カラー

| input  |n = 50|
|:-:|:-:|:-:|:-:|
| ![input][ori] |![output][50c] |
|**n = 128**| **n = 200**|
|![output][128c] |![output][200c] |

- グレイスケール

| input  |n = 50|
|:-:|:-:|:-:|:-:|
| ![input][gray] |![output][50g] |
|**n = 128**|**n = 200**|
|![output][128g] |![output][200g] |


####関数
```py:変換関数
def threshold(frame, threshold_v = 50):
    frame[frame < threshold_v] = 0
    frame[frame >= threshold_v] = 255
    return frame
```

#メイン関数
```py:ex.py
import cv2
import numpy as np

#関数群(省略)

def main():
    img_path = '.\\image\\castle.jpg' #任意に画像を指定
    img = cv2.imread(img_path)
    frame = grayScale(img)
    #関数を呼び出し, 実行してください.
    #例:
    # cv2.imwrite('tone_changed.jpg', negaPosi(img))

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

#おわりに
今回いくつかのトーンカーブによる諧調変換をつらつらと紹介しました.
次は, フィルタによる画像処理をやっていこうと思っています.

UIを追加して, 実用性を上げるでもよしですね.

それでは:wave:

#参考
- [コンピュータ グラフィックス][cv]
- [画像取得元](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)

#関連記事
- [画像処理のトーンカーブについての話しをしよう ~LUTはすごいぞ~][tc]

[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

[tc]:https://qiita.com/RoaaaA/items/373f309f724c4b569749#%E5%B0%8E%E5%85%A5

[ori]:https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/20f3b750-7a57-c4ef-5f4d-e5eba1f208c1.jpeg
[gray]:https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/ecab733e-b36b-985e-281c-6fe151fe37bf.jpeg

[ng1]:https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/9393f3b3-519f-26af-bb3b-e1d95acce9b9.jpeg
[ng2]:https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/ae0371a2-7385-983c-1156-e089c5047307.jpeg

[ore1c]:https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/e7497bc7-ee7d-73db-111d-bd27a1b7d2d7.jpeg
[ore1g]:https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/9d48c060-a542-e7b9-10b7-3e0b4d113aa7.jpeg
[ore2c]:https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/6fefd423-1f28-3e7e-3f32-65c2734617c8.jpeg
[ore2g]:https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/60cc7b02-62be-98fd-eb06-baad881bb884.jpeg

[sc]:https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/c29c5d4f-8fd6-24d6-5b35-7217f8e6a525.jpeg
[sg]:https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/7ef532e9-c241-a9a3-7e55-79f2a1d248a6.jpeg

[g3c]:https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/1806b4e7-27a5-17b2-ba50-c8422e0f9c66.jpeg
[g2c]:https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/c8cdf4d5-d777-a87e-4f29-6d0f2b62073f.jpeg
[g1c]:https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/4ec6e5cd-21e6-21d8-8cca-3fab93ee5a5c.jpeg
[g05c]:https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/00201a76-6fb3-5c9c-ef5a-69567b234274.jpeg
[g033c]:https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/caa39640-b882-b2bf-b70d-9a1f949d69da.jpeg

[g3g]:https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/619ef04e-e6cc-4ab5-bb7e-11b6ddca779c.jpeg
[g2g]:https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/08f6a7cf-4274-98c6-182a-f49087045626.jpeg
[g1g]:https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/ba4946ae-f0fc-423d-7e7d-029e9304dedf.jpeg
[g05g]:https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/9bfee9db-ada4-57f5-c33a-f4802a974d3b.jpeg
[g033g]:https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/07175e39-7e10-9328-3e79-2b7716063f89.jpeg

[src]:https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/75a74950-be71-3719-9721-e1a2fc4e025a.jpeg
[srg]:https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/1e4ce7a1-2e9b-801f-e734-9c4a36f00481.jpeg

[p2c]:https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/ce32fe6f-7a65-e644-3121-5f8836406680.jpeg
[p3c]:https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/d1d6b40c-2e39-55f0-7456-48116fed3ef5.jpeg
[p4c]:https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/4557591d-50a8-244b-a815-e592e98192c7.jpeg

[p2g]:https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/f3eaab89-e15a-0087-eb49-049e94341d50.jpeg
[p3g]:https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/cef31cae-38d8-33a5-54a7-6cf32fd4b14e.jpeg
[p4g]:https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/5ff3e8f2-528e-d79e-d1b4-902e10d1b16a.jpeg

[50c]:https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/07184b29-9024-1898-302e-207f74ff9038.jpeg
[128c]:https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/155ee4dd-0bdb-85fa-e9fd-44f28841431c.jpeg
[200c]:https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/b960edcf-e414-1746-270a-9b18b28d7fb7.jpeg

[50g]:https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/02a0e178-38b9-ddfa-2cf6-1857070bd583.jpeg
[128g]:https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/24dc2950-6d9f-e05d-c3f9-59185ac47bcf.jpeg
[200g]:https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/ca01cf6b-0a2c-3edf-46c5-3f75c7942f0d.jpeg