1. 程式人生 > >Python下opencv使用筆記(十)(影象頻域濾波與傅立葉變換)

Python下opencv使用筆記(十)(影象頻域濾波與傅立葉變換)

前面曾經介紹過空間域濾波,空間域濾波就是用各種模板直接與影象進行卷積運算,實現對影象的處理,這種方法直接對影象空間操作,操作簡單,所以也是空間域濾波。

頻域濾波說到底最終可能是和空間域濾波實現相同的功能,比如實現影象的輪廓提取,在空間域濾波中我們使用一個拉普拉斯模板就可以提取,而在頻域內,我們使用一個高通濾波模板(因為輪廓在頻域內屬於高頻訊號),可以實現輪廓的提取,後面也會把拉普拉斯模板頻域化,會發現拉普拉斯其實在頻域來講就是一個高通濾波器。

既然是頻域濾波就涉及到把影象首先變到頻域內,那麼把影象變到頻域內的方法就是傅立葉變換。關於傅立葉變換,感覺真是個偉大的發明,尤其是其在訊號領域的應用,對於傅立葉變換的理解,要是剛接觸這個東西,想要理解還真是非常的困難,除非你的數學功底特別好。這裡推薦一個非常好的通俗易懂的部落格(待會再去o-_-)

傅立葉的原理表明,任何連續測量的時序或訊號,都可以表示為不同頻率的正弦波訊號的無限疊加。利用傅立葉變換演算法直接測量原始訊號,以累加方式來計算該訊號中不同正弦波訊號的頻率、振幅和相位就可以表示原始訊號。這裡借用上述部落格的一個圖:這裡寫圖片描述
這個圖就是把時域影象(大概是方波)變成了一系列的正弦波的線性疊加,其等價關係可以表示為:

f()=A1sin(w1x+ϕ1)+A2sin(w2x+ϕ2)+...
那麼w1,w2,...可以看成是頻率的變化(一般認為就是從1,2,…n定死了),所有的A就是對應頻率下的振幅,所有的ϕ就是對應頻率下的相位,那麼對於任一個訊號,如果都認為頻率w是從1,2,3…一直增加的話,那麼每個訊號就只由一組振幅與一組ϕ
來決定,他們的不同決定了最終訊號的不同。

再來理解下什麼是振幅,振幅就是各個頻率下的訊號的決定程度有多大,如果某個頻率的振幅越大,那麼它對原始訊號的的重要性越大,像上圖,當然是w=1的時候振幅最大,說明它對總的訊號影響最多(去掉w=1的訊號,原始訊號講嚴重變形)。越往後面,也就是越高頻,振幅逐漸減小,那麼他們的作用就越小,而他們對於整體訊號又有什麼影響呢?既然越小,那就是影響小,所以其實去掉,原始訊號也基本上不變,他們影響就在於對原始訊號的細節上的表現,比如原始訊號上的邊邊角角,偶爾有個小凸起凹槽什麼的,這些小細節部分都是靠這些個影響不大的高頻訊號來表現出來的。深入推廣一下,這就很好理解為什麼影象的高頻訊號其實表現出來的就是影象的邊緣輪廓、噪聲等等這些細節的東西了,而低頻訊號,表現的卻是影象上整塊整塊灰度大概一樣的區域了(這些個區域又稱為直流分量區域)。

再來理解下什麼是相位,相位表示其實表面對應頻率下的正弦分量偏離原點的程度,再借用下上述部落格中的一個圖,把分量示意圖放大了:
這裡寫圖片描述
上圖看到,如果各個頻率的分量相位都是0的話,那麼每個正弦分量的最大值(在頻率軸附近的那個最大值)都會落在頻率軸為0上,然而上述圖並不是這樣。在說簡單一點,比如原始訊號上有個凹槽,正好是由某一頻率的分量疊加出來的,那麼如果這個頻率的相位變大一點或者變小一點的話,帶來的影響就會使得這個凹槽向左或者向右移動一下,也就是說,相位的作用就是精確定位到訊號上一點的位置的。

