卷積的介紹
卷積(convolution)是泛函分析里的一個(gè)概念,不過泛函分析一般都是數(shù)學(xué)系才學(xué)的,計(jì)算機(jī)系的學(xué)生大多在概率統(tǒng)計(jì)課本里了解到。它分為兩種形式,一個(gè)是離散形式,一個(gè)是連續(xù)(積分)形式。在圖像處理中我們更關(guān)心離散卷積,不過也先看看積分形式的卷積。現(xiàn)在假設(shè)我們有兩個(gè)函數(shù)
和
,這里
又叫做平滑函數(shù)或者卷積核,那么它們?cè)谶B續(xù)空間的卷積是:
一般我們有一個(gè)這樣的結(jié)論,就是當(dāng)
經(jīng)過足夠多次相同平滑函數(shù)
卷積,就會(huì)足夠接近高斯函數(shù),也就是正態(tài)分布的函數(shù)形式。卷積就是一種平滑操作,這說明高斯函數(shù)就是“最平滑的函數(shù)”。引入熱力學(xué)中熵的概念,高斯函數(shù)就是擁有最高熵的函數(shù),最穩(wěn)定的狀態(tài),以至于自然界大多數(shù)的統(tǒng)計(jì)規(guī)律都呈現(xiàn)出正態(tài)分布:
下面介紹離散形式的卷積。這卷積,首先是由有限項(xiàng)的多項(xiàng)式體現(xiàn)。神奇的是,而它們的乘積就是卷積。首先我們?cè)O(shè)有兩個(gè)多項(xiàng)式
以及
。計(jì)算它們的乘積:
再引入離散形式卷積(向量卷積)的定義,大家比較一下這個(gè)定義和上面多項(xiàng)式的計(jì)算。稍微說明一下,中括號(hào)的意義是p[n]代表向量第n個(gè)元素。將兩個(gè)多項(xiàng)式的系數(shù)寫成向量形式然后進(jìn)行向量卷積,也就是例如
,而沒定義的地方當(dāng)作0。可以發(fā)現(xiàn),兩者是完全一致的:
知道了多項(xiàng)式的乘積就是其相應(yīng)的卷積,我們甚至可以直接得出兩個(gè)冪級(jí)數(shù)卷積的結(jié)果。因?yàn)樘├占?jí)數(shù)就是冪級(jí)數(shù)的一種,所以我們可以將幾乎所有的連續(xù)函數(shù)轉(zhuǎn)換成離散形式,避免了繁復(fù)的積分運(yùn)算:比如我們希望得到
,其中
,只需要簡單地計(jì)算這兩個(gè)級(jí)數(shù)的柯西乘積,所得結(jié)果就是
的卷積。當(dāng)然了,這是后話,與本文的主題無關(guān)。
卷積與圖像處理
在開始講圖像處理之前,我希望先理解一下卷積的整個(gè)過程是怎樣的。從上面的公式看得還是有點(diǎn)懵懵懂懂,從直覺上去理解一下很有必要。觀察卷積的公式以及下面的圖片,這個(gè)過程可以看作,當(dāng)你想求一個(gè)r[n]的時(shí)候:
你先把卷積核q疊在p上面,盡量使左端靠近(如果左對(duì)齊就再好不過了),然后看看在[0, n]內(nèi)p, q重疊的部分是從哪里到哪里,分別寫成向量,那么r[n]就等于其中一個(gè)向量與另一個(gè)向量的逆序的內(nèi)積。
比如當(dāng)n = 2時(shí),兩個(gè)向量是[a_0, a_1, a_2]和[b_2, b_1, b_0];n = 4時(shí),兩個(gè)向量是[a_1, a_2, a_3, a_4]和[b_3, b_2, b_1, b_0]。至于求內(nèi)積,一定難不倒你。下圖說明了這一點(diǎn):
上面是對(duì)某一點(diǎn)上卷積的理解。對(duì)整個(gè)域的卷積,則可以看成是將卷積核(除了開頭幾個(gè)外)不停向右移動(dòng),每移動(dòng)一格就將重疊部分拿出來求內(nèi)積。
這時(shí)我們可以把圖像處理和卷積聯(lián)系起來了。圖像處理是,將一副“源圖像”(Source),通過一些算法,變成一副“目標(biāo)圖像”(Destination)。當(dāng)我們進(jìn)行平滑處理的時(shí)候,用到一個(gè)叫做濾波器(filter)的 東西,也叫做濾鏡。想想我們現(xiàn)實(shí)生活中放大鏡是怎么用的:拿著放大鏡,從報(bào)紙的左上角開始,一直掃啊掃到右下角,掃的過程中一直望著放大鏡和報(bào)紙的重疊區(qū) 域(其實(shí)就是望著放大鏡,因?yàn)樗葓?bào)紙小多了),這樣你就瀏覽完了一張放大過的報(bào)紙。平滑濾鏡也是同樣的使用方法,從源圖的左上角開始掃到右下角,掃的過 程中一直取出重疊部分進(jìn)行內(nèi)積計(jì)算,然后將結(jié)果存放到目標(biāo)圖像中 —— 顯然這個(gè)操作跟卷積是一致的,只不過定義在二維空間內(nèi)。
為了方便量化表示,我們把圖像抽象成定義在
數(shù)環(huán)內(nèi)的二維矩陣,其意義是灰度值,顏色信息我們暫且忽略。卷積核,也就是濾波器同樣也是定義在
內(nèi)的二維矩陣。這樣,二維的卷積我們這樣定義它的離散形式:
我們的卷積核大小并不是無限的,它一個(gè)半徑r,這樣它的大小就是2r+1。規(guī)定了這個(gè)r使得,當(dāng)|x| > r 或 |y| > r,都有Ker[y, x] = 0。規(guī)定過大小之后,由 |i-y| < r; |j-x| < r得到 i-r < y < i+r; j-r < x < j+r。同時(shí)我們規(guī)定Dest和Src的大小是
。于是我們得到了濾波器的算法:
高斯濾波器

