OpenCV 4.13.0
Open Source Computer Vision
読み込み中...
検索中...
見つかりません
🤖 AIによる機械翻訳(非公式) — これは OpenCV 4.13.0 公式リファレンス(英語)を AI (Claude) で自動翻訳したものです。訳に誤りを含む場合があります。正確な情報は 公式英語版(原文) を参照してください。
フーリエ変換

目的

このセクションでは、次のことを学ぶ

  • OpenCVを使って画像のフーリエ変換を求める
  • Numpyで利用可能なFFT関数を活用する
  • フーリエ変換のいくつかの応用
  • 次の関数を扱う : cv.dft(), cv.idft() など

理論

フーリエ変換は、さまざまなフィルタの周波数特性を解析するために使われる。画像に対しては、周波数領域を求めるために 2次元離散フーリエ変換 (DFT) が用いられる。DFTの計算には 高速フーリエ変換 (FFT) と呼ばれる高速なアルゴリズムが使われる。これらの詳細は、画像処理や信号処理の任意の教科書で見つけることができる。Additional Resources_ セクションを参照。

正弦波信号 \(x(t) = A \sin(2 \pi ft)\) について、\(f\) は信号の周波数であると言える。その周波数領域をとると、\(f\) にスパイクが見られる。信号がサンプリングされて離散信号になった場合、同じ周波数領域が得られるが、範囲 \([- \pi, \pi]\) または \([0,2\pi]\)(N点DFTの場合は \([0,N]\))で周期的になる。画像は2方向にサンプリングされた信号と考えることができる。したがって、X方向とY方向の両方でフーリエ変換をとると、画像の周波数表現が得られる。

より直感的に言えば、正弦波信号の場合、振幅が短い時間で非常に速く変化するなら、それは高周波信号だと言える。ゆっくり変化するなら、低周波信号である。同じ考え方を画像に拡張できる。画像のどこで振幅が激しく変化するだろうか。エッジ点やノイズである。したがって、エッジやノイズは画像中の高周波成分だと言える。振幅の変化があまりなければ、それは低周波成分である。(周波数変換を例とともに直感的に説明するいくつかのリンクを Additional Resources_ に追加してある。)

ではフーリエ変換の求め方を見ていこう。

Numpyでのフーリエ変換

まず、Numpyを使ってフーリエ変換を求める方法を見る。Numpyにはこれを行うためのFFTパッケージがある。np.fft.fft2() は周波数変換を提供し、その結果は複素配列となる。第1引数は入力画像で、これはグレースケールである。第2引数は省略可能で、出力配列のサイズを決める。入力画像のサイズより大きい場合、FFTの計算前に入力画像はゼロでパディングされる。入力画像より小さい場合、入力画像は切り取られる。引数を渡さなければ、出力配列のサイズは入力と同じになる。

結果が得られると、ゼロ周波数成分(DC成分)は左上の角に位置する。これを中央に持ってきたい場合は、結果を両方向に \(\frac{N}{2}\) だけシフトする必要がある。これは関数 np.fft.fftshift() によって簡単に行える。(解析がより容易になる。)周波数変換が求められたら、振幅スペクトルを求めることができる。

import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt
img = cv.imread('messi5.jpg', cv.IMREAD_GRAYSCALE)
assert img is not None, "file could not be read, check with os.path.exists()"
f = np.fft.fft2(img)
fshift = np.fft.fftshift(f)
magnitude_spectrum = 20*np.log(np.abs(fshift))
plt.subplot(121),plt.imshow(img, cmap = 'gray')
plt.title('Input Image'), plt.xticks([]), plt.yticks([])
plt.subplot(122),plt.imshow(magnitude_spectrum, cmap = 'gray')
plt.title('Magnitude Spectrum'), plt.xticks([]), plt.yticks([])
plt.show()
Mat imread(const String &filename, int flags=IMREAD_COLOR_BGR)
Loads an image from a file.

結果は以下のようになる:

image

中央により白い領域が見え、低周波成分が多いことを示しているのがわかる。

周波数変換が求められたので、周波数領域でハイパスフィルタリングのような操作を行い、画像を再構成する、すなわち逆DFTを求めることができる。そのためには、サイズ60x60の矩形ウィンドウでマスクすることで単純に低周波を取り除く。次に np.fft.ifftshift() を使って逆シフトを適用し、DC成分を再び左上の角に戻す。それから np.ifft2() 関数を使って逆FFTを求める。結果は、やはり複素数となる。その絶対値を取ることができる。

rows, cols = img.shape
crow, ccol = rows//2, cols//2
fshift[crow-30:crow+31, ccol-30:ccol+31] = 0
f_ishift = np.fft.ifftshift(fshift)
img_back = np.fft.ifft2(f_ishift)
img_back = np.real(img_back)
plt.subplot(131),plt.imshow(img, cmap = 'gray')
plt.title('Input Image'), plt.xticks([]), plt.yticks([])
plt.subplot(132),plt.imshow(img_back, cmap = 'gray')
plt.title('Image after HPF'), plt.xticks([]), plt.yticks([])
plt.subplot(133),plt.imshow(img_back)
plt.title('Result in JET'), plt.xticks([]), plt.yticks([])
plt.show()