好了,有了上述的概念,再來看看影象的傅立葉變換,上述舉得例子是一維訊號的傅立葉變換,並且訊號是連續的,我們知道影象是二維離散的,連續與離散都可以用傅立葉進行變換,那麼二維訊號無非就是在x方向與y方向都進行一次一維的傅立葉變換得到,這麼看來,可以想象,它的頻率構成就是一個網格矩陣了,橫軸從w=1到n,縱軸也是這樣。所有影象的頻率構成都認為是這樣的,那麼不同的就是一幅圖的振幅與相位了(振幅與相位此時同樣是一個網格矩陣),也就是說你在opencv或者matlab下對影象進行傅立葉變換後其實是可以得到影象的振幅圖與相點陣圖的,而想把影象從頻域空間恢復到時域空間,必須要同時有影象的振幅圖與相點陣圖才可以,缺少一個就恢復的不完整(後面會實驗看看)。
下面看看二維傅立葉變換,一個影象為M*N的影象f(x,y)進過離散傅立葉變換得到F(u,v),那麼一般的公式為:

F(u,v)=x=0M1y=0N1f(x,y)ej2π(ux/M+vy/N)
它的反變換就是
f(x,y)=1MNx=0M1y=0N1F(u,v)ej2π(ux/M+vy/N)
反變換就可以實現將頻域影象恢復到時域影象。對正變換分析,當u=v=0時,那麼:
F(0,0)=x=0M1y=0N1f(x,y)=f¯(x,y)
看看這個式子發現它是什麼?f(x,y)可是時域裡面影象的灰度。很明顯,這個東西其實是整個影象的灰度求平均了(這裡說的都是灰度影象),當影象進行傅立葉變換以後,你去把F(0,0)與原始影象平均灰度去比較看看是不是一樣的。那麼F(0,0)在頻域內稱為直流分量,其他的所有F稱為交流分量,直流分量可以看到是在0處獲得的,所以很明顯是存在於低頻分量下的。

說了這麼多,來上個實驗看看到底什麼是傅立葉變換吧。在python+opencv下想實現影象傅立葉變換有兩種途徑,一種採用numpy包可以實現,還有opencv自帶的可以實現,其中numpy帶的使用方便,直觀易懂。

import cv2
import numpy as np
import matplotlib.pyplot as plt

img = cv2.imread('flower.jpg',0) #直接讀為灰度影象
f = np.fft.fft2(img)
fshift = np.fft.fftshift(f)
#取絕對值:將複數變化成實數
#取對數的目的為了將資料變化到較小的範圍(比如0-255)
s1 = np.log(np.abs(f))
s2 = np.log(np.abs(fshift))
plt.subplot(121),plt.imshow(s1,'gray'),plt.title('original')
plt.subplot(122),plt.imshow(s2,'gray'),plt.title('center')

這裡寫圖片描述
注意的是,上圖其實並沒有什麼含義,顯示出來的可以看成是頻域後圖像的振幅資訊,並沒有相位資訊,影象的相位ϕ=atan(),numpy包中自帶一個angle函式可以直接根據複數的實部與虛部求出角度(默認出來的角度是弧度)。像上述程式出來的f與fshift都是複數,就可以直接angle函式一下,比如試試並把對應的相點陣圖像顯示出來:

import cv2
import numpy as np
import matplotlib.pyplot as plt

img = cv2.imread('flower.jpg',0) #直接讀為灰度影象
f = np.fft.fft2(img)
fshift = np.fft.fftshift(f)
#取絕對值:將複數變化成實數
#取對數的目的為了將資料變化到較小的範圍(比如0-255)
ph_f = np.angle(f)
ph_fshift = np.angle(fshift)

plt.subplot(121),plt.imshow(ph_f,'gray'),plt.title('original')
plt.subplot(122),plt.imshow(ph_fshift,'gray'),plt.title('center')

