search
LoginSignup
62

posted at

updated 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)
3x3.png 3x3eq.png
ヒストグラム
image.png image.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)

オリジナル写真 平坦化後
gray.png g.png
ヒストグラム
image.png image.png

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

カラー画像 (NG)

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

オリジナル写真 平坦化後
IMG_0837.jpg t0.png

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

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

オリジナル 平坦化後
3ddc032d.jpg t.png
ヒストグラム
image.png image.png

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

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

赤成分(red) 緑成分 (green) 青成分 (blue)
red.png green.png blue.png
元のヒストグラム
image.png image.png image.png
平坦化
red-eq.png green-eq.png blue-eq.png
平坦化ヒストグラム
image.png image.png image.png

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

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

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

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 t1.png
image.png image.png

いい感じです。

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

オリジナル ヒストグラム平坦化
IMG_0837.jpg t1.png
ヒストグラム
image.png image.png

緑を取り戻しました。

PIL 以外

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

ImageMagick

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

% convert IMG_0837.jpg -equalize IMG_0837-eq.jpg
オリジナル ImageMagick -equalize
IMG_0837.jpg IMG_0837-eq.jpg
ヒストグラム
image.png image.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 でも -channel 指定で !Sync すると、PIL のように R,G,B 独立して平坦化できます。

% convert im2758073.jpg -channel "RGB,\!Sync" -equalize imeq-nosync.jpg
オリジナル !Sync
im2758073.jpg imeq-nosync.jpg
ヒストグラム
image.png image.png

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

OpenCV

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

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'

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

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 を使うと良さそうです。

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 も利用できます。

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 t1.png t2.png
ヒストグラム
image.png image.png image.png

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)
IMG_1979.jpg im-equalize.jpg im-clahe.jpg
ヒストグラム
image.png image.png image.png

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
62