結果は以下のようになる:

image

結果は、ハイパスフィルタリングがエッジ検出操作であることを示している。これは Image Gradients の章で見たものである。これはまた、画像データの大部分がスペクトルの低周波領域に存在することも示している。いずれにせよ、NumpyでDFTやIDFTなどを求める方法を見てきた。次に、OpenCVでそれを行う方法を見よう。

結果、特にJETカラーの最後の画像をよく観察すると、いくつかのアーティファクトが見える(赤い矢印で1つの例を示した)。そこにはリプル状の構造が見え、これは リンギング効果 と呼ばれる。これはマスクに使った矩形ウィンドウが原因である。このマスクがsinc形状に変換され、この問題を引き起こす。したがって、矩形ウィンドウはフィルタリングには使われない。より良い選択肢はガウスウィンドウである。

OpenCVでのフーリエ変換

OpenCVはこのために関数 cv.dft()cv.idft() を提供する。これは前回と同じ結果を返すが、2チャンネルになる。第1チャンネルには結果の実部が、第2チャンネルには結果の虚部が入る。入力画像はまずnp.float32に変換する必要がある。その方法を見ていく。

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
img = cv.imread('messi5.jpg', cv.IMREAD_GRAYSCALE)
assert img is not None, "file could not be read, check with os.path.exists()"
dft = cv.dft(np.float32(img),flags = cv.DFT_COMPLEX_OUTPUT)
dft_shift = np.fft.fftshift(dft)
magnitude_spectrum = 20*np.log(cv.magnitude(dft_shift[:,:,0],dft_shift[:,:,1]))
plt.subplot(121),plt.imshow(img, cmap = 'gray')
plt.title('Input Image'), plt.xticks([]), plt.yticks([])
plt.subplot(122),plt.imshow(magnitude_spectrum, cmap = 'gray')
plt.title('Magnitude Spectrum'), plt.xticks([]), plt.yticks([])
plt.show()
void magnitude(InputArray x, InputArray y, OutputArray magnitude)
Calculates the magnitude of 2D vectors.
void dft(InputArray src, OutputArray dst, int flags=0, int nonzeroRows=0)
Performs a forward or inverse Discrete Fourier transform of a 1D or 2D floating-point array.
覚え書き
振幅と位相を一度に返す cv.cartToPolar() を使うこともできる

さて、今度は逆DFTを行う必要がある。前回のセッションではHPFを作成したが、今回は画像中の高周波成分を取り除く方法、すなわち画像にLPFを適用する方法を見る。これは実際には画像を平滑化する。このために、まず低周波で高い値(1)を持つマスクを作成する、すなわち低周波成分を通過させ、高周波領域では0とする。

rows, cols = img.shape
crow, ccol = rows//2, cols//2
# create a mask first, center square is 1, remaining all zeros
mask = np.zeros((rows,cols,2),np.uint8)
mask[crow-30:crow+30, ccol-30:ccol+30] = 1
# apply mask and inverse DFT
fshift = dft_shift*mask
f_ishift = np.fft.ifftshift(fshift)
img_back = cv.idft(f_ishift)
img_back = cv.magnitude(img_back[:,:,0],img_back[:,:,1])
plt.subplot(121),plt.imshow(img, cmap = 'gray')
plt.title('Input Image'), plt.xticks([]), plt.yticks([])
plt.subplot(122),plt.imshow(img_back, cmap = 'gray')
plt.title('Magnitude Spectrum'), plt.xticks([]), plt.yticks([])
plt.show()
void idft(InputArray src, OutputArray dst, int flags=0, int nonzeroRows=0)
Calculates the inverse Discrete Fourier Transform of a 1D or 2D array.

結果を見てみる:

image
覚え書き
いつものように、OpenCVの関数 cv.dft()cv.idft() はNumpyの対応する関数より高速である。しかし、Numpyの関数の方がユーザーフレンドリーである。パフォーマンスの問題に関する詳細は、以下のセクションを参照。

DFTのパフォーマンス最適化

DFT計算のパフォーマンスは、ある種の配列サイズに対してより良くなる。配列サイズが2のべき乗のとき最速である。サイズが2、3、5の積である配列も非常に効率的に処理される。したがって、コードのパフォーマンスが気になる場合は、DFTを求める前に(ゼロをパディングして)配列のサイズを任意の最適なサイズに変更できる。OpenCVでは、手動でゼロをパディングする必要がある。しかしNumpyでは、FFT計算の新しいサイズを指定すれば、自動的にゼロをパディングしてくれる。

