pythonとOpenCVで画像処理の勉強をします。
前回
画像の読み込みと表示
今回使う画像はペンギンにします。
import cv2
import matplotlib.pyplot as plt
image_path = image_path # 画像のパス
img = cv2.imread(image_path)
img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
img = cv2.resize(img[250:2250, 2200:4200], dsize=None, fx=0.5, fy=0.5)
plt.imshow(img, 'gray')
明るさ・コントラストの変換
ここではグレースケール画像の濃淡変換について説明します。
トーンカーブ
画像の画素値を変換するために、入力画素値に対しどのように出力値を対応付けるか指定します。
これを指定する関数が階調変換関数であり、グラフで表したものをトーンカーブと呼びます。
この後は、いくつかトーンカーブとその変換例を紹介していきます。
ここではルックアップテーブルと呼ばれる入力画素値と出力画素値の対応表を作成して変換を行います。
折れ線型トーンカーブ
単純な変換ですが、暗くつぶれていた部分を見やすくすることなどによく使用されます。
曲がっている点前後で急に変化する、トーンカーブが平坦な部分で出力値が一定になるなど欠点があります。
まず、使用する画像の画素値(明るさ)のヒストグラムを確認します。
plt.hist(img.flatten(), 256, [0,256]);
ここで、次のトーンカーブを考えてみます。
import numpy as np
import japanize_matplotlib
# 変換前の画素値x
x = np.arange(256)
# 200が255に変換されるように傾きを求める
a = 255 / (200 - 0)
# xを線形変換する
# 画素値の最大値は255である
y = np.clip(a * (x - 0) + 0, 0, 255)
plt.plot(x, y)
plt.xlabel('変換前 x')
plt.ylabel('変換後 y')
これは、現在の画素値が0~200のものは0~255に変換され、現在の画素値が200より大きいものはすべて255に変換されることを表しています。
このyがルックアップテーブルとなります。
このルックアップテーブルを使用して画像を変換するにはLUT()を使います。
img_lut = cv2.LUT(img, y)
plt.imshow(img_lut, 'gray')
ちょっと変化が分かりづらいですが、ヒストグラムを見てみます。
plt.hist(img_lut.flatten(), 256, [0,256]);
確かに先ほど見たヒストグラムが横に広がったような形になったので変換はできているようです。
他にも条件を変えて結果を確認してみます。
まず、折れ線型トーンカーブ用のルックアップテーブルを作成する関数を定義します。
def make_linear_lut(range_min=0, range_max=255, beta=0, a=-1):
"""
折れ線型トーンカーブのLUTを作成する
"""
if a >= 0:
# 傾きを直接決める
a = a
else:
# 傾きの計算
range_max = range_max
range_min = range_min
beta = beta
a = 255 / (range_max - range_min)
# 変換前の画素値x
x = np.arange(256)
# ルックアップテーブルyの作成
y = np.clip(a * (x - range_min), 0, 255)
y = np.clip(y + beta, 0, 255).astype('int')
return y
3パターンで見てみます。
# 3種類のLUTを作成
x = np.arange(256)
y1 = make_linear_lut(0, 150, 0)
y2 = make_linear_lut(100, 255, 0)
y3 = make_linear_lut(0, 255, 50, 0.5)
fig, ax = plt.subplots(3, 3, figsize=(15, 10))
for i, y in enumerate([y1,y2,y3]):
# トーンカーブ
ax[0][i].plot(x, y)
ax[0][i].set_xlim([0, 255])
ax[0][i].set_ylim([-5, 260])
ax[0][i].set_xlabel('変換前')
ax[0][i].set_ylabel('変換後')
# 変換画像
img_lut = cv2.LUT(img, y)
ax[1][i].imshow(img_lut, 'gray')
ax[1][i].set_xticks([])
ax[1][i].set_yticks([])
# ヒストグラム
ax[2][i].hist(img_lut.flatten(),256,[0,256]);
画像が暗くなったり、明るくなったりしているのがわかります。
ヒストグラムは0や255などの端の値にカウントが偏っていることも確認できます。
累乗型トーンカーブ
折れ線型トーンカーブでは、トーンカーブの折れ曲がっている点の前後で変換の性質が急激に変わる欠点がありました。
このような折れ線型トーンカーブの欠点に対応するために曲線のトーンカーブを用いることがあります。
以下のような式で変換することをガンマ補正と呼びます。
$$
y = 255(\frac{x}{255})^{\frac{1}{\gamma}}
$$
γ>1でyは上に凸となり画像は明るくなり、γ<1でyは下に凸となり画像は暗くなります。
まずは、ガンマ補正のルックアップテーブルを作る関数を定義します。
def make_gamma_lut(gamma, imax=255):
"""
ガンマ補正のLUTを作成
"""
x = np.arange(256)
y = imax*(x / imax) ** (1/gamma)
return y.astype('int')
いくつかの$\gamma$値で作成したトーンカーブを見てみます。
x = np.arange(256)
# ガンマ補正のトーンカーブの描画
gammas = [3.0, 2.0, 1.0, 0.66, 0.33]
plt.figure(figsize=(6,6))
for g in range(len(gammas)):
y = make_gamma_lut(gammas[g])
plt.plot(x, y, label=str(gammas[g]));
plt.legend();
plt.xlabel('変換前 x')
plt.ylabel('変換後 y')
折れ線型と違い滑らかに変化しているのが分かります。
これらのトーンカーブによって変換した画像も確認してみます。
# 変換結果の描画
fig2, ax2 = plt.subplots(5, 2, figsize=(10, 20))
for i in range(len(gammas)):
y = make_gamma_lut(gammas[i])
img_lut = cv2.LUT(img, y)
ax2[i][0].imshow(img_lut, 'gray')
ax2[i][0].set_xticks([])
ax2[i][0].set_yticks([])
ax2[i][0].set_title('γ=' + str(gammas[i]))
ax2[i][1].hist(img_lut.flatten(),256,[0,256]);
plt.show()
$\gamma$値が大きいほど明るく、小さくなるほど暗くなっていくのがわかります。
ヒストグラムも端の値にカウントが偏ることがなく元の形状を保っているように見えます。
S字トーンカーブ
S字トーンカーブというものもあり、これは画素値が中央に偏っているときなどに使用します。
x = np.arange(256)
# S字カーブを適当に定義
y = ((np.sin(np.pi * (x/255 - 0.5)) + 1)/2 * 255).astype('int')
img_lut = cv2.LUT(img, y)
fig, ax = plt.subplots(1, 3, figsize=(17, 4))
# トーンカーブ
ax[0].plot(x, y);
# 変換後画像
ax[1].imshow(img_lut, 'gray');
ax[1].set_xticks([]);
ax[1].set_yticks([]);
ax[2].hist(img_lut.flatten(),256,[0,256]);
ヒストグラム平坦化
トーンカーブによる濃淡の変化は、人手で作業する必要があります。
これを自動化する変換方法にヒストグラム平坦化と呼ばれるものがあります。
ヒストグラム平坦化では画素値のヒストグラムが、画素値の全域にわたって均等になるように変換します。
しかし、ある画素値の分布がすでに均等になる値を超えているときなど、均等になることはまれです。
また、適応的ヒストグラム平坦化と呼ばれる画像をタイルと呼ばれる小領域に分割し,領域毎にヒストグラム平坦化を適用したものもあります。
ヒストグラム平坦化はcv2.equalizeHist()を、適応的ヒストグラム平坦化はcv2.createCLAHE()を使用します。
これらの処理結果を確認してみます。
# ヒストグラム平坦化
dst1 = cv2.equalizeHist(img)
# 適応的ヒストグラム平坦化
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
dst2 = clahe.apply(img)
#描画
plt.figure(figsize=(16, 6))
plt.subplot(2, 3, 1)
plt.title('基画像')
plt.hist(img.flatten(),256,[0,256], color = 'r');
plt.subplot(2, 3, 2)
plt.title('ヒストグラム平坦化')
plt.hist(dst1.flatten(),256,[0,256], color = 'r');
plt.subplot(2, 3, 3)
plt.title('適応的ヒストグラム平坦化')
plt.hist(dst2.flatten(),256,[0,256], color = 'r');
plt.subplot(2, 3, 4)
plt.imshow(img, 'gray');
plt.xticks([]);
plt.yticks([]);
plt.subplot(2, 3, 5)
plt.imshow(dst1, 'gray');
plt.xticks([]);
plt.yticks([]);
plt.subplot(2, 3, 6)
plt.imshow(dst2, 'gray');
plt.xticks([]);
plt.yticks([]);
特殊な効果
#### 濃淡の反転
濃淡を反転させる処理はネガ・ポジ反転と呼ばれます。
ルックアップテーブルを定義してもできますが、cv2.bitwise_not()を使って結果を確認します。
x = np.arange(256)
# LUTではなく関数を使って変換
dst = cv2.bitwise_not(img)
fig, ax = plt.subplots(1, 3, figsize=(17, 4))
# トーンカーブ
ax[0].plot(x, 255-x);
# 変換後画像
ax[1].imshow(dst, 'gray');
ax[1].set_xticks([]);
ax[1].set_yticks([]);
ax[2].hist(dst.flatten(),256,[0,256]);
白黒が反転して、ヒストグラムも反転していることがわかります。
ポスタリゼーション
階段状のトーンカーブを用いることで、出力画像の画素値を数段階に制限することができます。
このような変換をポスタリゼーションと呼び、2段階のものは2値化と呼ばれています。
2値化については、また別の機会に解説します。
まずは、ポスタリゼーション用のルックアップテーブルを作る関数を定義します。
def make_post_lut(n):
"""
ポスタリゼーション用のLUTを作成
"""
x = np.arange(256)
y = np.floor(x/n)*int(255/np.floor(255/n))
y = y.astype('uint8')
return y
この関数を使って3種類の段数でポスタリゼーションを適用してみます。
c = [64, 32, 8]
x = np.arange(256)
# 変案及び描画
fig, ax = plt.subplots(3, 3, figsize=(14, 10))
for i in range(3):
y = make_post_lut(c[i])
ax[0][i].plot(x, y)
ax[0][i].set_xlim([0, 255])
ax[0][i].set_ylim([-5, 260])
img_lut = cv2.LUT(img, y)
ax[1][i].imshow(img_lut, 'gray')
ax[1][i].set_xticks([])
ax[1][i].set_yticks([])
ax[2][i].hist(img_lut.flatten(),256,[0,256]);
画素値のパターンが減っているのが画像からもヒストグラムからもわかります。
ソラリゼーション
画像の濃淡の一部を反転させることで、ネガ画像とポジ画像が混ざったような効果が出ることがあります。
この効果をソラリゼーションと呼びます。
x = np.arange(256)
# ソラリゼーション用のLUTを適当に作成
y = ((np.sin(3 * np.pi * (x / 255 + 1 / 2 )) + 1) * 255 / 2).astype('uint8')
img_lut = cv2.LUT(img, y)
fig, ax = plt.subplots(1, 3, figsize=(17, 4))
# トーンカーブ
ax[0].plot(x, y);
# 変換後画像
ax[1].imshow(img_lut, 'gray');
ax[1].set_xticks([]);
ax[1].set_yticks([]);
ax[2].hist(img_lut.flatten(),256,[0,256]);
ネガ・ポジ反転とは違い、完全に反転しているわけではないことが分かります。
カラー画像の変換
ここからはカラー画像の変換について説明します。
img = cv2.imread(image_path)
img = cv2.resize(img[250:2250, 2200:4200], dsize=None, fx=0.5, fy=0.5)
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
plt.imshow(img_rgb)
R,G,Bトーンカーブによる変換
カラー画像でも同様にトーンカーブによってコントラストの変換ができます。
R,G,B各チャンネルに同じトーンカーブを指定した場合の例を示します。
処理は今までと同様にできます。
# 3種類のLUTを作成
x = np.arange(256)
y1 = make_linear_lut(0, 150, 0)
y2 = make_linear_lut(100, 255, 0)
y3 = make_linear_lut(0, 255, 50, 0.5)
fig, ax = plt.subplots(2, 3, figsize=(15, 8))
for i, y in enumerate([y1,y2,y3]):
# トーンカーブ
ax[0][i].plot(x, y)
ax[0][i].set_xlim([0, 255])
ax[0][i].set_ylim([-5, 260])
ax[0][i].set_xlabel('変換前')
ax[0][i].set_ylabel('変換後')
# 変換画像
img_lut = cv2.LUT(img_rgb, y)
ax[1][i].imshow(img_lut)
ax[1][i].set_xticks([])
ax[1][i].set_yticks([])
画像が暗くなったり明るくなったりしていることが確認できます。
同じトーンカーブを指定しても、意図しない色が出力される場合があります。
その極端な例としてソラリゼーションをした画像を出してみます。
x = np.arange(256)
# ソラリゼーション用のLUTを適当に作成
y = ((np.sin(3 * np.pi * (x / 255 + 1 / 2 )) + 1) * 255 / 2).astype('uint8')
img_lut = cv2.LUT(img_rgb, y)
# 描画
fig, ax = plt.subplots(1, 2, figsize=(10, 4))
ax[0].plot(x, y)
ax[1].imshow(img_lut)
ax[1].set_xticks([]);
ax[1].set_yticks([]);
1つのチャンネルのみ変換することもあります。
fig, ax = plt.subplots(1, 3, figsize=(16, 5))
for i in range(3):
tmp_img = img_rgb.copy()
tmp_img[:,:,i] = cv2.LUT(tmp_img[:,:,i], y1)
ax[i].imshow(tmp_img)
ax[i].set_title(col[i])
ax[i].set_xticks([]);
ax[i].set_yticks([]);
左から赤・緑・青のみ図に示したトーンカーブで変換した画像です。
疑似カラー画像
グレースケール画像の各画素値に対し、チャンネルごとに異なるトーンカーブを用いることで疑似的に色を付けることができます。
この画像を疑似カラーと呼びます。
a = 255 / 50
x = np.arange(256)
yy1 = np.clip(a * x[:161], 0, 255)
yy2 = np.clip(255 - a * (x[161:]-161), 0, 255)
g = np.concatenate([yy1,yy2])
r = make_linear_lut(100, 160, 0)
b = make_linear_lut(100, 50, -1)
tmp_img = np.array([cv2.LUT(img, r), cv2.LUT(img, g), cv2.LUT(img, b)]).transpose(1,2,0).astype(int)
fig, ax = plt.subplots(1, 3, figsize=(16, 3))
ax[0].imshow(img, 'gray')
ax[0].set_xticks([]);
ax[0].set_yticks([]);
ax[1].plot(x, r, 'red')
ax[1].plot(x, g, 'green')
ax[1].plot(x, b, 'blue')
ax[2].imshow(tmp_img)
ax[2].set_xticks([]);
ax[2].set_yticks([]);
色相・彩度・明度の変化
カラー画をの変換するとき、直感的に理解しやすい色相・彩度・明度(マンセル表色系)に変換したうえで、それぞれの成分を調整する方法もつかわれます。
それぞれの値を変化させたときの画像の変化を確認してみます。
まずは、色相(H)、彩度(S)、明度(V)を変換する関数を定義します。
def change_sv(img, h=1, s=1, v=1):
"""
彩度と明度を変換する
RGB -> HSV -> 変換 -> RGB
"""
img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
img_hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
s_magnification = s
v_magnification = v
h_magnification = h
img_hsv[:,:,(0)] = img_hsv[:,:,(0)]*h_magnification
img_hsv[:,:,(1)] = img_hsv[:,:,(1)]*s_magnification
img_hsv[:,:,(2)] = img_hsv[:,:,(2)]*v_magnification
img_bgr = cv2.cvtColor(img_hsv, cv2.COLOR_HSV2BGR)
return cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)
定義した関数を使って、それぞれ変化させていきます。
上から色相(H)、彩度(S)、明度(V)で、右に行くほど値が小さくなります。
H = [1.4, 1, 0.2]
S = [1.4, 1, 0.2]
V = [1.4, 1, 0.2]
types = ['high', 'ref', 'low']
# 変換及び描画
fig, ax = plt.subplots(3, 3, figsize=(15, 15))
for i in range(3):
dst = change_sv(img_rgb, h=H[i])
ax[0][i].imshow(dst)
ax[0][i].set_title('H_'+types[i])
ax[0][i].set_xticks([])
ax[0][i].set_yticks([])
dst = change_sv(img_rgb, s=S[i])
ax[1][i].imshow(dst)
ax[1][i].set_title('S_'+types[i])
ax[1][i].set_xticks([])
ax[1][i].set_yticks([])
dst = change_sv(img_rgb, v=V[i])
ax[2][i].imshow(dst)
ax[2][i].set_title('V_'+types[i])
ax[2][i].set_xticks([])
ax[2][i].set_yticks([])
色相(H)が変化すると色が変化、彩度(S)が小さくなるとグレースケールに近くなり、明度(V)が小さくなると暗くなることが確認できました。
複数の画像の利用
画像間の演算
画像間演算とは、複数の画像の同じ位置にある画素ごとに、ある決められた演算を行うものです。
演算には四則演算などの算術演算や、ANDやORなどの論理演算があります。
img1 = cv2.imread(image_path1)
img1 = cv2.cvtColor(img1, cv2.COLOR_BGR2RGB)
img1 = cv2.resize(img1[250:2250, 2200:4200], dsize=None, fx=0.5, fy=0.5)
img1_gray = cv2.cvtColor(img1, cv2.COLOR_RGB2GRAY)
img2 = cv2.imread(image_path2)
img2 = cv2.cvtColor(img2, cv2.COLOR_BGR2RGB)
img2 = cv2.resize(img2[250:2250, 1500:3500], dsize=None, fx=0.5, fy=0.5)
img2_gray = cv2.cvtColor(img2, cv2.COLOR_RGB2GRAY)
fig, ax = plt.subplots(1, 2, figsize=(10, 5))
ax[0].imshow(img1)
ax[1].imshow(img2)
加算
画像間の足し算を行います。
cv2.add()がありますが、「+」で計算しても大丈夫です。
# img1 + img2でもよい
dst = cv2.add(img1, img2)
# 描画
plt.imshow(dst)
減算
cv2.absdiff()で引き算の結果の絶対値が取得できます。
np.abs(img1 - img2)でも同様の計算ができまが、負の値も出したければimg1 - img2で計算させます。
dst = cv2.absdiff(img1, img2)
# 描画
plt.imshow(dst)
乗算
cv2.multiply()で計算できますが、そのまま計算すると多くの値が255を超えてしまうので元の値を割るなどして処理します。
dst = cv2.multiply(img1//6, img2//6)
# 描画
plt.imshow(dst)
論理演算
AND、OR、XOR、NOTの関数がそれぞれ用意されています。
それぞれの結果を確認します。
im1 = cv2.bitwise_or(img1_gray, img2_gray)
im2 = cv2.bitwise_and(img1_gray, img2_gray)
im3 = cv2.bitwise_xor(img1_gray, img2_gray)
im4 = cv2.bitwise_not(img1_gray)
fig, ax = plt.subplots(2, 2, figsize=(10, 10))
ax[0][0].imshow(im1, 'gray');
ax[0][0].set_xticks([]);
ax[0][0].set_yticks([]);
ax[0][0].set_title('or');
ax[0][1].imshow(im2, 'gray');
ax[0][1].set_xticks([]);
ax[0][1].set_yticks([]);
ax[0][1].set_title('and');
ax[1][0].imshow(im3, 'gray');
ax[1][0].set_xticks([]);
ax[1][0].set_yticks([]);
ax[1][0].set_title('xor');
ax[1][1].imshow(im4, 'gray');
ax[1][1].set_xticks([]);
ax[1][1].set_yticks([]);
ax[1][1].set_title('not');
アルファブレンディング
2枚の画像の重み付きの平均値を計算することをブレンディングといいます。
α(重み)の値を時間的に変化させて、ある画像から別の画像へ変化させることができます。
このような手法はディゾルブまたはオーバーラップと呼ばれています。
cv2.addWeighted()を使って、オーバーラップをしてみたいと思います。
alpha = [1.0, 0.75, 0.5, 0.25, 0]
fig, ax = plt.subplots(1, 5, figsize=(18, 6))
for i in range(5):
dst = cv2.addWeighted(img1, alpha[i], img2, (1-alpha[i]), 0)
ax[i].imshow(dst)
ax[i].set_xticks([]);
ax[i].set_yticks([]);
ax[i].set_title("α=" + str(alpha[i]))
左から右へ徐々に画像が変化していくことが分かります。
マスク処理
画像のある部分を使うか指定したい場合に、画素ごとに1,0で指定した画像(マスク画像)をあらかじめ作成し、それに基づいて画像を取得することをマスク処理と呼びます。
height = img1_gray.shape[0]
width = img1_gray.shape[1]
img_mask = np.zeros((height, width, 3), np.uint8)
img_mask[height//4:height*3//4, width//4:width*3//4, :] = [255]
dst = cv2.bitwise_and(img1, img_mask)
fig, ax = plt.subplots(1, 3, figsize=(16, 4))
ax[0].imshow(img1);
ax[0].set_title("基画像");
ax[0].set_xticks([]);
ax[0].set_yticks([]);
ax[1].imshow(img_mask);
ax[1].set_title("マスク画像");
ax[1].set_xticks([]);
ax[1].set_yticks([]);
ax[2].imshow(dst)
ax[2].set_title("マスク処理");
ax[2].set_xticks([]);
ax[2].set_yticks([]);
これで画素ごとに濃淡を変化させる処理の基礎は押さえました。
次回はフィルタ処理について説明したいと思います。
次回
空間フィルタリング
参考文献