這裡寫圖片描述
這就是影象上每個畫素點對應的相點陣圖,其實是毫無規律的,理解就是偏移的角度。

Ok再來說說程式中為什麼要有一個np.fft.fftshift(f)中心化操作,整個影象是在傅立葉變換的一個週期內完成的,將其看成橫縱兩個方向的一維傅立葉變換,在每個方向上都會有高頻訊號和低頻訊號,那麼傅立葉變換將低頻訊號放在了邊緣,高頻訊號放在了中間,然而一副影象,很明顯的低頻訊號多而明顯,所以將低頻訊號採用一種方法移到中間,在時域上就是對f乘以(-1)^(M+N),換到頻域裡面就是位置的移到了。

影象變換到頻域後就可以進行操作了,目前接觸到的頻域操作似乎也就是一些濾波操作,如同空域裡面的濾波操作一樣,不過原理不一樣了,後面再說說一些頻域濾波方法。好了一旦操作完,得到的資料還是頻域資料,那麼如何將其變換到時域呢?這裡就是傅立葉反變換了,公式表示就如同前面那樣。這個頻域變換到時域的操作就是逆向傅立葉變換再走一遍(比如先反中心化,在逆變換)。一個例項如下:

import cv2
import numpy as np
import matplotlib.pyplot as plt

img = cv2.imread('flower.jpg',0) #直接讀為灰度影象
f = np.fft.fft2(img)
fshift = np.fft.fftshift(f)
#取絕對值:將複數變化成實數
#取對數的目的為了將資料變化到0-255
s1 = np.log(np.abs(fshift))
plt.subplot(131),plt.imshow(img,'gray'),plt.title('original')
plt.subplot(132),plt.imshow(s1,'gray'),plt.title('center')
# 逆變換
f1shift = np.fft.ifftshift(fshift)
img_back = np.fft.ifft2(f1shift)
#出來的是複數,無法顯示
img_back = np.abs(img_back)
plt.subplot(133),plt.imshow(img_back,'gray'),plt.title('img back')

這裡寫圖片描述
可以看到恢復的一模一樣。

我們說,恢復一個頻域影象需要影象的振幅以及相位,而一個複數也正好包含這些,振幅就是實部虛部的平方和開方,相位就是atan(),前面說過。那麼現在假設我們只用一副影象的振幅或者相位來將頻域內影象恢復到時域會怎麼樣呢?下面給出只用振幅、只用相位以及兩者在聯合起來來恢復的程式:

import cv2
import numpy as np
import matplotlib.pyplot as plt

img = cv2.imread('flower.jpg',0) #直接讀為灰度影象
f = np.fft.fft2(img)
fshift = np.fft.fftshift(f)
#取絕對值:將複數變化成實數
#取對數的目的為了將資料變化到0-255
s1 = np.log(np.abs(fshift))
plt.subplot(221),plt.imshow(img,'gray'),plt.title('original')
plt.xticks([]),plt.yticks([])
#---------------------------------------------
# 逆變換--取絕對值就是振幅
f1shift = np.fft.ifftshift(np.abs(fshift))
img_back = np.fft.ifft2(f1shift)
#出來的是複數,無法顯示
img_back = np.abs(img_back)
#調整大小範圍便於顯示
img_back = (img_back-np.amin(img_back))/(np.amax(img_back)-np.amin(img_back))
plt.subplot(222),plt.imshow(img_back,'gray'),plt.title('only Amplitude')
plt.xticks([]),plt.yticks([])
#---------------------------------------------
# 逆變換--取相位
f2shift = np.fft.ifftshift(np.angle(fshift))
img_back = np.fft.ifft2(f2shift)
#出來的是複數,無法顯示
img_back = np.abs(img_back)
#調整大小範圍便於顯示
img_back = (img_back-np.amin(img_back))/(np.amax(img_back)-np.amin(img_back))
plt.subplot(223),plt.imshow(img_back,'gray'),plt.title('only phase')
plt.xticks([]),plt.yticks([])
#---------------------------------------------
# 逆變換--將兩者合成看看
s1 = np.abs(fshift) #取振幅
s1_angle = np.angle(fshift) #取相位
s1_real = s1*np.cos(s1_angle) #取實部
s1_imag = s1*np.sin(s1_angle) #取虛部
s2 = np.zeros(img.shape,dtype=complex) 
s2.real = np.array(s1_real) #重新賦值給s2
s2.imag = np.array(s1_imag)

