7
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

python+opencvで画像処理の勉強2 画素ごとの濃淡変換

Last updated at Posted at 2021-12-07

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

image.png

明るさ・コントラストの変換

ここではグレースケール画像の濃淡変換について説明します。

トーンカーブ

画像の画素値を変換するために、入力画素値に対しどのように出力値を対応付けるか指定します。
これを指定する関数が階調変換関数であり、グラフで表したものをトーンカーブと呼びます。
この後は、いくつかトーンカーブとその変換例を紹介していきます。
ここではルックアップテーブルと呼ばれる入力画素値と出力画素値の対応表を作成して変換を行います。

折れ線型トーンカーブ

単純な変換ですが、暗くつぶれていた部分を見やすくすることなどによく使用されます。
曲がっている点前後で急に変化する、トーンカーブが平坦な部分で出力値が一定になるなど欠点があります。

まず、使用する画像の画素値(明るさ)のヒストグラムを確認します。

plt.hist(img.flatten(), 256, [0,256]);

image.png

ここで、次のトーンカーブを考えてみます。

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

image.png

これは、現在の画素値が0~200のものは0~255に変換され、現在の画素値が200より大きいものはすべて255に変換されることを表しています。
このyがルックアップテーブルとなります。
このルックアップテーブルを使用して画像を変換するにはLUT()を使います。

img_lut = cv2.LUT(img, y)
plt.imshow(img_lut, 'gray')

image.png

ちょっと変化が分かりづらいですが、ヒストグラムを見てみます。

plt.hist(img_lut.flatten(), 256, [0,256]);

image.png

確かに先ほど見たヒストグラムが横に広がったような形になったので変換はできているようです。

他にも条件を変えて結果を確認してみます。
まず、折れ線型トーンカーブ用のルックアップテーブルを作成する関数を定義します。

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

image.png

画像が暗くなったり、明るくなったりしているのがわかります。
ヒストグラムは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')

image.png

折れ線型と違い滑らかに変化しているのが分かります。
これらのトーンカーブによって変換した画像も確認してみます。

# 変換結果の描画
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()

image.png

$\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]);

image.png

ヒストグラム平坦化

トーンカーブによる濃淡の変化は、人手で作業する必要があります。
これを自動化する変換方法にヒストグラム平坦化と呼ばれるものがあります。
ヒストグラム平坦化では画素値のヒストグラムが、画素値の全域にわたって均等になるように変換します。
しかし、ある画素値の分布がすでに均等になる値を超えているときなど、均等になることはまれです。
また、適応的ヒストグラム平坦化と呼ばれる画像をタイルと呼ばれる小領域に分割し,領域毎にヒストグラム平坦化を適用したものもあります。
ヒストグラム平坦化は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([]);

image.png

特殊な効果

#### 濃淡の反転
濃淡を反転させる処理はネガ・ポジ反転と呼ばれます。
ルックアップテーブルを定義してもできますが、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]);

image.png

白黒が反転して、ヒストグラムも反転していることがわかります。

ポスタリゼーション

階段状のトーンカーブを用いることで、出力画像の画素値を数段階に制限することができます。
このような変換をポスタリゼーションと呼び、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]);

image.png

画素値のパターンが減っているのが画像からもヒストグラムからもわかります。

ソラリゼーション

画像の濃淡の一部を反転させることで、ネガ画像とポジ画像が混ざったような効果が出ることがあります。
この効果をソラリゼーションと呼びます。

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

image.png

ネガ・ポジ反転とは違い、完全に反転しているわけではないことが分かります。

カラー画像の変換

ここからはカラー画像の変換について説明します。

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)

image.png

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

image.png

画像が暗くなったり明るくなったりしていることが確認できます。

同じトーンカーブを指定しても、意図しない色が出力される場合があります。
その極端な例としてソラリゼーションをした画像を出してみます。

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([]);

image.png

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([]);

image.png
image.png

左から赤・緑・青のみ図に示したトーンカーブで変換した画像です。

疑似カラー画像

グレースケール画像の各画素値に対し、チャンネルごとに異なるトーンカーブを用いることで疑似的に色を付けることができます。
この画像を疑似カラーと呼びます。

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([]);

image.png

色相・彩度・明度の変化

カラー画をの変換するとき、直感的に理解しやすい色相・彩度・明度(マンセル表色系)に変換したうえで、それぞれの成分を調整する方法もつかわれます。
それぞれの値を変化させたときの画像の変化を確認してみます。

まずは、色相(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([])

image.png

色相(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)

image.png

加算

画像間の足し算を行います。
cv2.add()がありますが、「+」で計算しても大丈夫です。

# img1 + img2でもよい
dst = cv2.add(img1, img2)

# 描画
plt.imshow(dst)

image.png

減算

cv2.absdiff()で引き算の結果の絶対値が取得できます。
np.abs(img1 - img2)でも同様の計算ができまが、負の値も出したければimg1 - img2で計算させます。

dst = cv2.absdiff(img1, img2)

# 描画
plt.imshow(dst)

image.png

乗算

cv2.multiply()で計算できますが、そのまま計算すると多くの値が255を超えてしまうので元の値を割るなどして処理します。

dst = cv2.multiply(img1//6, img2//6)

# 描画
plt.imshow(dst)

image.png

論理演算

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');

image.png

アルファブレンディング

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

image.png

左から右へ徐々に画像が変化していくことが分かります。

マスク処理

画像のある部分を使うか指定したい場合に、画素ごとに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([]);

image.png

これで画素ごとに濃淡を変化させる処理の基礎は押さえました。
次回はフィルタ処理について説明したいと思います。

次回

空間フィルタリング

参考文献

7
12
0

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
  3. You can use dark theme
What you can do with signing up
7
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?