一 不同色彩空間的轉換
OpenCV中有數百種關於在不同色彩空間之間轉換的方法。當前,在計算機中有三種常用的色彩空間:灰度,BGR以及HSV(Hue,Saturation,Value)。
- 灰度色彩空間是通過去除色彩信息來將其轉換成灰階,灰度色彩空間對中間處理特別有效,比如人臉檢測。
- BGR,即藍-綠-紅色彩空間,每一個像素點都由一個三元數組來表示,分別代表藍、綠、紅三種顏色。網頁開發者可能熟悉另一個與之相似的色彩空間:RGB,他們只是在顏色順序上不同。
- HSV,H(Hue)是色調,S(Saturation)是飽和度,V(Value)表示黑暗的程度(或光譜另一端的命令程度)。
在第一次處理BGR色彩空間的時候,可以不要其中的一個色彩分量,比如像素值[0 255 255](沒有藍色,綠色分量取最大值,紅色分量取最大值)表示黃色。如果讀者有藝術背景,會發現綠色和紅色混合產生渾濁的褐色,這是因為計算所使用的顏色模型具有可加性並且處理的是光照,而繪畫不是這樣的(它遵從減色模型 subtractive color model)。計算機使用顯示器發光來做顏色的媒介,因此運行在計算機上的軟件所使用的色彩模型是加色模型。
二 傅里葉變換
在OpenCV中,對圖像和視頻的大多數處理或多或少都會涉及到傅里葉變換的概念。Joseph Fourier(約瑟夫.傅里葉)是以為18世紀的法國數學家,他發現並推廣了很多數學概念,主要研究熱學規律,在數學上,他認為一切都可以用波形來描述。具體而言,他觀察到所有的波形都是由一系列簡單且頻率不同的正弦曲線疊加得到。
也就是說人們看到的波形都是由其他波形疊加得到的。這個概念對操作圖像非常有幫助,因為這樣我們可以區分圖像哪些區域的信號變化特別強,哪些區域的信號變化不那么強,從而可以任意地標記噪聲區域,感興趣區域,前景和背景等。原始圖像由許多頻率組成,人們能夠分離這些頻率來處理圖像和提取感興趣的數據。
下面通過傅里葉變換來介紹圖像的幅度譜(magnitude specturm)。圖像的幅度譜是另一種圖像,幅度譜圖像呈現了原始圖像在變化方面的一種表示:把一張圖像中最明亮的像素放到圖像中央,然后逐漸變暗,在邊緣上像素最暗。這樣可以發現圖像中有多少亮的像素和暗的像素,以及它們分布的比例。
傅里葉變換的概念是許多常見的圖像處理操作的基礎,比如邊緣檢測或線段和形狀檢測。
下面介紹兩個概念:高通濾波器和低通濾波器。
1.高通濾波器
高通濾波器(HPF)是檢測圖像的某個區域,然后根據像素與周圍像素的亮度差值來提升該像素的亮度的濾波器。
以如下的核(kernel),即濾波器矩陣為例:
注:核是指一組權重的集合,它會應用在源圖像的一個區域,並由此生成目標圖像的一個像素。比如,大小為7的核意味着每49(7x7)個源圖像的像素會產生目標圖像的一個像素。
可把核看做一塊覆蓋在源圖像上可移動的毛玻璃片,玻璃片覆蓋區域的光線會按某種方式進行擴散混合后透過去。
在計算完中央像素與周圍鄰近像素的亮度差值之和以后,如果亮度變化很大,中央像素的亮度會增加,反之則不會。換句話說,如果一個像素比它周圍的像素更突出,就會提升它的亮度。
這在邊緣檢測上尤為有效,它采用一種稱為高頻提升濾波器(high boost filter)的高通濾波器。
高通和低通濾波器都有一個半徑(radius)的屬性,它決定了多大面積的臨近像素參與濾波運算。
下面是一個高通濾波器的例子,代碼如下:
# -*- coding: utf-8 -*- """ Created on Fri Apr 20 21:10:35 2018 @author: Administrator """ ''' OPenCV3 計算機視覺 筆記 第三章 :使用Open CV3處理圖像 ''' import cv2 import numpy as np from scipy import ndimage ''' 1.傅里葉變換 ''' ''' (1)高通濾波器 ''' kernel_3x3 = np.array([[-1,-1,-1],[-1,8,-1],[-1,-1,-1]]) kernel_5x5 = np.array([[-1,-1,-1,-1,-1], [-1,-1, 2, 1,-1], [-1, 2, 4, 2,-2], [-1, 1, 2, 2,-1], [-1,-1,-1,-1,-1]]) #讀取圖像,指定格式為灰度圖像 img = cv2.imread('./image/img6.jpg',cv2.IMREAD_GRAYSCALE) #進行卷積運算 k3 = ndimage.convolve(img,kernel_3x3) k5 = ndimage.convolve(img,kernel_5x5) #模糊濾波 blurred = cv2.GaussianBlur(img,(11,11),0) #作差 g_hpf = img - blurred #顯示圖像 cv2.imshow('original',img) cv2.imshow('3x3',k3) cv2.imshow('5x5',k5) cv2.imshow('g_hpf',g_hpf) cv2.waitKey() cv2.destroyAllWindows()
運行后顯示如下:
導入 模塊后,我們定義了一個3x3和一個5x5的核,然后將讀入的圖像轉換成灰度格式。通常大多數圖像處理都會用Numpy模塊來完成,但是這里的情況比較特殊,因為需要用一個給定核與圖像進行'卷積',但是Numpy碰巧只接受一維數組。
上面代碼用了兩個自定義卷積核來實現兩個高通濾波器。最后又用一種不同的方法來實現高通濾波器:通過對圖像應用低通濾波器之后,與原始圖像計算差值。這樣得到的效果會更好。
這里注意有一點需要注意:使用卷積進行運算,並不能保證的每個像素值都在0~255之間。對於在區間外的像素點會導致灰度圖無法顯示,所以還需要做歸一化,然后每個值乘以255,再將所有的值映射到這個區間內。歸一化算法:x=(x-Min)(Max-Min),這樣x的范圍就在[0,1]之間了。我們在上面調用的函數ndimage.convolve()在內部已經做了這些處理,所以我們就不需要自己寫歸一化的處理過程了。
2 低通濾波器
高通濾波器是根據像素與鄰近像素的亮度差值來提升該像素的亮度。低通濾波器(LPF)則是在像素與周圍像素的亮度差值小於一定特征值,平滑該像素的亮度。它主要用於去噪和模糊化,比如說,高斯模糊是最常用的模糊濾波器(平滑濾波器)之一,它是削弱高頻信號強度的低通濾波器。
三 創建模塊
和CptureManager類和WindowManager類一樣,濾波器需要在Cameo外也能被重用。所以需要把濾波器分割到各自的python模塊或者python文件中。
在Cameo.py的同一目錄下創建一個filters.py文件,在該文件下添加一些濾波函數和類,fliters.py文件中需要導入如下模塊:
import cv2 import numpy as np import utils
在同一目錄下還要創建一個名為utils.py的文件,該文件存放一些通用的數學函數,同時需要導入以下模塊:
import cv2 import numpy as np import scipy.interpolate
四 邊緣檢測
邊緣在人類視覺和計算機視覺中均起着重要的作用。人類能夠僅憑一張背景剪影或一個草圖就能識別出物體的類型和姿態。
Open CV提供了許多邊緣檢測濾波函數,包括以下:
Laplacian() #作為邊緣檢測函數,他會產生明顯的邊緣線條,灰度圖像更是如此。
Sobel()
Scharr()
這些濾波函數都會將非邊緣區域轉換為黑色,邊緣區域轉換成白色或其他飽和的顏色。但是這些函數都容易將噪聲錯誤的識別為邊緣。緩解這個問題的方法就是在找到邊緣之前對圖像進行模糊處理,去除噪聲。
Open CV也提供了需要模糊濾波函數,包括以下:
blur()
medianBlur() #它對去除數字化的視頻噪聲特別有效,特別是去除彩色圖像的噪聲
GaussianBlur()
邊緣檢測和模糊濾波的函數的參數有很多,但總會有一個ksize參數,它是一個奇數,表示濾波核的寬和高(以像素為單位)。
cv2.blur(src,ksize[,dst[,anchor[,borderType]]])函數,均值濾波。
均值濾波是一種典型的線性濾波算法,主要是利用像素點鄰域的像素值來計算像素點的值。其具體方法是首先給出一個濾波kernel,該核將覆蓋像素點周圍的其他鄰域像素點,去掉像素本身,將其鄰域像素點相加然后取平均值即為該像素點的新的像素值,這就是均值濾波的本質。
- src: 輸入圖像,圖像深度是cv2.CV_8U、cv2.CV_16U、cv2.CV_16S、cv2.CV_32F以及cv2.CV_64F其中的某一個。
- dst: 輸出圖像,深度和類型與輸入圖像一致。
- ksize: 濾波kernel的尺寸,元組類型。
- anchor: 字面意思是錨點,也就是處理的像素位於kernel的什么位置,默認值為(-1, -1)即位於kernel中心點,如果沒有特殊需要則不需要更改。
- borderType 用於推斷圖像外部像素的某種邊界模式,有默認值cv2.BORDER_DEFAULT。
cv2.medianBlur(src,ksize[,dst])中值濾波函數。
中值濾波是一種典型的非線性濾波,是基於排序統計理論的一種能夠有效抑制噪聲的非線性信號處理技術,基本思想是用像素點鄰域灰度值的中值來代替該像素點的灰度值,讓周圍的像素值接近真實的值從而消除孤立的噪聲點。該方法在取出脈沖噪聲、椒鹽噪聲的同時能保留圖像的邊緣細節。這些優良特性是線性濾波所不具備的。
中值濾波首先也得生成一個濾波核,將該核內的各像素值進行排序,生成單調上升或單調下降的二維數據序列,二維中值濾波輸出為g(x, y)=medf{f(x-k, y-1),(k, l∈w)},其中f(x,y)和g(x,y)分別是原圖像和處理后圖像, w為輸入的二維模板,能夠在整幅圖像上滑動,通常尺寸為3*3或5*5區域,也可以是不同的形狀如線狀、圓形、十字形、圓環形等。通過從圖像中的二維模板取出奇數個數據進行排序,用排序后的中值取代要處理的數據即可。
中值濾波對消除椒鹽噪聲非常有效,能夠克服線性濾波器帶來的圖像細節模糊等弊端,能夠有效保護圖像邊緣信息,是非常經典的平滑噪聲處理方法。在光學測量條紋圖像的相位分析處理方法中有特殊作用,但在條紋中心分析方法中作用不大。
- src: 輸入圖像,圖像為1、3、4通道的圖像,當核尺寸為3或5時,圖像深度只能為cv2.CV_8U、cv2.CV_16U、cv2.CV_32F中的一個,如而對於較大孔徑尺寸的圖片,圖像深度只能是cv2.CV_8U。
- dst: 輸出圖像,尺寸和類型與輸入圖像一致。
- ksize: 濾波核的尺寸大小,必須是大於1的奇數,如3、5、7……
cv2.GaussianBlur(src,ksize,sigmaX[,sigmaxY[,borderType]]])高斯濾波函數。
高斯濾波是一種線性平滑濾波,對於除去高斯噪聲有很好的效果。在其官方文檔中形容高斯濾波為”Probably the most useful filter”,同時也指出高斯濾波並不是效率最高的濾波算法。高斯算法在官方文檔給出的解釋是高斯濾波是通過對輸入數組的每個點與輸入的高斯濾波核執行卷積計算然后將這些結果一塊組成了濾波后的輸出數組,通俗的講就是高斯濾波是對整幅圖像進行加權平均的過程,每一個像素點的值都由其本身和鄰域內的其他像素值經過加權平均后得到。高斯濾波的具體操作是:用一個核(或稱卷積、掩模)掃描圖像中的每一個像素,用模板確定的鄰域內像素的加權平均灰度值去替代模板中心像素點的值。
在圖像處理中高斯濾波一般有兩種實現方式:一種是用離散化窗口滑窗卷積,另一種是通過傅里葉變換。最常見的就是第一種滑窗實現,只有當離散化的窗口非常大,用滑窗計算量非常大的情況下會考慮基於傅里葉變換的方法。
我們在參考其他文章的時候可能會出現高斯模糊和高斯濾波兩種說法,其實這兩種說法是有一定區別的。我們知道濾波器分為高通、低通、帶通等類型,高斯濾波和高斯模糊就是依據濾波器是低通濾波器還是高通濾波器來區分的。比如低通濾波器,像素能量低的通過,而對於像素能量高的部分將會采取加權平均的方法重新計算像素的值,將能量像素的值編程能量較低的值,我們知道對於圖像而言其高頻部分展現圖像細節,所以經過低通濾波器之后整幅圖像變成低頻造成圖像模糊,這就被稱為高斯模糊;相反高通濾波是允許高頻通過而過濾掉低頻,這樣將低頻像素進行銳化操作,圖像變的更加清晰,被稱為高斯濾波。說白了很簡單就是:高斯濾波是指用高斯函數作為濾波函數的濾波操作而高斯模糊是用高斯低通濾波器。
高斯濾波在圖像處理中常用來對圖像進行預處理操作,雖然耗時但是數字圖像用於后期應用但是其噪聲是最大的問題,噪聲會造成很大的誤差而誤差在不同的處理操作中會累積傳遞,為了能夠得到較好的圖像,對圖像進行預處理去除噪聲也是針對數字圖像處理的無奈之舉。
高斯濾波器是一類根據高斯函數的形狀來選擇權值的線性平滑濾波器,高斯濾波器對於服從正太分布的噪聲非常有效,一維高斯函數如下:
二維高斯函數如下:
- src: 輸入圖像,圖像深度為cv2.CV_8U、cv2.CV_16U、cv2.CV_16S、cv2.CV_32F、cv2.CV_64F。
- dst: 輸出圖像,與輸入圖像有相同的類型和尺寸。
- ksize: 高斯內核大小,元組類型
- sigmaX: 高斯核函數在X方向上的標准偏差
- sigmaY: 高斯核函數在Y方向上的標准偏差,如果sigmaY是0,則函數會自動將sigmaY的值設置為與sigmaX相同的值,如果sigmaX和sigmaY都是0,這兩個值將由ksize[0]和ksize[1]計算而來。具體可以參考getGaussianKernel()函數查看具體細節。建議將size、sigmaX和sigmaY都指定出來。
- borderType: 推斷圖像外部像素的某種便捷模式,有默認值cv2.BORDER_DEFAULT,如果沒有特殊需要不用更改,具體可以參考borderInterpolate()函數。
cv2. bilateralFilter()雙邊濾波函數。
這里使用medianBlur()作為模糊函數,使用Laplacian()作為邊緣檢測函數。在使用medianBlur()之后,需要將圖像從BGR色彩空間轉換為灰度色彩空間。在得到Laplacian()函數結果之后,需要將圖像轉換為黑色邊緣和白色背景(之前是白色邊緣黑色背景)。然后將其歸一化,並乘以源圖像以便能將邊緣變黑。在filters.py文件中實現這個函數:
def strokeEdges(src,blurKsize=7,edgeKsize=5): ''' 該函數實現性能更好的邊緣檢測 這里使用medianBlur()作為模糊函數,使用Laplacian()作為邊緣檢測函數。在使用medianBlur()之后, 需要將圖像從BGR色彩空間轉換為灰度色彩空間。在得到Laplacian()函數結果之后,需要將圖像轉換為 黑色邊緣和白色背景(之前是白色邊緣黑色背景)。然后將其歸一化,並乘以源圖像以便能將邊緣變黑。 args: src:源圖像數據 BGR色彩空間 blurKsize:模糊濾波卷積核的寬和高 小於3,不進行模糊處理 edgeKsize:邊緣檢測卷積核的寬和高
return:
dst:目標圖像數據 灰度色彩空間 ''' if blurKsize >= 3: #先模糊處理 blurredSrc = cv2.medianBlur(src,blurKsize) cv2.imshow('blurredSrc',blurredSrc) #BGR格式轉化為灰度格式 graySrc = cv2.cvtColor(blurredSrc,cv2.COLOR_BGR2GRAY) else: graySrc = cv2.cvtColor(src,cv2.COLOR_BGR2GRAY) cv2.imshow('graySrc',graySrc) #邊緣檢測 對灰度圖像檢測效果更好 cv2.Laplacian(graySrc,cv2.CV_8U,graySrc,ksize = edgeKsize) cv2.imshow('laplacian',graySrc) #顏色反向處理 並歸一化 normalizedInverseAlpha = (1.0/255)*(255 - graySrc) cv2.imshow('normalizedInverseAlpha',normalizedInverseAlpha) #通道分離 B,G,R 單通道圖像 channels = cv2.split(src) cv2.imshow('B',channels[0]) #計算后的結果分別與每個通道相乘 for channel in channels: #這里是點乘,即對應元素相乘 channel[:] = channel * normalizedInverseAlpha cv2.imshow('B1',channels[0]) #通道合並(只能合並多個單通道成為多通道) return cv2.merge(channels) img = cv2.imread('./image/img6.jpg',cv2.IMREAD_COLOR) dst = strokeEdges(img) cv2.imshow('dst',dst) cv2.waitKey() cv2.destroyAllWindows()
運行結果如下:最后一張是我們輸出的目標,我們從圖片上可以看到圖片上還是有很多噪聲被識別成邊緣了。
五 用定制內核做卷積
Open CV預定的許多濾波器(濾波函數)都會使用核。其實核是一組權重,它決定了如何通過臨近像素點來計算新的像素點。核也稱作卷積矩陣,它對一個區域的像素做調和或卷積運算。通常基於核的濾波器被稱為卷積濾波器。
Open CV提供了一個通用分人filter2D()函數,它運用由用戶指定的任意核或卷積矩陣。
卷積矩陣是一個二維數組,有奇數行,奇數列,中心的元素對應於感興趣的像素,其它的元素對應於這個像素周圍的鄰近像素,每個像素都有一個整數或浮點數的值,這些值就是應用在像素上的權重。如下:
kernel = np.array([[-1,-1,-1], [-1, 9,-1], [-1,-1,-1]])
上面感興趣的像素權重為9,其余鄰近像素上的權重為-1。對於感興趣的像素來說,新像素值使用當前像素值乘以9,然后減去8個鄰近像素值。如果感興趣的像素和鄰近像素有一點差別,那么這個差別會增加。這樣會使圖片銳化,因為該像素與鄰近像素值之間的差距拉大了。
接下來的例子在源圖像和目標圖像上分別使用卷積核:
cv2.filter2D(src,ddepth,kernel[,dst[,anchor[,delta[,borderType]]]])函圖卷積運算函數。
該函數使用於任意線性濾波器的圖像,支持就地操作。當其中心移動到圖像外,函數可以根據指定的邊界模式進行插值運算。
- src: 輸入圖像。
- ddepth: 目標圖像深度,如果沒寫將生成與原圖像深度相同的圖像。當ddepth輸入值為-1時,目標圖像和原圖像深度保持一致。輸入和輸出對應關系:
- kernel: 卷積核(或者是相關核),一個單通道浮點型矩陣。如果想在圖像不同的通道使用不同的kernel,可以先使用split()函數將圖像通道事先分開。
- dst: 輸出圖像,和輸入圖像具有相同的尺寸和通道數量
- anchor: 內核的基准點(anchor),其默認值為(-1,-1)說明位於kernel的中心位置。基准點即kernel中與進行處理的像素點重合的點。
- delta: 在儲存目標圖像前可選的添加到像素的值,默認值為0
- borderType: 像素向外逼近的方法,默認值是cv2.BORDER_DEFAULT,即對全部邊界進行計算。
接着我們向filter.py文件中添加幾個類,第一個類為VconvolutionFilter,它表示一般的卷積濾波器;第二個子類是SharpenFilter,它表示特定的銳化濾波器。第三個子類是FindEdgesFilter,是一個特定的邊緣檢測器。第四個子類是BlurFilter,是一個模糊濾波器。最后一個EmbossFilter子類。
class VConvolutionFilter(object): ''' 卷積濾波器 ''' def __init__(self,kernel): ''' args: kernel:卷積核 ''' self.__kernel = kernel def apply(self,src,dst): ''' 對源BGR或者弧度色彩圖像src應用濾波器 ''' cv2.filter2D(src,-1,self.__kernel,dst) class SharpenFilter(VConvolutionFilter): ''' 銳化濾波器:銳化圖像,使得感興趣的像素與鄰近像素的差距拉大了 ''' def __init__(self): #注意這里權重加起來為1,如果修改卷積核,使得權重加起來為0,這樣得到就是一個邊緣檢測器,這會把 #邊緣轉換為白色,非邊緣轉換為黑色。這樣就改變了圖像的亮度了。 kernel = np.array([[-1,-1,-1], [-1, 9,-1], [-1,-1,-1]]) VConvolutionFilter.__init__(self,kernel) class FindEdgesFilter(VConvolutionFilter): ''' 邊緣濾波器 ''' def __init__(self): #注意這里權重加起來為0,這樣得到就是一個邊緣檢測器,這會把 #邊緣轉換為白色,非邊緣轉換為黑色。 這里采用基於二階微分的圖像增強拉普拉斯算子 kernel = np.array([[-1,-1,-1], [-1, 8,-1], [-1,-1,-1]]) VConvolutionFilter.__init__(self,kernel) class BlurFilter(VConvolutionFilter): ''' 模糊濾波器:實現一個平均濾波器 ''' def __init__(self): ''' 權重和為1,鄰近像素的權重全為正。 ''' kernel = np.array([[0.04,0.04,0.04,0.04,0.04], [0.04,0.04,0.04,0.04,0.04], [0.04,0.04,0.04,0.04,0.04], [0.04,0.04,0.04,0.04,0.04], [0.04,0.04,0.04,0.04,0.04]]) VConvolutionFilter.__init__(self,kernel) class EmbossFilter(VConvolutionFilter): ''' 模糊(有正的權重)和銳化(有負的權重) 產生一個脊狀或者浮雕的效果。 ''' def __init__(self): kernel = np.array([[-2,-1, 0], [-1, 1, 1], [ 0, 1, 2]]) VConvolutionFilter.__init__(self,kernel)
修改應用,在上一節Cameo項目捕獲的數據幀上,對圖像進行處理,修改如下兩處,代碼如下;
class Cameo(object): def __init__(self): #創建窗口管理器和視頻捕獲管理器 self.__window_manager = WindowManager('Cameo',self.on_key_press) self.__capture_manager = CaptureManager(cv2.VideoCapture(0),self.__window_manager,True) self.__filter = filters.SharpenFilter() def run(self): ''' 運行主循環 ''' #創建窗口 self.__window_manager.create_window() #循環捕獲每一幀圖像,並顯示 直至窗口銷毀 while self.__window_manager.is_window_created: #獲取一幀圖像 並在指定的窗口顯示 self.__capture_manager.enter_frame() frame = self.__capture_manager.frame ''' 這里可以對這一幀圖像進行處理 ''' filters.strokeEdges(frame,blurKsize=5) self.__filter.apply(frame,frame) self.__capture_manager.exit_frame() #執行鍵盤回調函數 self.__window_manager.process_event() #銷毀對象 self.__capture_manager.release()
六 Canny邊緣檢測
Open CV還提供了一個非常方便的Canny()函數,該算法非常流行,不僅是因為它的效果,還因為在Open CV程序中實現時非常簡單:
''' Canny邊緣檢測 ''' can = cv2.Canny(img,200,300) cv2.imshow('candy',can) cv2.waitKey() cv2.destroyAllWindows()
Candy邊緣檢測算法算法非常復雜,但是也很有趣,它有5個步驟,即使用高斯濾波器對圖像進行去噪,計算梯度,在邊緣上使用非最大抑制,在檢測到的邊緣上使用閾值去除假陽性,最后還會分析所有的邊緣及其之間的連接,以保留真正的邊緣並消除不明顯的邊緣。
cv2.Candy函數部分參數如下:
- 第一個參數是需要處理的原圖像,該圖像必須是單通道的灰度圖;
- 第二個參數是閾值1。
- 第三個參數是閾值2.
其中較大的閾值2用於檢測圖像中明顯的邊緣,但是一般情況下檢測的效果不會那么完美,邊緣檢測出來是斷斷續續的。所以這時候較小的第一個閾值用於將這些斷續的邊緣連接起來。
參考文章:
[2]圖像處理基礎(2):自適應中值濾波器(基於OpenCV實現)
[7]邊緣檢測