vImage學習筆記——卷積(Convolution)
卷積(Convolution)是一個常用的圖像處理技術,可以改變像素強度,從而影響周圍其他像素的強度。卷積的常用技術是創建濾鏡,使用卷積技術,你可以獲取一些流行的圖像效果,比如模糊(blur)、銳化(sharpen)及邊緣檢測(edge detection),這些效果在Photo Booth、iPhoto和Aperture都有廣泛使用。
如果你對圖像濾鏡和實時處理有興趣的話,你會發現vImage函數集的好處。用圖像濾鏡,卷積操作可以完成一些常用的濾鏡效果,比如浮雕、模糊及色調分離。
vImage卷積技術對銳化或增強圖像質量也很有用。當處理一些科學圖像時,增強圖像質量很有用。此外,由于科學圖像通常都很大,就很有必要使用這些vImage技術來達到合適的性能需求。這種情況下你需要用到的技術有邊緣檢測(edge detection)、銳化、描繪外觀輪廓(surface contour outlining)、平滑、及動作檢測(motion detection)。
本章節講述了卷積技術,以及如何使用vImage提供的卷積函數。通過本文,你可以:
- 了解卷積技術可以實現哪些效果;
- 學習什么是卷積核以及如何構建卷積核;
- 通過代碼示例,學習如何對一個圖像使用卷積技術。
卷積核(Convolution Kernels)
圖1展示了一個圖像通過vImage卷積函數添加了浮雕效果前后的對比圖。為了達到這個效果,vImage使用一個類網格的數學概念,稱為核(kernel),來完成卷積操作。
圖1 浮雕

圖2是一個3×3的kernel。kernel的高度和寬度不必一樣,但必須是奇數。kernel內部的數值會影響卷積的整體效果。這些數值決定了初始圖像像素會如何轉換成目標圖像像素,這看起來可能不是很直觀,9個數字會如何影響到濾鏡效果呢?卷積技術經過一系列的操作,根據周圍像素的強度改變當前像素的強度。vImage根據kernel執行卷積操作,這種通過kernel執行卷積計算的過程就稱為kernel convolution(核卷積)。
圖2 3×3卷積

卷積是像素單位的操作,即對每個像素都要執行同樣的算法。因此,大圖像比小圖像需要更多的卷積操作。一個kernel可以被看做一個二維的網格數據,而圖像也可以被看做一個二維網格數據(如圖3),對一個圖像應用kernel可以想象成把一個小格子(kernel)平鋪在大格子(圖像)上。
圖3 圖像是二維網格數據

kernel內部的數值會作為與它下面的數值相乘的乘數,下面的數值指的是被kernel數值覆蓋的像素的強度。在進行卷積計算時,把kernel的中心值覆蓋在待轉換的像素上,然后將kernel的每個值與其正下方的像素值相乘,最后將所有的結果相加,相加后的結果就是新的像素強度。圖4展示了kernel是如何轉化像素的。
圖4 核卷積