二維的高斯函數(shù)(俗稱避孕套函數(shù))
高斯濾波器是最常用的平滑濾波器之一,在Photoshop里面它被用作高斯模糊濾鏡。高斯濾波器的定義很經(jīng)典,就是簡單地把正態(tài)分布離散開來。二維形式只是單純把x替代成(x2 + y2),然后修改系數(shù)令實(shí)數(shù)域上的積分為1:
也許你已經(jīng)發(fā)現(xiàn)了一個(gè)這樣的規(guī)律,這一規(guī)律,這在更高維上仍然是滿足的,也就是在連續(xù)空間里同樣滿足。這將成為我們優(yōu)化算法的關(guān)鍵。將這個(gè)規(guī)律代回到二維離散卷積的公式里,因?yàn)閥在第二個(gè)連加中相當(dāng)于常數(shù)系數(shù)可以提出來,我們發(fā)現(xiàn):
如果x連加所表示是卷積是從右上角開始按照文字書寫順序從左到右,然后從上到下的順序進(jìn)行一維卷積,那么y連加表示的卷積就是先從上到下,再從左到有的順序卷積。在OpenCV提供的數(shù)據(jù)結(jié)構(gòu)的基礎(chǔ)上,不用imgproc提供的算法,我寫了一個(gè)示例:
1 // cflags: -lopencv_highgui -lopencv_core
2
3 #include <iostream>
4 #include <cmath>
5 #include <opencv2/highgui/highgui.hpp>
6
7 using namespace cv;
8 using namespace std;
9
10 const char* title = "gaussian-filter";
11
12 Mat kernelMatrix(int radius, double sigma)
13 {
14 int d = radius * 2 + 1;
15 Mat kernel(2, d, CV_64F);
16
17 double coef = 0;
18 for(int i = 0; i <= radius; i++) {
19 // f(x) = 1/(sigma * sqrt(2 pi)) * e ^ -x^2/(2 s^2)
20 int dx = i - radius;
21 double dx_2 = dx * dx;
22 double w = pow(M_E, - dx_2 / (2 * sigma * sigma));
23
24 coef += w;
25 kernel.at<double>(0, i) = w;
26 kernel.at<double>(0, d - i - 1) = w;
27 // when you used values from i to j (j>i), the sum of them is:
28 // kernel[1, j] - (i ? kernel[1, i-1] : 0)
29 kernel.at<double>(1, i) = coef;
30 }
31
32 for(int i = radius + 1; i < d; i++) {
33 coef += kernel.at<double>(0, i);
34 kernel.at<double>(1, i) = coef;
35 }
36
37 return kernel;
38 }
39
40
41 void convolution(const Mat& img, const Mat& kernel, Mat& output, bool t = true)
42 {
43 for(int y = 0, x = 0; y < img.rows; x = (++x<img.cols)? x : (y++, 0)) {
44 Vec3d r(0, 0, 0);
45
46 int ideal = x - int(kernel.cols / 2),
47 ran_beg = max(ideal, 0) - ideal,
48 ran_end = min(ideal + kernel.cols, img.cols) - ideal;
49
50 for(int i = ran_beg; i < ran_end; i++) {
51 double weight = kernel.at<double>(0, i);
52 Vec3b pixel = img.at<Vec3b>(y, ideal + i);
53
54 r[0] += pixel[0] * weight;
55 r[1] += pixel[1] * weight;
56 r[2] += pixel[2] * weight;
57 }
58
59 double coef = kernel.at<double>(1, ran_end - 1);
60 if(ran_beg) coef -= kernel.at<double>(1, ran_beg - 1);
61
62 output.at<Vec3b>(t?x:y, t?y:x) = Vec3b(
63 saturate_cast<uchar>(r[0]/coef),
64 saturate_cast<uchar>(r[1]/coef),
65 saturate_cast<uchar>(r[2]/coef));
66 }
67 }
68
69
70 int main()
71 {
72 namedWindow(title, WINDOW_AUTOSIZE);
73
74 const int r = 10;
75
76 Mat img = imread("ai-sample.jpg"),
77 kernel = kernelMatrix(r, (r - 1) * 0.3 + 0.8);
78
79 Mat product_v = Mat(img.cols, img.rows, img.type());
80 Mat product_h = Mat(img.rows, img.cols, img.type());
81
82 convolution(img, kernel, product_v);
83 convolution(product_v, kernel, product_h);
84
85 imshow(title, product_h);
86 for(; waitKey(0) > 0;);
87
88 destroyWindow(title);
89 }