画像処理はじめました。

AI/AR/VRという近未来的な言葉に惹かれ、その技術分野に参戦するために立ち上げたブログです。日々の格闘結果を記録に残してゆこうと思います。

2-2 「ガンマ補正」

モチベーション

画像全体を明るくしたり暗くしたりできます。
バイアス値を加えて作成する直線的な画像変換よりも、より自然な出力が得られます。
PCで表示される明るさはディスプレイによってさまざまであり、見え方の差を補正するためにγ補正が用いられることがあります。

やり方

PILライブラリを使用し画像を読み込みます。
各画素値にガンマ変換を適用し計算結果を新たな画素値とします。

プログラム

#!/usr/bin/python
# -*- coding: utf-8 -*-

# ガンマ補正

from PIL import Image 
import numpy as np
from matplotlib import pylab as plt

# 画像の読み込み
im = np.array(Image.open('input.bmp'))
print(im.shape, im.dtype)

# 画像の表示
plt.title("before")
plt.imshow(im)
plt.show()


# 画像サイズを取得
height= im.shape[0]
width = im.shape[1]

# γ値
g = 2.2

##全画素を走査し、γ補正を適用する
for x in range(height):
    for y in range(width):
        im[x,y] = 255.0 * pow( im[x,y]/255.0, 1.0/g )

# 画像の表示
plt.title("after")
plt.imshow(im)
plt.show()

#保存
Image.fromarray(im).save('output.bmp')

実行結果

f:id:genetaka1810:20200127133349p:plain
ガンマ補正の実行結果

解説

折れ線型トーンカーブでは、トーンカーブの切り替わり前後で画素値が急激に変換されてしまいますが、ガンマ補正は曲線のトーンカーブで 次のように表現されます。

f:id:genetaka1810:20200127130942p:plain
ガンマ変換式

ガンマ値を変化させたときのグラフを以下に示します。

f:id:genetaka1810:20200127132553p:plain
ガンマ変換のトーンカーブ
γ > 1の時は上に凸、γ < 1 のときは下に凸のトーンカーブとなります。

ガンマ補正のトーンカーブ

#!/usr/bin/python
# -*- coding: utf-8 -*-

# ガンマ補正のトーンカーブ
#
# 参考
# ガンマ関数
#  ディジタル画像処理(CG-ARTS協会)P85
# 折れ線型トーンカーブでは、トーンカーブの切り替わり前後で画素値が急激に変換されてしまう
#  ガンマ補正は曲線のトーンカーブで y = 255 * (x/255)^(1/γ) と表現される。
#  γ > 1の時は上に凸、γ < 1 のときは下に凸のトーンカーブとなる。

# numpy.empty関数
#   https://deepage.net/features/numpy-empty.html
#
#   np.empty 値の初期化を行うことなくNumPy配列を生成する
#   第一引数に配列数、第二引数にはデータ型を指定
#   実行速度が早く値を初期化する必要がないときはnp.emptyを使う

# numpy.clip関数
#   https://note.nkmk.me/python-numpy-clip/
#
#   np.clip 配列要素の値を任意の最小値・最大値にクリッピングする
#   第一引数にNumpy配列、第二引数に最小値、第三引数には最大値を指定する。


import numpy as np
import matplotlib.pyplot as plt

p = np.empty(256, np.uint8)  
 
for gamma in np.array([0.33, 0.5, 0.66, 1.0, 1.5, 2.0, 3.0]):

    for i in range(256):
        p[i] = np.clip(pow(i / 255.0, 1 / gamma) * 255.0, 0, 255)
    if (gamma >= 1) :
        plt.plot(p, label=str(gamma))
    else:
        plt.plot(p, label=str(gamma), linestyle="dashed")

plt.legend()
plt.xlabel("INPUT")
plt.ylabel("OUTPUT")
plt.show()

2-1 「バイアス調整」

モチベーション

画像全体を明るくしたり暗くしたりできます。

やり方

PILライブラリを使用し画像を読み込みます。
各画素値にバイアス値を加えて作成します。

プログラム

#!/usr/bin/python
# -*- coding: utf-8 -*-

# バイアス調整

from PIL import Image 
import numpy as np
from matplotlib import pylab as plt

# 画像の読み込み
im = np.array(Image.open('input.bmp'))
print(im.shape, im.dtype)

# 画像の表示
plt.title("before")
plt.imshow(im)
plt.show()


# 画像サイズを取得
height= im.shape[0]
width = im.shape[1]

