【翻譯:OpenCV-Python教程】傅里葉變換

⚠️由於自己的拖延症,3.4.3翻到一半,OpenCV發佈了4.0.0了正式版,所以接下來是按照4.0.0翻譯的。

⚠️除了版本之外,其他還是照舊,Fourier Transform,原文

目標

在這一節,我們將學習:

  • 用OpenCV求圖像的傅里葉變換
  • 利用Numpy中可用的FFT函數(FFT下面有解釋)
  • 傅里葉變換的一些應用
  • 我們會遇到以下這些函數:cv.dft()cv.idft() 等等

理論

利用傅立葉變換是用來分析了各種濾波器的頻率特性。對於圖像來說,二維離散傅里葉變換(2D Discrete Fourier Transform (DFT))用來找出頻域。一個較快的算法叫做快速傅里葉轉換(Fast Fourier Transform (FFT))用來計算DFT。關於這些的細節,可以在任何圖像處理或者信息處理的教科書裏找到。請看額外資源的那一部分。

對於一個正弦信號,x(t)=Asin(2πft),我們可以認爲f就是信號頻率。如果我們取到它的頻域,那我們可以看到在f處有一個峯值。如果信號被採樣形成一個離散的信號,我們會取到相同的頻域,但卻是週期性的出現在區間[−π,π]或者[0,2π] (或者[0,N],對於多點離散傅里葉變換來說)。你可以把一張圖像看作是在兩個方向上採樣的信號。在X和Y方向上進行傅里葉變換,就可以描述出圖像的頻率。

更直觀的講,對於正弦信號,如果振幅在短時間內變化非常大,你可以說它是一個高頻信號。如果它變化很慢,那它就是個低頻信號。你可以把同樣的想法延伸到圖像上,圖像上什麼地方的振幅會激烈變化呢?在邊緣,或者噪音點上啊。所以我們可以說邊緣和噪音是一張圖像中的高頻內容。如果沒有那麼大的振幅變化,那它就屬於低頻分量。(一些鏈接已經被添加在了額外資源的部分,它通過一個示例直觀的解釋了頻率轉換)。

現在我們來看看如何求傅里葉變換。

Numpy裏的傅里葉變換

首先我們看看如何用Numpy求傅里葉變換。Numpy有一個FFT包來做這件事。np.fft.fft2()提供給我們頻率轉換的算法,返回一個複雜的數組。它的第一個參數是輸入圖像,圖像得是灰度圖。第二個參數是可選的,它決定了輸出數組的大小。如果它大於輸入圖像,輸入圖像在做FFT算法之前,就要用零來填充(到和這個輸出大小一樣)。如果它小於輸入圖像,輸入圖像就會被裁剪。如果你不傳這個參數,輸出數組的大小就會和輸入圖像一致。

現在一旦你得到這個結果,零頻分量(直流分量(譯者附:DC components))將會出現在左上角,如果你想要把它們移動到中心,你需要同時在兩個方向把結果移動2N。這可以通過函數 np.fft.fftshift() 輕鬆的辦到(它更易分析)。一旦你算出了頻率變換,你就能算出振幅譜。

import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt
img = cv.imread('messi5.jpg',0)
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()

結果看起來如下:

fft1.jpg

看,你可以發現,更多更白的區域在中心處,這說明低頻內容更多。

然後你找到了頻率變換,接下來你可以在頻域內做一些操作。比如高通濾波、重構圖像,這些步驟其實就是求逆DFT。爲了達到這個目的,你只需用一個60x60大小的矩形窗口來屏蔽低頻。然後通過 np.fft.ifftshift() 來做逆變換,這樣直流分量就會再次出現在左上角。然後使用 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()

結果看起來如下:

fft2.jpg

這個結果告訴我們,高通濾波其實是一種邊緣檢測的操作,這早在圖像梯度那一章我們就看到過了。這個結果還告訴了我們,其實大量的圖像實際都存在於頻譜的低頻區域。總之我們知道了用Numpy怎麼算DFT,IDFT等等。現在看看在OpenCV怎麼實現。