では、この最適なサイズをどうやって求めるのか。OpenCVはこのために関数 cv.getOptimalDFTSize() を提供する。これは cv.dft()np.fft.fft2() の両方に適用できる。IPythonのマジックコマンドtimeitを使って、それらのパフォーマンスを確認してみよう。

In [15]: img = cv.imread('messi5.jpg', cv.IMREAD_GRAYSCALE)
In [16]: assert img is not None, "file could not be read, check with os.path.exists()"
In [17]: rows,cols = img.shape
In [18]: print("{} {}".format(rows,cols))
342 548
In [19]: nrows = cv.getOptimalDFTSize(rows)
In [20]: ncols = cv.getOptimalDFTSize(cols)
In [21]: print("{} {}".format(nrows,ncols))
360 576
int getOptimalDFTSize(int vecsize)
Returns the optimal DFT size for a given vector size.

ご覧のとおり、サイズ (342,548) は (360, 576) に変更されている。次にこれを(OpenCVのために)ゼロでパディングし、DFTの計算性能を求める。これは新しい大きなゼロ配列を作成してそこにデータをコピーするか、cv.copyMakeBorder() を使うことで実行できる。

nimg = np.zeros((nrows,ncols))
nimg[:rows,:cols] = img

または:

right = ncols - cols
bottom = nrows - rows
bordertype = cv.BORDER_CONSTANT #just to avoid line breakup in PDF file
nimg = cv.copyMakeBorder(img,0,bottom,0,right,bordertype, value = 0)
void copyMakeBorder(InputArray src, OutputArray dst, int top, int bottom, int left, int right, int borderType, const Scalar &value=Scalar())
Forms a border around an image.

次に、Numpy関数のDFT性能比較を計算する:

In [22]: %timeit fft1 = np.fft.fft2(img)
10 loops, best of 3: 40.9 ms per loop
In [23]: %timeit fft2 = np.fft.fft2(img,[nrows,ncols])
100 loops, best of 3: 10.4 ms per loop

4倍の高速化を示している。次に同じことをOpenCVの関数で試してみる。

In [24]: %timeit dft1= cv.dft(np.float32(img),flags=cv.DFT_COMPLEX_OUTPUT)
100 loops, best of 3: 13.5 ms per loop
In [27]: %timeit dft2= cv.dft(np.float32(nimg),flags=cv.DFT_COMPLEX_OUTPUT)
100 loops, best of 3: 3.11 ms per loop

こちらも4倍の高速化を示している。また、OpenCVの関数はNumpyの関数より約3倍高速であることもわかる。これは逆FFTについても検証でき、それは演習として読者に委ねる。

なぜラプラシアンはハイパスフィルタなのか?

あるフォーラムで同様の質問がなされた。その質問は、なぜラプラシアンはハイパスフィルタなのか? なぜSobelはHPFなのか? といったものである。そして、それに対して最初に与えられた回答はフーリエ変換の観点からのものであった。より大きなサイズのFFTでラプラシアンのフーリエ変換をとってみればよい。それを解析する:

import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt
# simple averaging filter without scaling parameter
mean_filter = np.ones((3,3))
# creating a gaussian filter
gaussian = x*x.T
# different edge detecting filters
# scharr in x-direction
scharr = np.array([[-3, 0, 3],
[-10,0,10],
[-3, 0, 3]])
# sobel in x direction
sobel_x= np.array([[-1, 0, 1],
[-2, 0, 2],
[-1, 0, 1]])
# sobel in y direction
sobel_y= np.array([[-1,-2,-1],
[0, 0, 0],
[1, 2, 1]])
# laplacian
laplacian=np.array([[0, 1, 0],
[1,-4, 1],
[0, 1, 0]])
filters = [mean_filter, gaussian, laplacian, sobel_x, sobel_y, scharr]
filter_name = ['mean_filter', 'gaussian','laplacian', 'sobel_x', \
'sobel_y', 'scharr_x']
fft_filters = [np.fft.fft2(x) for x in filters]
fft_shift = [np.fft.fftshift(y) for y in fft_filters]
mag_spectrum = [np.log(np.abs(z)+1) for z in fft_shift]
for i in range(6):
plt.subplot(2,3,i+1),plt.imshow(mag_spectrum[i],cmap = 'gray')
plt.title(filter_name[i]), plt.xticks([]), plt.yticks([])
plt.show()
Mat getGaussianKernel(int ksize, double sigma, int ktype=CV_64F)
Returns Gaussian filter coefficients.

結果を見てみる:

image

画像から、各カーネルがどの周波数領域を遮断し、どの領域を通過させるかがわかる。その情報から、各カーネルがなぜHPFまたはLPFなのかを説明できる。

追加リソース

  1. An Intuitive Explanation of Fourier Theory (Steven Lehar 著)
  2. HIPRの Fourier Transform
  3. What does frequency domain denote in case of images?