# バイアス値
b = 70

##全画素を走査し、バイアス値を加算する
for x in range(height):
    for y in range(width):

        # im[x,y,0]で、座標x,yのカラー情報(赤)を取得する
        # グレースケール画像の場合は、R,G,Bの値が同じとなるので、1色分を参照する
        p = im[x,y,0] + b   

        # 画素値は255でクリッピングする
        if p > 255:
            im[x,y] = 255
        elif p < 0:
            im[x,y] = 0
        else:
            im[x,y]=p


# 画像の表示
plt.title("after")
plt.imshow(im)
plt.show()

#保存
Image.fromarray(im).save('output.bmp')

実行結果

f:id:genetaka1810:20200125170432p:plain
バイアス調整結果

解説

各画素値にバイアス値70を加えています。
そのため元の画像よりも明るい画像が得られます。モノクロ8ビット画像の場合、画素値の最大値は255ですので、バイアス値を加えた結果が70を超える場合は、255にクリッピングしています。
同様に負のバイアス値を設定し、画素値とバイアス値の加算結果がマイナスになる場合は、画素値の下限値を0としています。

1-7 「モザイク画像処理」

モチベーション

画素に匿名性を持たせることができます。

やり方

PILライブラリを使用し画像を読み込みます。
読み込んだ画像に対してウインドウサイズに応じて処理をおこないます。
ウィンドウサイズが 3x3 の場合、対象9画素の平均値を計算し、元画像へ算出した平均値を代入します。

プログラム

#!/usr/bin/python
# -*- coding: utf-8 -*-

# モザイク処理

from PIL import Image 
import numpy as np
from matplotlib import pylab as plt

# 画像の読み込み
im = np.array(Image.open('input.bmp'))
#print(im.shape, im.dtype)

# 画像の表示
plt.title("before")
plt.imshow(im)
plt.show()


# 画像サイズを取得
Y= im.shape[0]
X = im.shape[1]

# ウィンドウサイズ指定
w = 5

s = np.zeros(3)



##全画素を走査し、ウィンドウサイズ毎にモザイク処理を行う
height=[ i for i in range(int((w-1)/2), Y, w)] 
width=[ i for i in range(int((w-1)/2), X, w)]

for y in height:
    for x in width:
#        print("y:%d x:%d " % (y,x))
#        print(im[y,x]) # 行(height)、列(width)の順

        ## ウィンドウ内の座標指定
        wj = [j for j in range(y-int((w-1)/2), int(y+(w-1)/2)+1)]
        wi = [i for i in range(x-int((w-1)/2), int(x+(w-1)/2)+1)]
        for j in wj:
            for i in wi:
#                print("i:%d j:%d" %(i,j))
#                print(im[j,i])
                s = s + im[j,i]

        # 元画像へモザイク処理した画素値を書き込む
        for j in wj:
            for i in wi:
                im[j,i] = s/(w*w)

        s = np.zeros(3)
            

# 画像の表示
plt.title("after")
plt.imshow(im)
plt.show()

#保存
Image.fromarray(im).save('output.bmp')

実行結果

f:id:genetaka1810:20200119225301p:plain
モザイク処理

解説

画像サイズ10x10、ウィンドウサイズ3x3の場合のモザイク処理について説明します。

f:id:genetaka1810:20200119225759p:plain
モザイク処理3x3入力画像

PILの場合、画像ピクセルは、左上から(0,0), (0,1)…の要素として参照できます。
黄色で示したピクセルが、プログラムの外側のForループで参照される画素座標です。
赤枠はウィンドウサイズを示しています。

ウィンドウ内の座標指定を行っているForループでは、黄色セルを中心に、ウィンドウサイズ(3x3)画素の画素値の合計を算出しています。

元画像へモザイク処理した画素値を書き込む処理は、画素値の合計値の平均値を算出し代入してます。
この処理を以下に図示しておきます。

f:id:genetaka1810:20200119231458p:plain
モザイク処理 平均値の算出と代入

1-6 「ソラリゼーション」

モチベーション

画素値の濃淡を一部反転させることで特殊な表現効果が見込めます。
ネガ画像とポジ画像が混じりあったような効果が得られます。

やり方

PILライブラリを使用し画像を読み込みます。読み込んだ画像の画素値に対してトーンカーブを適用し各色情報を変換します。

プログラム

画像をPILで取得しNumpy配列化します。
入力画素値127で折り返すトーンカーブを適用しています。