如果你仔細看看結果,特別是以JET色圖模式(譯者注:JET從藍到紅,中間經過青綠、黃和橙色。它是HSV色圖的一個變異。)來看最後一張的話。你可以看到一些副產物(我已經用紅色箭頭標記了其中一個實例)。它顯示了一些波紋狀的結構,這被稱爲振鈴效應。它是由我們用來遮蔽的矩形窗口(60X60那個)引起的。這個遮蔽層被轉換成辛格函數的形狀(譯者注:即sinc(x),頻域中的矩形對應時域中的sinc函數,它的函數圖像看起來像是連續、但高度漸漸降低的拱橋),這導致了這個問題。所以矩形窗口其實不適用於過濾。高斯窗口是更好的選擇。

OpenCV裏的傅里葉轉換

OpenCV爲此提供了函數 cv.dft()cv.idft()。但是有兩個通道。第一通道會有結果的實部,第二通道會有結果的虛部。輸入圖像應提前轉換爲np.float32的格式。我們來看看怎麼做。

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
img = cv.imread('messi5.jpg',0)
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()

提示

你也可以使用 cv.cartToPolar() 函數,一次性返回大小和方向。(譯者注:數學中的複數和向量可以相互轉換,上面的方法是把結果看做複數,因此分實部虛部。下面的方法則是看做向量,因此分大小和方向。總之返回的結果數值意義其實是相同的,可以通過某些方式相互轉換。)

現在我們要做逆DFT。在之前的學習中,我們創建了一個高通濾波器(譯者注:HPF,High Pass Filtering)這次我們將看到如何刪除圖像中的高頻內容,即我們將對圖像應用低通濾波器(LPF)。它實際上會模糊圖像。爲此,我們首先創建一個在低頻下具有高值(1)的遮罩層,就是說,(這個遮罩層的樣子應該是)我們在低頻區域傳入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()

看看結果:

fft4.jpg

提示

和往常一樣,OpenCV函數cv.dft()cv.idft()要比Numpy的版本快。但是Numpy函數對用戶使用來說更友好。有關性能問題的更多細節,請參見下面一節。

DFT的性能優化

Performance of DFT calculation is better for some array size. It is fastest when array size is power of two. The arrays whose size is a product of 2’s, 3’s, and 5’s are also processed quite efficiently. So if you are worried about the performance of your code, you can modify the size of the array to any optimal size (by padding zeros) before finding DFT. For OpenCV, you have to manually pad zeros. But for Numpy, you specify the new size of FFT calculation, and it will automatically pad zeros for you.

So how do we find this optimal size ? OpenCV provides a function, cv.getOptimalDFTSize() for this. It is applicable to both cv.dft() and np.fft.fft2(). Let's check their performance using IPython magic command timeit.

In [16]: img = cv.imread('messi5.jpg',0)

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

See, the size (342,548) is modified to (360, 576). Now let's pad it with zeros (for OpenCV) and find their DFT calculation performance. You can do it by creating a new big zero array and copy the data to it, or use cv.copyMakeBorder().

nimg = np.zeros((nrows,ncols))

nimg[:rows,:cols] = img

OR:

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)

Now we calculate the DFT performance comparison of Numpy function:

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

It shows a 4x speedup. Now we will try the same with OpenCV functions.

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

It also shows a 4x speed-up. You can also see that OpenCV functions are around 3x faster than Numpy functions. This can be tested for inverse FFT also, and that is left as an exercise for you.

Why Laplacian is a High Pass Filter?

A similar question was asked in a forum. The question is, why Laplacian is a high pass filter? Why Sobel is a HPF? etc. And the first answer given to it was in terms of Fourier Transform. Just take the fourier transform of Laplacian for some higher size of FFT. Analyze it:

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

x = cv.getGaussianKernel(5,10)

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 xrange(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()

See the result:

fft5.jpg

image

From image, you can see what frequency region each kernel blocks, and what region it passes. From that information, we can say why each kernel is a HPF or a LPF

Additional Resources

  1. An Intuitive Explanation of Fourier Theory by Steven Lehar
  2. Fourier Transform at HIPR
  3. What does frequency domain denote in case of images?

Exercises


上篇:【翻譯:OpenCV-Python教程】OpenCV裏的直方圖

下篇:【翻譯:OpenCV-Python教程】圖像金字塔

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章