Edited at

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


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

みんな大好き、ヒストグラム平坦化。

輝度ヒストグラムの偏りを減らす事で視認性を向上させる代表的なアルゴリズムです。

例えば、暗い方に色の輝度が偏っているケース。

% 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)


ヒストグラム


(ヒストグラムは横軸が輝度、縦軸は該当輝度のカウント数を最大値で正規化)

このように輝度の階調を均すと、見た目潰れかかっている階調が改善する傾向にあります。

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

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


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

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

from PIL import Image, ImageOps

import sys

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


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

オリジナル写真
平坦化後


ヒストグラム


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


カラー画像 (NG)

さて、ここから本題です。

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

オリジナル写真
平坦化後


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

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

オリジナル
平坦化後


ヒストグラム


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

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

赤成分(red)
緑成分 (green)
青成分 (blue)



元のヒストグラム



平坦化



平坦化ヒストグラム



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


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

輝度と色味を分ける色空間 YCbCr (Y:輝度、Cb:青の色差、Cr:赤の色差) を利用するとよいです。

色味の問題は、RGB を一旦この YCbCr に変換して Y だけヒストグラム平坦化すれば解決します。

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

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])

オリジナル
平坦化後



いい感じです。

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

オリジナル
ヒストグラム平坦化


ヒストグラム


緑を取り戻しました。


PIL 以外

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


ImageMagick

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

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

オリジナル
ImageMagick -equalize


ヒストグラム


実装的には、R,G,B をバラバラに256個のヒストグラムで処理するのでなく、 R1,G1,B1,R2,G2,G3 のように並べて 256x3個のヒストグラムで処理するだけの単純なものです。

擬似的に3倍の粒度で均すおかげか、出来あがる画像も綺麗な気がします。もしくはRGBの見た目の輝度差を気にせずヒストグラムを作る為に本来の色合いと変わって、たまたまキラキラして見えている可能性もありますね。

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

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

オリジナル
!Sync


ヒストグラム


使うことはないでしょうけど豆知識として。


OpenCV

OpenCV は equalizeHist で処理出来ます。

import sys

import cv2

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

ただし、imread の引数を見ての通り、グレースケール画像で渡す必要があります。

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

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)といった、より改良されたアルゴリズムがあります。


OpenCV で CLAHE

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

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)



ヒストグラム



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

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


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

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

% convert original.jpg  -equalize           im-equalize.jpg

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

オリジナル写真
Equalize(ImageMagick)
CLAHE(ImageMagick)



ヒストグラム