#!/usr/bin/python
# -*- coding: utf-8 -*-

# ソラリゼーション

from PIL import Image 
import numpy as np
from matplotlib import pylab as plt

# 画像の読み込み
im = np.array(Image.open('input.bmp'))
print(im.shape, im.dtype)
# ((399L, 600L, 3L), dtype('uint8'))


# 画像の表示
plt.title("before")
plt.imshow(im)
plt.show()


# 画像サイズを取得
height= im.shape[0]
width = im.shape[1]



##全画素を走査し、色情報を変換する。
for x in range(height):
    for y in range(width):
        
        # ソラリゼーション
        for i in range(3):
            if im[x,y,i] <= 127:
                im[x,y,i] = im[x,y,i] * 2
            else:
                im[x,y,i] = 255 * 2 - im[x,y,i] * 2
            

print(im.shape, im.dtype)
# ((399L, 600L, 3L), dtype('uint8'))

# 画像の表示
plt.title("after")
plt.imshow(im)
plt.show()

#保存
Image.fromarray(im).save('output.bmp')

実行結果

f:id:genetaka1810:20191110120854p:plain

補足

  • ネガ画像とは、実際の明暗が逆になっている画像のことです。明るい所が黒く、暗い所が白く表示されます。ポジ画像は、ネガ画像とは逆に明るい所が白く、暗い所が黒く表示されます。

 f:id:genetaka1810:20191110121825p:plain

1-5 「ポスタリゼーション」

モチベーション

画像が表現できる色数を減らすことで実写を手書き風の画像へ変換できます。

やり方

PILライブラリを使用し、グレースケールで画像を読み込みます。読み込んだ画像は、uint8型なので 0~255 の値をとります。これら各画素値に対して、階調を減らす作業がポスタリゼーションです。

例えば、0~255値の256階調で表現されている値を、5階調へ落とす作業を考えてみます。
その場合、256段階で 0~51の値をとっていた画素を、5段階表示では、全て25に置き換えたり、同様に、51~102の範囲の値を76へ置き換えたりします。

256段階表示 5段階表示 区間の中央値
0~51 25 25 = (0+51)/2
51~102 76 76 = (51+102)/2
102~153 127 127 = (102+153)/2
153~204 178 178 = (153+204)/2
204~255 229 229 = (204+255)/2

プログラム

グレー画像をPILで取得しNumpy配列化します。
256段階から5段階の画素値へ置き換える場合、区間の値を中央値に置き換えることで実現しています。

#!/usr/bin/python
# -*- coding: utf-8 -*-

# ポスタリゼーション

from PIL import Image 
import numpy as np
from matplotlib import pylab as plt

# 画像の読み込み
im = np.array(Image.open('input.bmp').convert('L'))
print(im.shape, im.dtype)
# ((540L, 960L), dtype('uint8'))


# 画像の表示
plt.title("before")
plt.gray()           # 表示カラーマップをグレースケールとする
plt.imshow(im)
plt.show()


# 画像サイズを取得
height= im.shape[0]
width = im.shape[1]

#量子化レベルを (1/q) とする
#q=5の場合、256階調を5階調へ変換することになる
q=5

#全画素を走査し、量子化レベルを変換する。
for x in range(height):
    for y in range(width):
        for l in range(q):
            t1 = l*((255)/q);
            t2 = (l+1)*((255)/q);

            # ポスタリゼーション処理            
            if t1 <= im[x,y] and im[x,y] < t2:
                im[x,y] = (t1+t2)/2
                break
    

print(im.shape, im.dtype)
# ((540L, 960L), dtype('uint8'))

# 画像の表示
plt.title("after")
plt.imshow(im)
plt.show()

#保存
Image.fromarray(im).save('output.bmp')

実行結果

f:id:genetaka1810:20191109152543p:plain

補足

f:id:genetaka1810:20191109152808p:plain

1-4 「輝度反転」

モチベーション

画像の細かいディテールを確認する場合に、反転前後の画像を比較すると見つけやすくなるかもしれません。

やり方

PILライブラリを使用し、グレースケールで画像を読み込みます。読み込んだ画像は、uint8型なので 0~255 の値をとります。輝度反転(白黒反転)するために、入力画像の画素値を 255 から引いています。

プログラム

グレー画像をPILで取得しNumpy配列化後、画素値(輝度値)を255から引くことで反転画像を取得します。

#!/usr/bin/python
# -*- coding: utf-8 -*-

# 輝度反転