雖然kernel會覆蓋到一些其它的像素,但是最終只有kernel中心值正下方的原始像素會發生變化。kernel和圖像之間所有乘積相加后的和被稱為加權和。為了確保處理后的圖像對比原圖不會過于飽和,vImage有一個常用的方法,就是設置一個除數因子,把加權和進行拆分。因為用周圍像素的加權和來代替原始像素時常導致像素強度過大(并且圖像整體也過于明亮),拆分加權和可以按比例降低濾鏡效果的強度,并確保維持原始亮度,這個過程稱為標準化。這個行為是可選的,被拆分后的加權和會代替原始像素值。kernel對每個像素重復這個過程。
注:如果要執行標準化,你必須向卷積函數提供你要使用的因子。因子最好是2的冪次方。你也可以在圖像像素值為整數的時候再提供因子。浮點型不需要使用,因為你可以直接依比例決定kernel的浮點型數值來達到標準化。
kernel的數據類型和圖像的數據類型必須保持一致,比如,如果圖像像素數據類型是浮點型,那么kernel中的數據類型也必須是浮點型。
記住以上所述的算法vImage都已經幫你做好了,你不需要牢記卷積算法的步驟。當然,你也可以在自定義的核中實現該算法。
反卷積
反卷積指的是解除先前的卷積效果——一般是原始圖像中物理攜帶的卷積效果,比如鏡頭中的衍射效果。通常,反卷積是一個銳化操作。
反卷積的算法比較多,vImage用的是Richardson-Lucy deconvolution算法。
Richardson-Lucy deconvolution算法的目標是根據卷積后的像素值找到原始的像素值,以及kernel數據。
基于以上需求,在使用反卷積函數時必須提供卷積后的圖像及卷積使用的kernel值。
vImage會自行處理反卷積的每一步操作,因此不需要牢記這些步驟。使用反卷積的時候,必須提供初始的卷積kernel(如果該kernel不對稱的話,還要額外提供一個對角線翻轉的kernel2)。
使用卷積核
現在你最好了解一下核的結構以及卷積的處理過程,是時候使用幾個vImage函數來看看了。本章節展示了如何實現圖1中的浮雕效果,并解釋了無偏差卷積和帶偏差卷積之間的差別。
卷積
vImage可以自動進行卷積計算,而你的工作是提供kernel,即描述卷積應該生成什么效果。表1展示了如何使用卷積去生成浮雕效果。你也可以通過合適的kernel,利用同樣的代碼來生成一個不同的效果,比如銳化。
表1 生成浮雕效果
int myEmboss(void *inData,
unsigned int inRowBytes,
void *outData,
unsigned int outRowBytes,
unsigned int height,
unsigned int width,
void *kernel,
unsigned int kernel_height,
unsigned int kernel_width,
int divisor ,
vImage_Flags flags )
{
uint_8 kernel = {-2, -2, 0, -2, 6, 0, 0, 0, 0}; // 1
vImage_Buffer src = { inData, height, width, inRowBytes }; // 2
vImage_Buffer dest = { outData, height, width, outRowBytes }; // 3
unsigned char bgColor[4] = { 0, 0, 0, 0 }; // 4
vImage_Error err; // 5
err = vImageConvolve_ARGB8888( &src, //const vImage_Buffer *src
&dest, //const vImage_Buffer *dest,
NULL,
0, //unsigned int srcOffsetToROI_X,
0, //unsigned int srcOffsetToROI_Y,
kernel, //const signed int *kernel,
kernel_height, //unsigned int
kernel_width, //unsigned int
divisor, //int
bgColor,
flags | kvImageBackgroundColorFill
//vImage_Flags flags
);
return err;
}
上述代碼都做了哪些工作呢?
- 聲明一個浮雕kernel,即int型數組。kernel的數據類型要與相應的vImage函數所需數據類型相匹配。示例中使用了vImageConvolve_ARGB8888函數,因此kernel的數據類型應該是uint_8(無符號,8-bit,整數)。kernel的數組元素從左至右,依次是第一行、第二行、第三行;
- 聲明一個vImage_Buffer變量,用來存儲原始圖像信息。圖像數據以數組的形式進行存儲,元素圖像二進制數據(inData),另外還存儲高度、寬度和每行的字節數。這樣vImage會知道要處理的圖像大小,并如何合適地處理它。
- 聲明一個vImage_Buffer變量,用來存儲目標圖像信息。
- 聲明一個Pixel8888-格式的像素來表示目標圖像的背景色(示例中用的是黑色)。
- 聲明一個vImage_Err變量來存儲卷積函數的返回值。
然后,將這些聲明的變量值傳入vImageConvolve_ARGB8888函數中,由vImage來處理后續的計算,并把結果存儲到dest變量中。vImageConvolve_ARGB8888函數是vImage中僅有的幾個卷積函數之一。通常,vImage會為每種圖像格式提供4種函數變體,ARGB8888前綴表示該函數處理的是交叉型圖像(full-color),每個像素由四個8字節的整數構成一組,代表alpha(A),red(R),green(G)和blue(B)四個通道。想了解vImage支持的圖像格式的更多細節,請看vImage概述
示例myEmboss中還使用了vImage_Flags參數。該參數由1個或多個flags(用或邏輯運算符 | 連接)組成。kvImageBackgroundColorFill表示vImage要使用預先提供的背景色。
為了熟悉kernel效果的使用方法,請在你自己的代碼中使用以下兩個kernel。圖6的kernel可以生成高斯模糊效果,圖7的kernel可以生成邊緣檢測效果。
圖6 高斯模糊

圖7 邊緣檢測

帶偏差的卷積
在執行卷積操作時,可以選擇是否帶偏差。偏差是指在卷積結果上再額外添加一個來自周圍像素的影響。由于某些卷積計算得到的結果可能為負值,偏差可以避免信號溢出。可以把偏差設為127或128,來允許負值也被描繪出來。偏差可能使整體圖像效果變亮或變暗。
每個標準的vImage函數(比如vImageConvolve_PlanarF)都有一個對應的帶偏差的函數(vImageConvolveWithBias_PlanarF)。偏差函數的使用方法和無偏差函數一樣,除了必須設置bias參數來使用偏差。bias的數據類型必須與圖像像素數據類型一致。
圖8 帶偏差與無偏差

使用高速濾鏡
vImage提供了一些特定的卷積函數,這些比一般的卷積函數具有更快的處理速度。OS X v10.4以上的系統,對于Planar_8和ARGB8888數據類型可以使用box濾鏡和tent濾鏡。這些濾鏡可以得到模糊效果,函數是根據他們在笛卡爾坐標系的形狀命名的。調用這些函數不需要提供kernel,效果等同于需要提供kernel的一般的卷積函數。但是這些函數比一般的函數性能上要快大約1個量級。
注:由于這些函數需要一個穩健精確的算法,vImage規定這些函數不支持浮點型。浮點型的計算誤差會導致圖像高密度區域附近的低密度區域顯得人工化或粗糙化。
box濾鏡用周圍像素的未加權的平均數來代替被處理的像素值,相當于通過所有值都為1的kernel來進行卷積處理。對應的函數是vImageBoxConvolve_Planar8和vImageBoxConvolve_ARGB8888。每個轉換后的像素都是其周圍像素的平均值(周圍像素的寬、高即kernel的寬、高)。
圖9 box濾鏡