f2shift = np.fft.ifftshift(s2) #對新的進行逆變換
img_back = np.fft.ifft2(f2shift)
#出來的是複數,無法顯示
img_back = np.abs(img_back)
#調整大小範圍便於顯示
img_back = (img_back-np.amin(img_back))/(np.amax(img_back)-np.amin(img_back))
plt.subplot(224),plt.imshow(img_back,'gray'),plt.title('another way')
plt.xticks([]),plt.yticks([])

這裡寫圖片描述
可以看到,僅僅振幅的恢復圖啥也不是,僅僅的相點陣圖還有那麼點意思,當然也是啥也不是。最後是把振幅與相位分別作為頻域內複數的實部和虛部,得到的恢復圖才與原來的一樣。

基於此,我們來做一個有趣的實驗,假設有兩幅影象,將這兩幅影象進行傅立葉變換到頻域,然後把用一個影象的振幅做為振幅,用另一幅影象的相位作為相位生成一副新的影象,那麼,這個影象會怎麼樣呢?你覺得生成的影象會更像取振幅的那副還是取相位的那副呢?來看看吧:

import cv2
import numpy as np
import matplotlib.pyplot as plt

img_flower = cv2.imread('flower.jpg',0) #直接讀為灰度影象
img_man = cv2.imread('woman.jpg',0) #直接讀為灰度影象
plt.subplot(221),plt.imshow(img_flower,'gray'),plt.title('origial1')
plt.xticks([]),plt.yticks([])
plt.subplot(222),plt.imshow(img_man,'gray'),plt.title('origial_2')
plt.xticks([]),plt.yticks([])
#--------------------------------
f1 = np.fft.fft2(img_flower)
f1shift = np.fft.fftshift(f1)
f1_A = np.abs(f1shift) #取振幅
f1_P = np.angle(f1shift) #取相位
#--------------------------------
f2 = np.fft.fft2(img_man)
f2shift = np.fft.fftshift(f2)
f2_A = np.abs(f2shift) #取振幅
f2_P = np.angle(f2shift) #取相位
#---圖1的振幅--圖2的相位--------------------
img_new1_f = np.zeros(img_flower.shape,dtype=complex) 
img1_real = f1_A*np.cos(f2_P) #取實部
img1_imag = f1_A*np.sin(f2_P) #取虛部
img_new1_f.real = np.array(img1_real) 
img_new1_f.imag = np.array(img1_imag) 
f3shift = np.fft.ifftshift(img_new1_f) #對新的進行逆變換
img_new1 = np.fft.ifft2(f3shift)
#出來的是複數,無法顯示
img_new1 = np.abs(img_new1)
#調整大小範圍便於顯示
img_new1 = (img_new1-np.amin(img_new1))/(np.amax(img_new1)-np.amin(img_new1))
plt.subplot(223),plt.imshow(img_new1,'gray'),plt.title('another way')
plt.xticks([]),plt.yticks([])
#---圖2的振幅--圖1的相位--------------------
img_new2_f = np.zeros(img_flower.shape,dtype=complex) 
img2_real = f2_A*np.cos(f1_P) #取實部
img2_imag = f2_A*np.sin(f1_P) #取虛部
img_new2_f.real = np.array(img2_real) 
img_new2_f.imag = np.array(img2_imag) 
f4shift = np.fft.ifftshift(img_new2_f) #對新的進行逆變換
img_new2 = np.fft.ifft2(f4shift)
#出來的是複數,無法顯示
img_new2 = np.abs(img_new2)
#調整大小範圍便於顯示
img_new2 = (img_new2-np.amin(img_new2))/(np.amax(img_new2)-np.amin(img_new2))
plt.subplot(224),plt.imshow(img_new2,'gray'),plt.title('another way')
plt.xticks([]),plt.yticks([])

