画像処理について考える (1) — 高速Fourier変換
目的
画像処理の教科書を開くと、ローパスフィルタの説明などで、画像を Fourier 変換して高周波成分をカットするような話が出て来る。そして、Fourier 変換の可視化として以下のような絵が掲載されている。
このグランドクロスかグランドクルスかそういった光の十字架は何なのだろうか?ということについて軽く試してみたい。
高速 Fourier 変換
Wikipedia で 高速フーリエ変換 を見ると、
高速フーリエ変換(こうそくフーリエへんかん、英: fast Fourier transform, FFT)は、離散フーリエ変換(英: discrete Fourier transform, DFT)を計算機上で高速に計算するアルゴリズムである。
ということで、正体は離散 Fourier 変換である。以下では、高速なアルゴリズム部分を忘れてしまって、離散 Fourier 変換について復習しよう。
1 次元の連続函数
適当に
と置いてみよう。
次に
このような導出が良いのか良く知らないが、Eq. (1) が 離散 Fourier 変換 である。一方、逆離散 Fourier 変換は
となる。
1 次元離散 Fourier 変換のナイーブな実装
後で使うので、以下を import しておく:
import numpy as np
from PIL import Image
そして、1 次元の離散 Fourier 変換を定義からナイーブに実装する:
def naive_dft(a: np.ndarray):
N = a.shape[0]
exps = np.exp(-2*np.pi*1j*np.arange(N)/N)
return np.array([np.sum(a * exps ** m) for m in range(N)])
def naive_idft(a: np.ndarray):
N = a.shape[0]
exps = np.exp(2*np.pi*1j*np.arange(N)/N)
return np.array([np.sum(a * exps ** m) for m in range(N)]) / N
適当にテストしてみよう:
a = np.arange(10, dtype=complex)
print(np.allclose(naive_dft(a), np.fft.fft(a)))
print(np.allclose(naive_idft(a), np.fft.ifft(a)))
print(np.allclose(np.fft.ifft(np.fft.fft(a)), a))
print(np.allclose(naive_idft(naive_dft(a)), a))
True
True
True
True
たぶん良さそうだ。
2 次元離散 Fourier 変換のナイーブな実装
1 次元版と同じノリで考えると、通常の Fourier 変換の多次元版から考えて実装は以下だ。大分ナイーブすぎて効率は最悪そうだが、今回の目的はそこではない。
def naive_dft2(a: np.ndarray):
def coeff(a, m, n):
h, w = a.shape
exps = [np.exp(-2*np.pi*1j*p*m/h)*np.exp(-2*np.pi*1j*np.arange(w)*n/w)
for p in range(h)]
exps = np.array(exps).reshape(a.shape)
return np.sum(a * exps)
a_ = np.array([coeff(a, m, n)
for m in range(a.shape[0]) for n in range(a.shape[1])])
return a_.reshape(a.shape)
def naive_idft2(a: np.ndarray):
def coeff(a, m, n):
h, w = a.shape
exps = [np.exp(2*np.pi*1j*p*m/h)*np.exp(2*np.pi*1j*np.arange(w)*n/w)
for p in range(h)]
exps = np.array(exps).reshape(a.shape)
return np.sum(a * exps)
a_ = np.array([coeff(a, m, n)
for m in range(a.shape[0]) for n in range(a.shape[1])])
return (a_ / np.prod(np.array(a.shape))).reshape(a.shape)
適当にテストしてみよう:
b = np.arange(4*5, dtype=complex).reshape(4, 5)
print(np.allclose(naive_dft2(b), np.fft.fft2(b)))
print(np.allclose(naive_idft2(b), np.fft.ifft2(b)))
True
True
たぶん良さそうだ。
画像処理での応用
画像処理の本にある光の十字架が出て来るところでは fftshift
という API が使われる。これはどうやら検索した限りでは、
def naive_fftshift(fimg):
return np.roll(fimg, [v // 2 for v in fimg.shape], [0, 1])
画像として何を使おうか考えたが、浮世絵や日本画も膨大!シカゴ美術館が5万件超の所蔵作品を無料ダウンロード公開!商用利用OK という記事があったので、Under the Wave off Kanagawa (Kanagawa oki nami ura), also known as The Great Wave, from the series “Thirty-Six Views of Mount Fuji (Fugaku sanjūrokkei)” を使ってみることにした。
ナイーブな実装でも計算時間的に耐えるようにリサイズして、かつチャネル数を 1 のグレースケールに落としている。
さっそく計算してみよう:
%%time
img = np.array(im)
fimg = naive_dft2(img)
mag = 15*np.log(np.abs(naive_fftshift(fimg)))
img2 = naive_idft2(fimg)
CPU times: user 4min 9s, sys: 1.56 s, total: 4min 11s
Wall time: 4min 12s
びっくりするほど遅いことに目を瞑ればよく見る結果が得られていることに気が付く。
光の十字架の正体は
- 低周波成分が中央に来るようにスクロールして
- 複素数値のデータなので絶対値をとって実数にして
- 対数をとって適当にスケールしたもの
ということになる。
大部分が低周波成分なので、高周波成分をカットして逆離散 Fourier 変換したらローパスフィルタになるよ、というよくある話につながる。
まとめ
内容的には画像処理の教科書の通りなので、特に新しい部分は何もないのだが、何も考えずに使っていた API の実装を自分で行うことで、中で何が行われているかがより明確になったと思う。
Discussion