画像処理のガンマ補正とコントラスト強調/低減を理解する

# はじめに
※今回のサンプル画像として、[乃木坂46の西野七瀬さん](https://www.amazon.co.jp/%E8%A5%BF%E9%87%8E%E4%B8%83%E7%80%AC1st%E3%83%95%E3%82%A9%E3%83%88%E3%83%96%E3%83%83%E3%82%AF%E3%80%8E%E3%82%8F%E3%81%9F%E3%81%97%E3%81%AE%E3%81%93%E3%81%A8%E3%80%8F-%E8%A5%BF%E9%87%8E-%E4%B8%83%E7%80%AC/dp/4087808378)の写真を使用します。元がいい写真なので、今回の処理は蛇足です。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3230781/5cb22f38-ef60-0dd6-dbf0-910a41225609.jpeg
" width ="300">

　画素値を補正する手法の一つにガンマ補正、コントラスト強調/低減があります。画素値の補正を行うことで、画像の印象を変更することができます。自身の理解を深めるために技術記事を書きます。n番煎じです。[OpenCVによる画像処理入門 改訂第3版](https://www.amazon.co.jp/OpenCV%E3%81%AB%E3%82%88%E3%82%8B%E7%94%BB%E5%83%8F%E5%87%A6%E7%90%86%E5%85%A5%E9%96%80-%E6%94%B9%E8%A8%82%E7%AC%AC3%E7%89%88-KS%E6%83%85%E5%A0%B1%E7%A7%91%E5%AD%A6%E5%B0%82%E9%96%80%E6%9B%B8-%E5%B0%8F%E6%9E%9D-%E6%AD%A3%E7%9B%B4/dp/4065301173/ref=asc_df_4065301173/?tag=jpgo-22&linkCode=df0&hvadid=624463547642&hvpos=&hvnetw=g&hvrand=15962302074307461325&hvpone=&hvptwo=&hvqmt=&hvdev=c&hvdvcmdl=&hvlocint=&hvlocphy=1009637&hvtargid=pla-1885578354178&psc=1&mcid=729d867698b737e38612d1a9e0901b93&th=1&psc=1)を参考に実装しました。

# ガンマ補正
## 計算方法
　以下のような画素値を変更させる関数をガンマ関数といいます。この時、入力の画素値を $I_{src}$ 、出力の画素値を $I_{dst}$ とします。
```math
I_{dst} = 255 \times (\frac{I_{src}}{255}) ^ {1/\gamma}
```
$\gamma$ の値を変えたガンマ関数に対し、$0$ から$255$ の任意の値を入力した際の出力値を以下に示します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3230781/42e9759c-2433-d411-15a4-a6c703feafa0.png
" width ="300">
　上記の入出力をみるとわかるように、$\gamma$ の値をいじることで、画像内の暗い箇所を明るく、または明るい箇所を暗く変換することが可能です。例えば、$\gamma = 5$ の画像では画像内の暗い（＝画素値が小さい）部位の画素値を大きくすることで、画像を明るく変換します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3230781/43f5039d-e2a6-543f-4510-f3406ddf861d.png" width ="500">
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3230781/1d8c6c7e-c182-7369-5744-63e68c4876fb.png
" width ="500">

## 実装
```py
import cv2
import matplotlib.pyplot as plt
import numpy as np

path = r"your_directory_path"
name_img = r"\nishino_1.jpg"
path_img = path + name_img
img = cv2.imread(path_img)
img_g = cv2.cvtColor(img_rgb, cv2.COLOR_BGR2GRAY)

# gamma_correction
def gamma_correction(value, gamma):
    converted_value = 255 * pow((value/255), 1/gamma)
    
    return converted_value


gamma = [5,3,1,0.33, 0.2]

fig, axes = plt.subplots(2,3, tight_layout=True)
axes = axes.flatten()
axes[0].imshow(img_g, cmap = "gray")
axes[0].set_title("original")

fig2, axes2 =  plt.subplots(2,3, tight_layout=True)
axes2 = axes2.flatten()
axes2[0].hist(img_g.ravel(), 256, [0,256])
axes2[0].set_title("original")

for ax, g in enumerate(gamma):

    gamma_image = np.zeros(img_g.size).reshape(img_g.shape)
    for i in range(gamma_image.shape[0]):
        for j in range(gamma_image.shape[1]):
            gamma_image[i,j] = gamma_correction(img_g[i,j], g)

    axes[ax+1].imshow(gamma_image, cmap = "gray")
    axes[ax+1].set_title(f"gamma: {g}")

    axes2[ax+1].hist(gamma_image.ravel(), 256, [0,256], label = "numpy")
    # calc by cv2
    x = np.arange(256)
    y = (x / 255) ** (1 / g) * 255
    gamma_image_cv2 = cv2.LUT(img_g, y)
    axes2[ax+1].hist(gamma_image_cv2.ravel(), 256, [0,256], label= "cv2")
    axes2[ax+1].set_title(f"gamma: {g}")
    axes2[ax+1].legend()

plt.show()

# calc tone curve
for g in gamma:
    value = np.zeros(256)
    for i in range(len(value)):
        value[i] = gamma_correction(i, g)
    plt.plot(value, label = f"gamma: {g}")
    
plt.title("tone_curve")
plt.legend()
plt.ylabel("corrected_value")
plt.xlabel("original_value")
plt.show()
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3230781/8a47eb55-d5c3-c89c-54f3-a7d16994fb93.png
" width ="500">
numpy によるスクラッチで、OpenCV の出力と同じ出力結果が得られました。

# コントラスト強調/低減
## 計算方法
　コントラスト低減は以下の式で表されます。この時、入力の画素値を $I_{src}$ 、出力の画素値を $I_{dst}$ とします。また、二つのパラメーター $MIN$ 、 $MAX$ を使用します。
 ```math
 I_{dst} = MIN + \frac{I_{src}}{255}(MAX - MIN) 
 ```
 同様に、コントラスト強調は以下の式で表されます。
  ```math
  I_{dst} = 
  \begin{cases}
  0 \quad (I_{src} < MIN)\\
  255 \frac{I_{src} - MIN}{MAX - MIN} \quad (MIN < I_{src} < MAX)\\
  255 \quad (MAX < I_{src})
  \end{cases}
  ```
  <img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3230781/3f8b6d74-56e8-acc1-ba75-74e737888601.png
 width ="500">
　トーンカーブを確認すると、コントラスト低減では $MIN$ から $MAX$ の間に元画像の画素値が収まるように変換します。一方、コントラスト強調では$MIN$、$MAX$ をスレショルドに元画像の画素値をそれぞれ $0$、$255$ に変換し、 $MIN$ から $MAX$ の間を線形でつないでいることがわかります
   <img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3230781/dbed9d9d-8bd2-9569-161f-f78b666789f1.png
 width ="500">
    <img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3230781/a6384f0f-79c7-54de-4199-c5c3b96e2d80.png
 width ="500">
 
## 実装
```py
# enhance/reduce contrast
def contrast_enhancement(image_value, min, max):
    if image_value < min:
        enhanced_value = 0
    elif min <= image_value  <= max:
        enhanced_value = 255 * (image_value - min)/(max - min)
    else:
        enhanced_value = 255
    return enhanced_value

def contrast_reduction(image_value, min, max):
    reduced_value = min + image_value/255 * (max-min)
    return reduced_value

# parameter
min = 100
max = 230

image_enhanced = np.zeros(img_g.size).reshape(img_g.shape)
image_reduced = np.zeros(img_g.size).reshape(img_g.shape)
for i in range(img_g.shape[0]):
    for j in range(img_g.shape[1]):
        image_enhanced[i,j] = contrast_enhancement(img_g[i,j], min, max)
        image_reduced[i,j]  = contrast_reduction(img_g[i,j], min, max)

# calc by cv2
lut_enhanced = np.zeros(256)
for i in range(0, min):
    lut_enhanced[i] = 0
for i in range(min, max):
    lut_enhanced[i] = 255 * (i - min) / (max - min)
for i in range(max, 255):
    lut_enhanced[i] = 255

image_enhanced_cv2 = cv2.LUT(img_g, lut_enhanced)

lut_reduced = np.zeros(256)
for i in range(0,255):
    lut_reduced[i] = min + i * (max - min) / 255 
image_reduced_cv2 = cv2.LUT(img_g, lut_reduced)

fig, axes = plt.subplots(1,3, tight_layout=True)
axes[0].imshow(img_g, cmap = "gray")
axes[0].set_title("original")

axes[1].imshow(image_enhanced, cmap = "gray")
axes[1].set_title("enhanced")

axes[2].imshow(image_reduced, cmap = "gray")
axes[2].set_title("reduced")

fig2, axes2 = plt.subplots(1,3, tight_layout=True)
axes2[0].hist(img_g.ravel(), 256, [0,256])
axes2[0].set_title("original")

axes2[1].hist(image_enhanced.ravel(), 256, [0,256], label = "numpy")
axes2[1].hist(image_enhanced_cv2.ravel(), 256, [0,256], label = "cv2")
axes2[1].set_title("enhanced")
axes2[1].legend()

axes2[2].hist(image_reduced.ravel(), 256, [0,256], label = "numpy")
axes2[2].hist(image_reduced_cv2.ravel(), 256, [0,256], label = "cv2")
axes2[2].set_title("reduced")
axes2[2].legend()

plt.show()

# calc tone curve
value_reduced = np.zeros(256)
value_enhanced = np.zeros(256)
for i in range(len(value)):
    value_reduced[i] = contrast_reduction(i, min, max)
    value_enhanced[i] = contrast_enhancement(i, min, max)
plt.plot(value_reduced, label = "contrast_reduction")
plt.plot(value_enhanced, label= "contrast_enhancement")
plt.title(f"contrast enhancement/reduction, min:{min}, max:{max}")
plt.legend()
plt.show()
```
<img src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3230781/3b4b8752-4486-df35-ef80-772afb865268.png"　width ="500">
numpy によるスクラッチで、OpenCV の出力と同じ出力結果が得られました。

# おわりに
ガンマ補正、コントラスト強調/低減を理解するために、自身の学びを出力しました。違っている点など見つかれば適宜修正します。ご指摘いただけると感謝です。