這裡寫圖片描述
這就是合成的影象,是不是很有趣,影象3是圖1的振幅加圖2的相位,影象4是圖1的相位加上圖2的振幅。很明顯的可以看到,新影象佔用誰的相位就越像誰,為什麼會這樣?很簡單,可以理解振幅不過描述影象灰度的亮度,佔用誰的振幅不過使得結果哪些部分偏亮或者暗而已,而影象是個什麼樣子是由它的相位決定的。相位描述的是一個方向,方向正確了,那麼最終的結果離你的目的就不遠了。可想而知,方向對於一件事物是多麼的重要,大自然的規律尚且如此,更別說做人做事了,找準並相信一個方向,慢慢的走下去吧,總有一天會看到成果的,哪怕你的振幅不對,走的慢一點,但是最終也能走的像模像樣的,不會說到了人生的最後,回頭一看,再來感嘆哎呀這是什麼玩意,那就很悲哀了。

好了,扯回來我們的問題,關於傅立葉變換下的影象恢復基本上就是這了。但是我們發現,似乎我們只說了開頭(怎麼變換)和結尾(怎麼變回去),那麼中間我們要做的東西才是傅立葉變換的目的—頻域下的各種變化操作。

這裡主要介紹頻域下的濾波–低通濾波器,高通濾波器,帶通帶阻濾波器。

我們知道,影象在變換加移動中心後,從中間到外面,頻率上依次是從低頻到高頻的,那麼我們如果把中間規定一小部分去掉,是不是相對於把低頻訊號去掉了呢?這也就是相當於進行了高通濾波。這個濾波模板畫出來可能就是這樣的:

這裡寫圖片描述
黑色為0,白色為1,把這個模板去和影象進過傅立葉變換的頻域矩陣去與(相乘)一下就實現了高通濾波。比如下面:

import cv2
import numpy as np
import matplotlib.pyplot as plt

img_man = cv2.imread('woman.jpg',0) #直接讀為灰度影象
plt.subplot(121),plt.imshow(img_man,'gray'),plt.title('origial')
plt.xticks([]),plt.yticks([])
#--------------------------------
rows,cols = img_man.shape
mask = np.ones(img_man.shape,np.uint8)
mask[rows/2-30:rows/2+30,cols/2-30:cols/2+30] = 0
#--------------------------------
f1 = np.fft.fft2(img_man)
f1shift = np.fft.fftshift(f1)
f1shift = f1shift*mask
f2shift = np.fft.ifftshift(f1shift) #對新的進行逆變換
img_new = np.fft.ifft2(f2shift)
#出來的是複數,無法顯示
img_new = np.abs(img_new)
#調整大小範圍便於顯示
img_new = (img_new-np.amin(img_new))/(np.amax(img_new)-np.amin(img_new))
plt.subplot(122),plt.imshow(img_new,'gray'),plt.title('Highpass')
plt.xticks([]),plt.yticks([])

這裡寫圖片描述
可以看到,高通濾波器有利於提取影象的輪廓,這裡我們從原理上分析一下為什麼,影象的輪廓或者邊緣或者一些噪聲處,灰度變化劇烈,那麼在把它們經過傅立葉變換後,就會變成高頻訊號(我們知道高頻時捕捉細節的),所以在把影象低頻訊號濾掉以後剩下的自然就是輪廓了。

反過來我們來看看空間域濾波中的拉普拉斯模板,我們知道這個模板是這樣的

M=0101