from PIL import Image 
import numpy as np
from matplotlib import pylab as plt

# 画像の読み込み
im = np.array(Image.open('input.bmp').convert('L'))
print(im.shape, im.dtype)
# ((540L, 960L), dtype('uint8'))

# 画像の表示
plt.title("before")
plt.gray()           # 表示カラーマップをグレースケールとする
plt.imshow(im)
plt.show()

# 読み込んだ画像は、uint8型なので 0~255 の値をとる
# 輝度反転するためには、入力画像の画素値を 255 から引く
im = 255 - im[:,:]

print(im.shape, im.dtype)
# ((540L, 960L), dtype('uint8'))

# 画像の表示
plt.title("after")
plt.imshow(im)
plt.show()

#保存
Image.fromarray(im).save('output.bmp')

実行結果

f:id:genetaka1810:20191103163811p:plain

1-3 「グレー化処理」

モチベーション

  • 被写体の形状だけを確認したい場合はグレースケールで事足りる
  • カラー画像(3チャンネル)に比べて扱う情報が1/3となるメリットあり

やり方

カラー画像をグレースケールへ変換する方法にはいくつかありますが、ここでは、以下を紹介します。

  • PILライブラリを使用し、グレースケールで画像を読み込む
  • 平均化法でグレースケールを作成
  • YUVのY画像(輝度情報)でグレースケールを作成

プログラム

カラー画像をPILで取得し、Numpy配列化してから色情報を変更しています。

#!/usr/bin/python
# -*- coding: utf-8 -*-

# グレー化処理

from PIL import Image 
import numpy as np
from matplotlib import pylab as plt

# 画像の読み込み
im = np.array(Image.open('input.bmp'))
print(im.shape, im.dtype)
# ((640L, 960L, 3L), dtype('uint8'))

# 画像の表示
plt.title("before")
plt.imshow(im)
plt.show()

####################################
# グレースケールで画像を読み込む

img = np.array(Image.open('input.bmp').convert('L'))
print(img.shape, img.dtype)
#((640L, 960L), dtype('uint8'))

# 画像の表示
plt.title("after gray")
plt.imshow(img)
plt.show()

#保存
Image.fromarray(img).save('output_gray.bmp')

#
####################################

####################################
# 平均化法でグレースケールを作成

# B/G/R各チャンネルの平均値を計算
s = (0.333 * im[:,:,0] + 0.333 * im[:,:,1] + 0.333 * im[:,:,2])

# 出力画像を保存する配列を作成 
im_gray1 = im.copy()

# グレー画像(B/G/Rの平均値を各色へ代入する)
im_gray1[:,:,0] = s  # Red
im_gray1[:,:,1] = s  # Green
im_gray1[:,:,2] = s  # Blue

print(im_gray1.shape, im_gray1.dtype)
#((640L, 960L, 3L), dtype('uint8'))

# 画像の表示
plt.title("after gray ave")
plt.imshow(im_gray1)
plt.show()

#保存
Image.fromarray(im_gray1).save('output_gray_ave.bmp')

#
####################################

####################################
# YUVのY画像(輝度情報)でグレースケールを作成
# 

# RGBカラーから輝度値へ変換する
# 参考: Wiki YUV https://ja.wikipedia.org/wiki/YUV 
im_gray2 = 0.299 * im[:, :, 0] + 0.587 * im[:, :, 1] + 0.114 * im[:, :, 2]

print(im_gray2.shape, im_gray2.dtype)
#((640L, 960L), dtype('float64'))

# imshowで画像を表示する場合は、
# float型をuint8型へ変換する必要がある。
im_gray2 = np.uint8(im_gray2)
print(im_gray2.shape, im_gray2.dtype)
#((640L, 960L), dtype('uint8'))

# 画像の表示
plt.title("after gray y")
plt.imshow(im_gray2)
plt.show()

#保存
# float型は、uint8へ変換する必要がある
Image.fromarray(im_gray2).save('output_gray_y.bmp')

#
####################################

実行結果

f:id:genetaka1810:20191102163616p:plain


グレースケール画像は、ぱっと見どれも同じようですが、「PILライブラリを使用し、グレースケールで画像を読み込む」と「YUVのY画像(輝度情報)でグレースケールを作成」により作成された画像を比較すると、差分が確認されました。

どのグレースケール画像が良いのかは、用途によるのかな?

f:id:genetaka1810:20191102163753p:plain
差分を、オレンジ色で示しています。