フーリエ変換は音声データに対して用いられることが多い手法だけど、画像データにも応用が効く。
音声データの場合、フーリエ変換を使うことで時間領域の情報を周波数領域の情報に直せる。
それに対し、画像データでは空間領域の情報を周波数領域の情報に直すことになる。
つまり、画像データの濃淡が複数の波形の合成によって作られていると見なす。
今回は、画像データをフーリエ変換して、周波数領域の情報にフィルタをかけたり元に戻したりして遊んでみる。
使った環境は次のとおり。
$ sw_vers
ProductName: Mac OS X
ProductVersion: 10.14 .6
BuildVersion: 18G5033
$ python -V
Python 3.7 .7
もくじ
下準備
あらかじめ、必要なパッケージをインストールしておく。
$ pip install pillow matplotlib
周波数領域の情報を可視化してみる
最初に、画像データをフーリエ変換して周波数領域で可視化してみる。
画像データは、二次元の NumPy 配列として読み込んだ上で np.fft.fft2()
に渡すことでフーリエ変換できる。
また、周波数領域のデータは np.fft.ifft2()
を使うことで空間領域のデータに戻せる。
以下のサンプルコードでは、元の画像と、周波数領域でのパワースペクトル、そして逆変換することで元に戻した画像を可視化している。
なお、読み込む画像は適当に用意しよう。
from PIL import Image
import numpy as np
from matplotlib import pyplot as plt
def main ():
img = Image.open('lena.png' )
gray_img = img.convert('L' )
f_xy = np.asarray(gray_img)
f_uv = np.fft.fft2(f_xy)
shifted_f_uv = np.fft.fftshift(f_uv)
magnitude_spectrum2d = 20 * np.log(np.absolute(shifted_f_uv))
unshifted_f_uv = np.fft.fftshift(shifted_f_uv)
i_f_xy = np.fft.ifft2(unshifted_f_uv).real
fig, axes = plt.subplots(1 , 3 , figsize=(8 , 4 ))
for ax in axes:
for spine in ax.spines.values():
spine.set_visible(False )
ax.set_xticks([])
ax.set_yticks([])
axes[0 ].imshow(f_xy, cmap='gray' )
axes[0 ].set_title('Input Image' )
axes[1 ].imshow(magnitude_spectrum2d, cmap='gray' )
axes[1 ].set_title('Magnitude Spectrum' )
axes[2 ].imshow(i_f_xy, cmap='gray' )
axes[2 ].set_title('Reversed Image' )
plt.show()
if __name__ == '__main__' :
main()
上記をファイルに保存して実行する。
$ python imgfft2d.py
すると、次のようなグラフが得られる。
FFT -> IFFT
真ん中のプロットが、元となった画像データの周波数領域での表現になる。
この表現では、中心に近いほど低い周波数・遠いほど高い周波数の成分を含む。
白い部分ほど成分が多いことを示しているため、この画像は低周波の成分が比較的多いように見える。
また、右の画像を見ると、ちゃんと周波数領域のデータから空間領域のデータに復元できていることがわかる。
ローパスフィルタ (Low Pass Filter) をかけて復元してみる
先ほど述べた通り、周波数領域の表現では中心に近いほど低い周波数・遠いほど高い周波数の成分を含んでいる。
つまり、中心部分だけを取り出すような演算をすると、画像を構成する波形にローパスフィルタをかけることができる。
以下のサンプルコードでは、周波数領域のデータに対して中心部分を取り出すフィルタをかけることで低周波成分だけを抽出した。
そして、フィルタしたデータを逆フーリエ変換することで画像に戻している。
from PIL import Image
from PIL import ImageDraw
import numpy as np
from matplotlib import pyplot as plt
def main ():
img = Image.open('lena.png' )
gray_img = img.convert('L' )
f_xy = np.asarray(gray_img)
f_uv = np.fft.fft2(f_xy)
shifted_f_uv = np.fft.fftshift(f_uv)
x_pass_filter = Image.new(mode='L' ,
size=(shifted_f_uv.shape[0 ],
shifted_f_uv.shape[1 ]),
color=0 ,
)
draw = ImageDraw.Draw(x_pass_filter)
ellipse_r = 50
center = (shifted_f_uv.shape[0 ] // 2 ,
shifted_f_uv.shape[1 ] // 2 )
ellipse_pos = (center[0 ] - ellipse_r,
center[1 ] - ellipse_r,
center[0 ] + ellipse_r,
center[1 ] + ellipse_r)
draw.ellipse(ellipse_pos, fill=255 )
filter_array = np.asarray(x_pass_filter)
filtered_f_uv = np.multiply(shifted_f_uv, filter_array)
magnitude_spectrum2d = 20 * np.log(np.absolute(filtered_f_uv))
unshifted_f_uv = np.fft.fftshift(filtered_f_uv)
i_f_xy = np.fft.ifft2(unshifted_f_uv).real
fig, axes = plt.subplots(1 , 4 , figsize=(12 , 4 ))
for ax in axes:
for spine in ax.spines.values():
spine.set_visible(False )
ax.set_xticks([])
ax.set_yticks([])
axes[0 ].imshow(f_xy, cmap='gray' )
axes[0 ].set_title('Input Image' )
axes[1 ].imshow(filter_array, cmap='gray' )
axes[1 ].set_title('Filter Image' )
axes[2 ].imshow(magnitude_spectrum2d, cmap='gray' )
axes[2 ].set_title('Filtered Magnitude Spectrum' )
axes[3 ].imshow(i_f_xy, cmap='gray' )
axes[3 ].set_title('Reversed Image' )
plt.show()
if __name__ == '__main__' :
main()
上記を実行してみよう。
$ python imglpf.py
すると、次のようなグラフが得られる。
分かりにくいかもしれないけど、復元した画像はちょっとぼやけている。
また、ぼやけ方も縞模様になっていて波を感じるものとなった。
これは、画像を構成するすべての周波数の中から、高周波の成分がフィルタによって取り除かれたことで生じている。
FFT -> LPF -> IFFT
中心のごく一部だけを取り出している (つまり、多くの情報が失われている) のに、それっぽい画像になるのはなんとも面白い。
なお、フィルタに使う円の半径を小さくすれば、それだけ高周波の成分が少なくなって、より分かりやすくぼやける。
ハイパスフィルタ (High Pass Filter) をかけて復元してみる
次は、同様に高周波成分だけを取り出すハイパスフィルタをかけてみよう。
つまり、中心だけを取り除くことになる。
先ほどとのコードの違いは、適用するフィルタの周波数特性が違うだけ。
from PIL import Image
from PIL import ImageDraw
import numpy as np
from matplotlib import pyplot as plt
def main ():
img = Image.open('lena.png' )
gray_img = img.convert('L' )
f_xy = np.asarray(gray_img)
f_uv = np.fft.fft2(f_xy)
shifted_f_uv = np.fft.fftshift(f_uv)
x_pass_filter = Image.new(mode='L' ,
size=(shifted_f_uv.shape[0 ],
shifted_f_uv.shape[1 ]),
color=255 ,
)
draw = ImageDraw.Draw(x_pass_filter)
ellipse_r = 50
center = (shifted_f_uv.shape[0 ] // 2 ,
shifted_f_uv.shape[1 ] // 2 )
ellipse_pos = (center[0 ] - ellipse_r,
center[1 ] - ellipse_r,
center[0 ] + ellipse_r,
center[1 ] + ellipse_r)
draw.ellipse(ellipse_pos, fill=0 )
filter_array = np.asarray(x_pass_filter)
filtered_f_uv = np.multiply(shifted_f_uv, filter_array)
magnitude_spectrum2d = 20 * np.log(np.absolute(filtered_f_uv))
unshifted_f_uv = np.fft.fftshift(filtered_f_uv)
i_f_xy = np.fft.ifft2(unshifted_f_uv).real
fig, axes = plt.subplots(1 , 4 , figsize=(12 , 4 ))
for ax in axes:
for spine in ax.spines.values():
spine.set_visible(False )
ax.set_xticks([])
ax.set_yticks([])
axes[0 ].imshow(f_xy, cmap='gray' )
axes[0 ].set_title('Input Image' )
axes[1 ].imshow(filter_array, cmap='gray' )
axes[1 ].set_title('Filter Image' )
axes[2 ].imshow(magnitude_spectrum2d, cmap='gray' )
axes[2 ].set_title('Filtered Magnitude Spectrum' )
axes[3 ].imshow(i_f_xy, cmap='gray' )
axes[3 ].set_title('Reversed Image' )
plt.show()
if __name__ == '__main__' :
main()
上記を保存して実行する。
$ python imghpf.py
すると、今度は次のようなグラフが得られる。
またもや分かりにくいけど、復元した画像は元の画像の輪郭だけがぼんやりと浮かび上がったものとなっている。
これが画像を構成する高周波の成分で、先ほどの復元画像から取り除かれたものと解釈できる。
FFT -> HPF -> IFFT
カラー画像を扱う場合
なお、カラー画像を扱う場合、それぞれのチャネルごとに処理する必要がある。
以下のサンプルコードでは、最初のサンプルコードを RGB のチャネルでそれぞれ適用している。
from itertools import chain
from PIL import Image
import numpy as np
from matplotlib import pyplot as plt
def main ():
img = Image.open('lena.png' )
f_xy_rgb = np.asarray(img)
fig, axes = plt.subplots(3 , 5 , figsize=(12 , 6 ))
for ax in chain.from_iterable(axes):
for spine in ax.spines.values():
spine.set_visible(False )
ax.set_xticks([])
ax.set_yticks([])
axes[1 ][0 ].imshow(f_xy_rgb)
axes[1 ][0 ].set_title('Input Image (RGB)' )
i_f_xy_rgb = np.empty_like(f_xy_rgb)
channel_names = ('R' , 'G' , 'B' )
channel_indices = range (f_xy_rgb.shape[2 ])
zipped_channel_info = zip (channel_names, channel_indices)
for channel_name, channel_index in zipped_channel_info:
f_xy = f_xy_rgb[:, :, channel_index]
axes[channel_index][1 ].imshow(f_xy, cmap='gray' )
axes[channel_index][1 ].set_title(f'Input Image ({channel_name})' )
f_uv = np.fft.fft2(f_xy)
shifted_f_uv = np.fft.fftshift(f_uv)
magnitude_spectrum2d = 20 * np.log(np.absolute(shifted_f_uv))
unshifted_f_uv = np.fft.fftshift(shifted_f_uv)
i_f_xy = np.fft.ifft2(unshifted_f_uv).real
axes[channel_index][2 ].imshow(magnitude_spectrum2d, cmap='gray' )
axes[channel_index][2 ].set_title(f'Magnitude Spectrum ({channel_name})' )
axes[channel_index][3 ].imshow(i_f_xy, cmap='gray' )
axes[channel_index][3 ].set_title(f'Reversed Image ({channel_name})' )
i_f_xy_rgb[:, :, channel_index] = i_f_xy
axes[1 ][4 ].imshow(i_f_xy_rgb)
axes[1 ][4 ].set_title('Reversed Image (RGB)' )
plt.show()
if __name__ == '__main__' :
main()
上記を保存して実行する。
$ python imgfftc.py
すると、次のようなグラフが得られる。
FFT (RGB) -> IFFT (RGB)
ちゃんと RGB それぞれのチャネルの情報から、カラー画像が復元できていることがわかる。
めでたしめでたし。