tent濾鏡用周圍像素的加權平均值來代替被處理的像素值。對應的函數是vImageTentConvolve_Planar8和vImageTentConvolve_ARGB8888。tent濾鏡的模糊操作相當于使用值不為1的kernel進行的卷積操作。和vImageBoxConvolve_Planar8和vImageBoxConvolve_ARGB8888一樣,不需要向函數提供kernel值,只需要寬高即可。
圖10 tent濾鏡

假設kernel的大小是3×5。那么第一個矩陣是

第二個矩陣是

那么生成的kernel是

3×5的tent濾鏡操作相當于使用上圖中的濾鏡來進行卷積操作。
使用多核
vImage允許在單個卷積操作中使用多個kernel??梢允褂?em>vImageConvolveMultiKernel函數,分別定義四個kernel,每個kernel對應一個圖像通道。一個kernel控制一個通道的話,你就可以對圖像進行更高級別的處理。例如,你可以利用多核卷積對圖像的顏色通道分別重新采樣,抵消屏幕上的RGB熒光效果。由于四個kernel可以分別對單個顏色通道進行處理,vImageConvolveMultiKernel函數只能應用于交叉型圖像。
這些函數的使用方法與單核卷積函數使用方法相同,唯一不同的是,需要提供一個指針數組,數組中每個元素指向一個kernel地址。
反卷積
與卷積計算相同的是,vImage同樣在內部封裝了反卷積的計算過程,你只需要提供一個kernel即可。表2展示了如何把浮雕效果通過反卷積消除的過程。你可以用同樣的代碼、合適的kernel去反卷積各種效果(比如模糊效果)。但是不同于卷積操作的是,反卷積函數還需要另一個kernel參數。除非kernel的寬和高相同,否則這個參數不能為NULL。如果kernel的寬和高不相等,必須再提供一個行列反轉的kernel。
表2是一段示例代碼,描述了如何使用vImage對一個ARGB8888-格式的圖像進行反卷積。
int myDeconvolve(void *inData,
unsigned int inRowBytes,
void *outData,
unsigned int outRowBytes,
unsigned int height,
unsigned int width,
void *kernel,
unsigned int kernel_height,
unsigned int kernel_width,
int divisor,
int iterationCount,
vImage_Flags flags )
{
//Prepare data structures
uint_8 kernel = {-2, -2, 0, -2, 6, 0, 0, 0, 0}; // 1
vImage_Error err; // 2
unsigned char bgColor[4] = { 0, 0, 0, 0 }; // 3
vImage_Buffer src = { inData, height, width, inRowBytes }; // 4
vImage_Buffer dest = { outData, height, width, outRowBytes }; // 5
//Send data to vImage for processing
err = vImageRichardsonLucyDeConvolve_ARGB8888( &src, // 6
&dest, //const vImage_Buffer *dest,
NULL,
0, //unsigned int srcOffsetToROI_X,
0, //unsigned int srcOffsetToROI_Y,
kernel, //const signed int *kernel,
NULL, //assumes symmetric kernel
kernel_height, //unsigned int kernel_height,
kernel_width, //unsigned int kernel_width,
0, //height of second kernel
0, //width of second kernel
divisor, //int
0, //for second kernel
bgColor,
iterationCount, //uint32_t
kvImageBackgroundColorFill | flags
//vImage_Flags
);
//Report result
return err; // 7
}
這段代碼都做了什么?
- 定義vImage要進行反卷積處理的初始卷積圖像的kernel值。示例使用的是對稱的kernel(寬高都為3),因此不用再定義第二個kernel;
- 聲明一個vImage_Error結構體,來存儲反卷積結果;
- 聲明一個Pixel8888-類型的像素,用于表示轉換后圖像的背景色;
- 聲明一個vImage_Buffer結構體,用于存儲初始圖像信息。圖像數據是作為一個字節型數組存儲的,包括圖像數據inData、寬、高、行字節等信息。這些信息可以讓vImage知道它要處理的圖像有多大,從而更好的執行操作;
- 聲明一個vImage_Buffer結構體,用于存儲目標圖像信息;
- 把上述聲明的變量傳給vImage函數。注意示例中在調用vImageRichardsonDeConvolve_ARGB8888函數時,第二個kernel為NULL,這是因為第一個kernel是對稱的,沒有必要再進行翻轉。
反卷積是一個迭代過程。你可以設置迭代次數,次數不同,得到的結果會不一樣,因此,為了得到最理想的結果,可以多試幾次。