はじめに
※今回のサンプル画像として、乃木坂46の西野七瀬さんの写真を使用します。元がいい写真なので、今回の処理は蛇足です。
画像の画素値に偏りがあると、輪郭が見にくいなどの問題が生じます。
これらの画素値ヒストグラムの分布が均一になるように処理を行うことで、画像の濃淡を明瞭にさせる処理をヒストグラム均一化といいます。
今回はこのヒストグラム均一化の処理について、理解を深めるために技術記事を書きます。n番煎じです。OpenCVによる画像処理入門 改訂第3版を参考に実装しました。
計算方法
画像の高さを $x$、幅を $y$ とします。各画素値 (0~255) が画像中に出現した回数をまとめたヒストグラムを作成します。
この度数を画素値(v)ごとに累積したもの(累積度数, $H_v$)、及び累積度数を総画素数で割った累積度数の比率($c_v$)を計算します。ここでは $N$ を総画素数、$h_i$ を各度数分布、$INT$ (= integer) を整数化関数とします。
H_v = \sum_{n = 1}^v h_i
c_v = \frac{H_v}{N}
均一化後の画素値 ($e_{x,y}$) を計算します。ここでは画像の座標 $x, y$における画素値を $v_{x,y}$ 、この$v_{x,y}$に対応した$c_v$ の値を $c_{x,y}$ 、$0$ を除いた $c_v$ の最小値を $c_0$ とします。これにより、ヒストグラム均一化後の画像が得られます。
e_{x,y} = INT(\frac{c_{x,y} - c_0}{1-c_0}) \times 255
実装
実装に際し、opencv とnumpy のスクラッチによる実装を記述します。
import cv2
import matplotlib.pyplot as plt
import numpy as np
path = r"directory path"
name_img = r"\nishino_1.jpg"
path_img = path + name_img
img = cv2.imread(path_img)
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img_g = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# check image
plt.imshow(img_rgb)
# scratch with numpy
# calculate image histogram
hist, bin = np.histogram(img_g.ravel(), 256, [0,256])
# cumulative sum function of hist
cdf = hist.cumsum()
# regularization of cdf
cdf_ratio = cdf/img_g.size
# mask 0 value
cdf_masked = np.ma.masked_equal(cdf_ratio, 0)
# create equalization image
eq_image = np.zeros(img_g.size).reshape(img_g.shape)
for i in range(eq_image.shape[0]):
for j in range(eq_image.shape[1]):
img_value = img_g[i,j]
eq_image[i,j] = int((cdf_ratio[img_value]-cdf_masked.min())/(1-cdf_masked.min())*255)
# with cv2
eq_g = cv2.equalizeHist(img_g)
# visualize
plt.imshow(eq_image, cmap = 'gray')
plt.title("equalized image with numpy")
plt.show()
plt.imshow(eq_g, cmap="gray")
plt.title("equalized image with cv2")
plt.show()
# plot histogram
plt.hist(eq_g.ravel(), 256, [0,256], label="cv2")
plt.hist(eq_image.ravel(), 256, [0,256], label="numpy")
plt.title("density histogram")
plt.legend()
plt.show()
均一化前後のヒストグラムを比較すると、均一化により画素値のスケールを幅広く使用していることがわかります。
均一化に関する関数に0~255 の画素値を入力した際の出力値を確認すると、全体的に画素値を低く変換していることがわかります。
val = np.zeros(256)
for i in range(len(val)):
val[i] = int((cdf_ratio[i]-cdf_masked.min())/(1-cdf_masked.min())*255)
plt.plot(val, label = "conversion curve")
plt.plot(np.arange(len(val)))
plt.xlabel("original_pixel_value")
plt.ylabel("equalized_pixel_value")
plt.legend()
plt.show()
おまけ
グレースケール他に、RGB空間の画像についてもヒストグラム均一化を試してみました。処理はRGB分割後、それぞれのチャネルでヒストグラム均一化をかけて、再度各チャネルを再結合させました。
r, g, b = cv2.split(img_rgb)
eq_r = cv2.equalizeHist(r)
eq_g = cv2.equalizeHist(g)
eq_b = cv2.equalizeHist(b)
eq_rgb = cv2.merge((eq_r, eq_g, eq_b))
fig, ax = plt.subplots(1, 2, tight_layout=True)
ax[0].imshow(img_rgb)
ax[0].set_title("rgb")
ax[1].imshow(eq_rgb)
ax[1].set_title("equalized_rgb")
plt.show()
fig, ax = plt.subplots(3,2, tight_layout=True)
ax[0,0].hist(r.ravel(), 256, [0,256])
ax[0,0].set_title("r")
ax[0,1].hist(eq_r.ravel(), 256, [0,256])
ax[0,1].set_title("eq_r")
ax[1,0].hist(g.ravel(), 256, [0,256])
ax[1,0].set_title("g")
ax[1,1].hist(eq_g.ravel(), 256, [0,256])
ax[1,1].set_title("eq_g")
ax[2,0].hist(b.ravel(), 256, [0,256])
ax[2,0].set_title("b")
ax[2,1].hist(eq_b.ravel(), 256, [0,256])
ax[2,1].set_title("eq_b")
plt.show()
RGBでも陰影が濃くなりました。一方、HSV、YCrCb空間の画像に関してはうまくいかなかったので、それに関しては追加の考察が必要です。
まとめ
ヒストグラム均一化を理解するために、自身の学びを出力しました。違っている点など見つかれば適宜修正します。ご指摘いただけると感謝です。