PIL でカラー画像を Equalize(ヒストグラム平坦化)する際の注意

# Equalize(ヒストグラム平坦化)

みんな大好き、ヒストグラム平坦化。
輝度ヒストグラムの偏りを減らす事で視認性を向上させる代表的なアルゴリズムです。

例えば、暗い方に色の輝度が偏っている画像があるとします。

```shell
% echo "P2 3 3  31 \
  31 7 6 \
   5 4 3  \
   2 1 0 " | convert - -sample 300x300 3x3.png
% convert 3x3.png -equalize 3x3eq.png
```

以下のように輝度の階調を均すと、見た目潰れかかっている階調が改善する傾向にあります。
(ヒストグラムは横軸が輝度、縦軸は該当輝度のカウント数を最大値で正規化)

|実験画像(3x3.png)| ヒストグラム平坦化(3x3eq.png) |
|---|---|
|![3x3.png](https://qiita-image-store.s3.amazonaws.com/0/1798/298eeba0-26c4-7da7-1c52-2926b0ded04d.png) | ![3x3eq.png](https://qiita-image-store.s3.amazonaws.com/0/1798/5af61a59-9d91-8200-3664-b623d882554d.png) |
|ヒストグラム| |
|![image.png](https://qiita-image-store.s3.amazonaws.com/0/1798/cbc384cd-8514-197d-6bf7-cae86be8008e.png) | ![image.png](https://qiita-image-store.s3.amazonaws.com/0/1798/a5a89f8c-1648-68b6-78fc-57118e494dd7.png) |

画像認識の前処理でよく使われます。(お世話になっております！)

サンプルデモを作ってみたので、とりあえず画像ファイルを試しに放り込んでみて下さい。

- https://app.awm.jp/image.js/histogram.html

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">暗がりで大したもの見えないだろうと油断してアップロードした写真も、ヒストグラム平坦化かけると細部まで見える事があるので注意しようね。( ^ω^ )
https://app.awm.jp/image.js/equalize.html
ここに画像ファイルを放り込むと試せるYO！ </p>&mdash; ＼助けよや／ (@yoya) <a href="https://twitter.com/yoya/status/1464612438452498437">2021年11月28日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

# PIL の Equalize(ヒストグラム平坦化)

Python で画像処理によく使われる PIL (Pillow) だと ImageOps で処理出来ます。

```python
from PIL import Image, ImageOps
import sys 

img = Image.open(sys.argv[1])
img = ImageOps.equalize(img);
img.show()
```

## グレースケール画像 (OK)

|オリジナル写真|平坦化後|
|---|---|
|![gray.png](https://qiita-image-store.s3.amazonaws.com/0/1798/91126486-415a-91c3-0c48-de1768132c52.png)|![g.png](https://qiita-image-store.s3.amazonaws.com/0/1798/2532f22c-071f-eace-00d8-3d49e373fc4d.png)|
|ヒストグラム| |
|![image.png](https://qiita-image-store.s3.amazonaws.com/0/1798/eb8167d9-7cc8-5814-08a2-d09dcd2f68a6.png) | ![image.png](https://qiita-image-store.s3.amazonaws.com/0/1798/210b5fd2-c421-3b9f-4422-ffb07decf337.png) |

<!--  何故か見れない > ![image.png](https://qiita-image-store.s3.amazonaws.com/0/1798/634cc07b-6489-57f7-d5f4-d2f5e923f65b.png) -->

暗がりで殆ど何も見えない写真が、細部までよく見えるようになります。

## カラー画像 (NG)

さて、ここから本題です。
カラー画像でも ImageOps で equalize 出来ますが。そのまま使うと色味がおかしくなる落とし穴があります。

|オリジナル写真|平坦化後|
|---|---|
![IMG_0837.jpg](https://qiita-image-store.s3.amazonaws.com/0/1798/2c9b7bbd-cc09-286a-977b-847a90d5b9e7.jpeg) | ![t0.png](https://qiita-image-store.s3.amazonaws.com/0/1798/2b3af592-f327-2b2a-3dc7-37d8ae20ba9c.png)|

地面も含めて緑のはずですが、いくつか紫に変わっています。

こちらのイラスト画像の方が違いがわかりやすいので、サンプルとして使わせて頂きます。

- http://seiga.nicovideo.jp/seiga/im2758073

|オリジナル|平坦化後|
|---|---|
|![3ddc032d.jpg](https://qiita-image-store.s3.amazonaws.com/0/1798/8238636f-8023-7d53-46de-696af38cb8f1.jpeg)|![t.png](https://qiita-image-store.s3.amazonaws.com/0/1798/c8df35ed-3b9c-43f0-a154-d7fc0f9209bd.png)|
|ヒストグラム| |
|![image.png](https://qiita-image-store.s3.amazonaws.com/0/1798/0c7060c9-8de2-f0da-777b-7eb7ee8fe7f4.png) | ![image.png](https://qiita-image-store.s3.amazonaws.com/0/1798/d22c36cd-496c-1e01-8599-3b03985363e9.png)|

元の画像では明るい赤が多かった為に、赤味が全体的に減るよう補正されています。特に髪の色が緑に変わってしまってますね。

ヒストグラムの累積グラフ(ななめ線)を見ると、R と G と B をバラバラにヒストグラム平坦化して、混ぜて RGB 画像にしているのが推察できます。

|赤成分(red) | 緑成分 (green) | 青成分 (blue)
|---|---|---|
| ![red.png](https://qiita-image-store.s3.amazonaws.com/0/1798/2983847d-de87-4f52-cec4-a982cf7e4a97.png) |  ![green.png](https://qiita-image-store.s3.amazonaws.com/0/1798/53c030dd-0a70-7ea5-3b1a-97ed44604a09.png) | ![blue.png](https://qiita-image-store.s3.amazonaws.com/0/1798/d743fee6-5ed4-ccc9-950d-cbd2f170a5ec.png) |
|元のヒストグラム | | |
| ![image.png](https://qiita-image-store.s3.amazonaws.com/0/1798/57c30a2c-4896-0e9f-569e-7066595289a0.png) |![image.png](https://qiita-image-store.s3.amazonaws.com/0/1798/9d87d754-4113-285c-758b-589ed1ef6940.png) | ![image.png](https://qiita-image-store.s3.amazonaws.com/0/1798/5ec2fd3a-9d14-ba41-43d6-3f54d4b6d7f5.png)
|平坦化 | | |
|![red-eq.png](https://qiita-image-store.s3.amazonaws.com/0/1798/1f959da8-4ff7-383b-742d-53250987b613.png) |  ![green-eq.png](https://qiita-image-store.s3.amazonaws.com/0/1798/6ff4d3a2-29b9-feb0-64f3-f150d25aedde.png) | ![blue-eq.png](https://qiita-image-store.s3.amazonaws.com/0/1798/19c1ba91-93a7-11c7-2f5a-2b1266291dc3.png) |
|平坦化ヒストグラム | | |
| ![image.png](https://qiita-image-store.s3.amazonaws.com/0/1798/89d18b7b-9901-f407-2ca5-a3abe3749225.png) | ![image.png](https://qiita-image-store.s3.amazonaws.com/0/1798/d7e1cb18-b612-8881-848f-f2c9334f20e0.png) | ![image.png](https://qiita-image-store.s3.amazonaws.com/0/1798/5e3c0223-e6e3-01a5-7aae-62d9b8313b5f.png) |

このように色成分毎にバラバラに補正をすると色味が維持されません。

## カラー画像の対応 (輝度だけヒストグラム平坦化)

輝度と色味を分ける色空間 YCbCr (Y:輝度、Cb:青の色差、Cr:赤の色差) を利用するとよいです。
色味の問題は、RGB を一旦この YCbCr に変換して Y だけヒストグラム平坦化すれば解決します。

https://gist.github.com/yoya/8670e7dd2c91b9bf59dd90ecd19eb323

```python
from PIL import Image, ImageOps
import sys

im = Image.open(sys.argv[1])
im = im.convert("YCbCr")
yy, cb, cr = im.split()

yy = ImageOps.equalize(yy);
im = Image.merge("YCbCr", (yy, cb, cr))

im = im.convert("RGB")
im.show()
# im.save(sys.argv[2])
```

|オリジナル|平坦化後|
|---|---|
|![3ddc032d.jpg](https://qiita-image-store.s3.amazonaws.com/0/1798/8238636f-8023-7d53-46de-696af38cb8f1.jpeg)| ![t1.png](https://qiita-image-store.s3.amazonaws.com/0/1798/7a3b428c-17d2-578a-c701-456e55cd1e12.png) |
| ![image.png](https://qiita-image-store.s3.amazonaws.com/0/1798/0c7060c9-8de2-f0da-777b-7eb7ee8fe7f4.png) | ![image.png](https://qiita-image-store.s3.amazonaws.com/0/1798/03c184b3-e479-1776-47ae-a4372b7a96ee.png)|

いい感じです。

では、コントラストの低い実際の写真を処理してみます。

| オリジナル | ヒストグラム平坦化 |
|---|---|
|![IMG_0837.jpg](https://qiita-image-store.s3.amazonaws.com/0/1798/4b6efd0d-6ea5-c324-c6ea-5c4b1b27ba22.jpeg) |![t1.png](https://qiita-image-store.s3.amazonaws.com/0/1798/a4c8eb88-5c95-a59f-05fa-4c44f7985419.png)|
|ヒストグラム | | 
| ![image.png](https://qiita-image-store.s3.amazonaws.com/0/1798/ac1bb497-9a4e-06e2-cb35-fe19b69a75d4.png) | ![image.png](https://qiita-image-store.s3.amazonaws.com/0/1798/a5246b18-14c8-ea0d-77ce-299750d55eb5.png) | 

緑を取り戻しました。

# PIL 以外

ついでに、ImageMagick と OpenCV についても少し触れます。

## ImageMagick

ImageMagick の -equalize オプションで実行するヒストグラム平坦化は色味を壊さずに処理します。

```shell
% convert IMG_0837.jpg -equalize IMG_0837-eq.jpg
```

|オリジナル | ImageMagick -equalize |
|---|---|
|![IMG_0837.jpg](https://qiita-image-store.s3.amazonaws.com/0/1798/4b6efd0d-6ea5-c324-c6ea-5c4b1b27ba22.jpeg) | ![IMG_0837-eq.jpg](https://qiita-image-store.s3.amazonaws.com/0/1798/8d4a586d-1c87-5ea7-fbc7-3e67680f70f8.jpeg)|
|ヒストグラム | | 
| ![image.png](https://qiita-image-store.s3.amazonaws.com/0/1798/ac1bb497-9a4e-06e2-cb35-fe19b69a75d4.png) | ![image.png](https://qiita-image-store.s3.amazonaws.com/0/1798/cf5f4dd7-02f1-afe0-19b1-6d8caa3eecfa.png)|

実装としては、R,G,B をバラバラに256個のヒストグラムで処理するのでなく、 R1,G1,B1,R2,G2,G3 のように並べて 256x3=>768個のヒストグラムで処理するだけの単純なものです。
中間処理とはいえ3倍の粒度で均すおかげか出来あがる画像も綺麗な気がします。

ちなみにですが、ImageMagick-6.7.9-3(2012年2月リリース)までは PIL 同様、色味を維持しませんでした。6.8.6-3(2013年6月)以降、今の挙動に落ち着いています。 

- ImageMagick の equalize の昔話
  - https://qiita.com/yoya/items/be70bfa89ad8456202b0

なお、ImageMagick でも -channel 指定で !Sync すると、PIL のように R,G,B 独立して平坦化できます。

```shell
% convert im2758073.jpg -channel "RGB,\!Sync" -equalize imeq-nosync.jpg
```

|オリジナル| !Sync |
|---|---|
|![im2758073.jpg](https://qiita-image-store.s3.amazonaws.com/0/1798/8238636f-8023-7d53-46de-696af38cb8f1.jpeg)|![imeq-nosync.jpg](https://qiita-image-store.s3.amazonaws.com/0/1798/cd3a5833-1837-45b5-2ea1-1652b0a5d391.jpeg)|
|ヒストグラム| |
|![image.png](https://qiita-image-store.s3.amazonaws.com/0/1798/0c7060c9-8de2-f0da-777b-7eb7ee8fe7f4.png) |![image.png](https://qiita-image-store.s3.amazonaws.com/0/1798/b224ae03-7f18-8790-c1de-d8e1bbb27096.png)|

 !Sync は他の用途やデバッグにも便利なので、豆知識としてどうぞ。

## OpenCV

OpenCV は equalizeHist で処理出来ます。
ただし、カラー画像を渡すとエラーになります。

```python
import sys
import cv2

img = cv2.imread(sys.argv[1], cv2.IMREAD_COLOR)
img= cv2.equalizeHist(img)
cv2.imshow('equalize', img)
cv2.waitKey(0)
```

```
% python equalize_test.py
Traceback (most recent call last):
  File "equalize_test.py", line 5, in <module>
    img= cv2.equalizeHist(img)
cv2.error: OpenCV(4.4.0) /
/(略)/opencv/modules/imgproc/src/histogram.cpp:3439: error: (-215:Assertion failed) _src.type() == CV_8UC1 in function 'equalizeHist'
```

以下のように、グレースケール画像を渡す必要があります。

```python
import sys
import cv2

img = cv2.imread(sys.argv[1], cv2.IMREAD_GRAYSCALE)
img= cv2.equalizeHist(img)
cv2.imshow('equalize', img)
cv2.waitKey(0)
```

カラー画像を処理したい時は、PIL と同様に YCbCr を使うと良さそうです。

```python
import sys
import cv2

img = cv2.imread(sys.argv[1])
img_yuv = cv2.cvtColor(img, cv2.COLOR_BGR2YUV)
img_yuv[:,:,0] = cv2.equalizeHist(img_yuv[:,:,0])
img = cv2.cvtColor(img_yuv, cv2.COLOR_YUV2BGR)
cv2.imshow('equalize', img)
cv2.waitKey(0)
```

むしろ PIL もグレイスケール画像以外を渡したらエラーにして欲しいですね。罠になっちゃってるので。

# より高度な平坦化 (CLAHE)

実は、今までのヒストグラム平坦化は画像全体のヒストグラムをとるので、場所によって視認性がむしろ下がる事もあり得ます。そこでさらに高度な手法が考えられました。

- 適応ヒストグラム平坦化(AHE, Adaptive Histogram Equalization)
    - ブロックに分けてバラバラにヒストグラム平坦化処理を行い、線形補完でマージする
- コントラスト制限適応ヒストグラム平坦化(CLAHE, Contrast Limited Adaptive Histogram Equalization)
    - AHE の改良で、ブロックに分けた分だけ局所的なノイズに弱くなるので、ヒストグラムのビンが高すぎる場合に他にバラす。

## OpenCV で CLAHE

OpenCV では CLAHE も利用できます。

- https://docs.opencv.org/3.1.0/d5/daf/tutorial_py_histogram_equalization.html
- https://en.wikipedia.org/wiki/Adaptive_histogram_equalization#Contrast_Limited_AHE

```python
import sys
import cv2

img = cv2.imread(sys.argv[1])
img_yuv = cv2.cvtColor(img, cv2.COLOR_BGR2YUV)

clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
img_yuv[:,:,0] = clahe.apply(img_yuv[:,:,0])

img = cv2.cvtColor(img_yuv, cv2.COLOR_YUV2BGR)
cv2.imshow('CLAHE', img)
cv2.waitKey(0)
```

CLAHE はパラメータ調整が手間なのと、ただの Equalize の方が都合の良い場合もあります。

|オリジナル写真 | Equalize(OpenCV) | CLAHE(OpenCV) |
|---|---|---|
| ![IMG_1979.jpg](https://qiita-image-store.s3.amazonaws.com/0/1798/3ad5349d-0b48-94d9-7ca2-7b4c507ac836.jpeg) | ![t1.png](https://qiita-image-store.s3.amazonaws.com/0/1798/e45b8a92-b6ad-5e4b-1f79-e1ae26e2518a.png) | ![t2.png](https://qiita-image-store.s3.amazonaws.com/0/1798/c773103b-08b3-fbe7-122a-7b05d0f8ca59.png) |
|ヒストグラム| | |
|![image.png](https://qiita-image-store.s3.amazonaws.com/0/1798/2454ca27-4207-47b7-cc79-5ac5478bdd4d.png) | ![image.png](https://qiita-image-store.s3.amazonaws.com/0/1798/97081fa2-b942-e15c-ae2d-9285c13730d7.png) | ![image.png](https://qiita-image-store.s3.amazonaws.com/0/1798/31cf17a9-e8a0-b18f-fd38-5f506e9edfd8.png) |

CLAHE の方が看板の文字は読み易いのですが、普通の Equalize で浮かび上がっていた背景が闇のとばりに隠れていますね。

一長一短なので、状況に応じて使い分けると幸せになれるでしょう。

## (追記) ImageMagick で CLAHE (ImageMagick 7.0.8-15以降)


ImageMagick-7.0.8-15 から -clahe オプションが導入されました。尚、何故か 6 系にはバックポートされていません。

-  usage
   - https://imagemagick.org/script/clahe.php

```shell
% convert original.jpg  -equalize           im-equalize.jpg
% convert original.jpg  -clahe 20x25%+256+7 im-clahe.jpg
```

|オリジナル写真 | Equalize(ImageMagick) | CLAHE(ImageMagick) |
|---|---|---|
| ![IMG_1979.jpg](https://qiita-image-store.s3.amazonaws.com/0/1798/3ad5349d-0b48-94d9-7ca2-7b4c507ac836.jpeg) |![im-equalize.jpg](https://qiita-image-store.s3.amazonaws.com/0/1798/76bf619b-39f1-99e6-a9e9-7f6445b94889.jpeg) |  ![im-clahe.jpg](https://qiita-image-store.s3.amazonaws.com/0/1798/d3eb567c-76bb-7bee-cef5-e84bdc358d34.jpeg)|
|ヒストグラム| | |
|![image.png](https://qiita-image-store.s3.amazonaws.com/0/1798/2454ca27-4207-47b7-cc79-5ac5478bdd4d.png)  | ![image.png](https://qiita-image-store.s3.amazonaws.com/0/1798/ee370f81-9d7a-8858-4fc9-8466e5115275.png) | ![image.png](https://qiita-image-store.s3.amazonaws.com/0/1798/0bd0d2ac-3bf3-d96c-860c-4b30fa29520b.png) |