使用JavaScript進行基本圖形操作與處理

Sneezry發表於2014-09-03

1.影像資料的相關介面

由於HTML5引入了canvas標籤,這大大簡化了JavaScript處理影像的工作。通過canvas,JavaScript可以對影像進行畫素級的操作,甚至還可以直接處理影像的二進位制原始資料,這為影像的簽名技術提供了支援。另外canvas還提供了常用的影像格式轉換功能,可以使用JavaScript簡單快捷地更改影像的編碼方式。

出於安全考慮,瀏覽器通常不允許處理跨域影像,但利用特殊的手段是可以突破這一限制的。解決處理跨域影像出現的安全警告的方法是使用CORS(Cross-Origin Resource Sharing),具體可以參加http://www.w3.org/TR/cors/

利用FileReadercanvas相配合,可以讀取本地影像檔案,比如我們有如下HTML程式碼:

<canvas id="myCanvas">抱歉,您的瀏覽器還不支援canvas。</canvas>
<input type="file" id="myFile" />

這兩行HTML程式碼包含一個idmyCanvascanvas畫布,還包含一個idmyFile的檔案選擇控制元件,我們將通過檔案選擇控制元件為使用者提供選擇本地檔案的介面,然後利用canvas畫布為JavaScript提供影像處理的介面。

下面通過JavaScript為這兩個元素繫結事件。為了方便引用,先用兩個變數來儲存這兩個元素:

var myCanvas = document.getElementById('myCanvas');
var myFile = document.getElementById('myFile');

當使用者選定一個檔案時,我們就應開始通過FileReader讀取檔案資料,為此監視myFileonchange事件,並構造FileReader

file.onchange = function(event) {
    var selectedFile = event.target.files[0];
var reader = new FileReader();
    reader.onload = putImage2Canvas;
    reader.readAsDataURL(selectedFile);
}

在這段程式碼中,onchange事件被啟用時會傳遞一個event引數給處理函式,eventtarget子屬性是一個描述當前檔案選擇控制元件的物件,其files屬性是一個描述使用者已選檔案資訊的陣列。files是陣列型別是因為HTML5支援一次選擇多個檔案,如果檔案選擇控制元件沒有開啟多選模式,那麼此陣列只有一個元素。

接下來建立了一個FileReader物件,將其儲存在reader中。reader.onload事件定義了檔案載入完成後的操作,reader.readAsDataURL將檔案內容讀取成Data URL。

接下來編寫putImage2Canvas函式,這個函式用來將FileReader讀取的資料放入canvas中供JavaScript處理:

function putImage2Canvas(event) {
    var img = new Image();
    img.src = event.target.result;
    img.onload = function(){
        myCanvas.width = img.width;
        myCanvas.height = img.height;
        var context = myCanvas.getContext('2d');
        context.drawImage(img, 0, 0);
        var imgdata = context.getImageData(0, 0, img.width, img.height);
        // 處理imgdata
    }
}

eventreader.onload傳遞的引數,event.target.resultFileReader讀取的結果,由於之前我們將檔案內容以Data URL的方式讀取,所以可以直接將讀取結果作為src建立影像物件。

當影像建立完畢後,img.onload事件被啟用,此時我們將canvas的尺寸設定成影像的尺寸,然後通過drawImage將影像畫在畫布上,最後通過getImageData獲取影像畫素資料供JavaScript處理。

下面我們來詳細瞭解下drawImagegetImageData這兩個方法,這兩個方法將會在後面的章節中一直用到,是JavaScript處理影像用到的最基本的方法。

drawImage有三種用法,第一種是隻指定圖片的繪製位置:

context.drawImage(img, x, y);

這也是本節開始的程式碼例項中用到的使用方法,這種方法會將圖片左上角置於座標相對於畫布的(x, y)點上,如果畫布尺寸足夠則畫出整個影像,否則將超出畫布的部分捨棄,如圖1-1。

enter image description here
圖1-1 超出畫布部分的影像會被捨棄1

1 影像來自Wikipedia,由Pamri上傳。

drawImage的第二種方法是指定指定圖片繪製位置的同時影像的尺寸:

context.drawImage(img, x, y, width, height);

新繪製的影像會根據指定的尺寸進行放大或縮小,如圖1-2。

enter image description here

圖1-2 利用drawImage縮放影像

drawImage的第三種用法是擷取圖片的一部分進行繪製:

context.drawImage(img, sx, sy, swidth, sheight, x, y, width, height);

sxsy指定影像被擷取部分左上角在圖片上的座標,swidthsheight指定影像被擷取部分的尺寸,xy指定影像被擷取部分畫在畫布上的位置,widthheight指定影像被擷取部分在畫布上重繪的尺寸。圖1-3中利用drawImage擷取了影像的一部分並畫在了畫布上。

enter image description here
圖1-3 利用drawImage擷取圖片的一部分

getImageData方法用來獲取canvas畫布中影像的畫素資料,使用方法比較簡單,只需指定要獲取影像左上角的位置和尺寸即可:

context.getImageData(x, y, width, height);

返回的資料是一個物件,此物件包含三個屬性,分別是datawidthheight,其中data就是影像的畫素資料。data是一個一維陣列,順次記錄著每個畫素點的RGBA資訊。R代表紅色,G代表綠色,B代表藍色,A代表不透明度,其取值範圍均為0-255。對於A,0代表完全透明,255代表完全不透明。

由於data是一個一維陣列,所以在處理資料時應以每4個元素為單位讀取data

for(var i=0, len=imgdata.data.length; i<len; i+=4) {
    var r = imgdata.data[i],
         g = imgdata.data[i+1],
         b = imgdata.data[i+2],
         a = imgdata.data[i+3];
    // 處理畫素資料
}

比如最簡單的反色操作,我們可以通過以下程式碼實現:

for(var i=0, len= imgdata.data.length; i<len; i+=4) {
    imgdata.data[i] = 255-imgdata.data[i];
    imgdata.data[i+1] = 255-imgdata.data[i+1];
    imgdata.data[i+2] = 255-imgdata.data[i+2];
}

由於反色無需對影像的不透明度進行處理,所以我們只處理了R、G和B的資料。

資料處理完畢後可以通過putImageData將處理結果輸出到畫布上:

putImageData(imgdata, x, y);

最後的處理結果如圖1-4所示。

enter image description here
圖1-4 通常操作影像畫素資料反色處理影像

toDataURL方法可以將canvas畫布中的影像儲存為圖片:

var imgsrc = myCanvas.toDataURL();
var img = document.create('img');
img.src = imgsrc;

toDataURL預設將影像轉換為PNG圖片,但也可以儲存為JPEG圖片:

var imgsrc = myCanvas.toDataURL('image/jpeg');

如果儲存為JPEG圖片,還可以通過第二個引數指定圖片質量:

var imgsrc = myCanvas.toDataURL('image/jpeg', quality);

quality的取值範圍為0.0-1.0,0.0代表圖片質量最差,1.0代表圖片質量最好。

此外,Chrome瀏覽器還支援轉換為WebP影像:

var imgsrc = myCanvas.toDataURL('image/webp');

2.影像幾何變換

得益於HTML5完善的影像處理介面,在對影像進行幾何變換時,我們並不需要單獨操作每個畫素點,下面將對影像平移、影像縮放、映象變換、影像旋轉和影像轉置的實現逐一講解。

2.1影像平移

canvas通過translate方法實現影像平移。注意,translate平移的是canvas畫布的座標,並不會改變畫布上已有影像的位置。

translate的用法非常簡單,只需指定canvas畫布左上角平移後的座標即可:

context.translate(x, y);

為進一步使讀者理解,下面舉一個例子。首先我們在畫布的(10, 10)處畫出一幅圖片:

context.drawImage(img, 10, 10);

然後將canvas畫布左上角移至(100, 100)

context.translate(100, 100);

此時畫布上的影像並沒有變換,因為平移的是畫布座標,而不是畫布上的影像,即影像並不與座標一同平移。之後再次在畫布的(10, 10)處畫出一幅圖片:

context.drawImage(img, 10, 10);

以上程式碼執行完畢後的結果如圖1-5所示。

enter image description here
圖1-5 translate示例執行結果

translate所操作的點永遠都是畫布的左上角,如果希望將執行context.translate(x, y)後畫布的座標恢復到之前的狀態應再執行context.translate(-x, -y),畫布預設的原點在左上角,水平方向右側為正方向,垂直方向下側為正方向,這與我們熟悉的直角座標系有所不同。

2.2影像縮放

canvas通過scale對影像進行縮放。縮放後的畫布的原點與原畫布的原點相對應,所以如果希望以影像中心為參考點,縮放前應先將畫布原點平移至影像中心。

scale有兩個引數,分別是畫布橫向的放大倍數和縱向的放大倍數:

context.scale(scalewidth, scaleheight);

比如context.scale(2, 2)將畫布的橫向和縱向均變為原來的2倍,圖1-6給出了縮放變換後的結果。

enter image description here
圖1-6 縮放變換後的結果

縮放變換是對畫布的變換,此操作並不影響已畫在畫布上的影像,比如如下程式碼不會改變影像:

context.drawImage(img, 10, 10);
content.scale(2, 2);

正確的方法是縮放影像前應先縮放畫布,然後再開始繪圖:

content.scale(2, 2);
context.drawImage(img, 10, 10);

scalewidthscaleheight的絕對值大於1時為放大影像,小於1時為縮小影像,等於1是尺寸與原影像一致。

如前所述,所以如果希望以影像中心為參考點,縮放前應先將畫布原點平移至影像中心。比如下列程式碼以影像中心為參考點,將原影像縮小至原來的1/4(邊長縮小至原來的1/2):

// 先將畫布原點移至影像中心,此處影像中心也是畫布的中心
content.translate(myCanvas.width/2, myCanvas.height/2);

// 將畫布縮小至原畫布的1/4
content.scale(0.5, 0.5);

// 將原點還原
content.translate(-myCanvas.width/2, -myCanvas.height/2);

// 將影像畫在畫布中
context.drawImage(img, 10, 10);

執行結果如圖1-7所示。

enter image description here
圖1-7 以影像中心為參考點縮放影像

2.3映象變換

canvas中並沒有為映象變換專門提供方法,但不必緊張,至此我們依然尚未接觸到畫素級的操作。在上一節中介紹了影像縮放的相關內容,其中講到scalewidthscaleheight的絕對值大於1時為放大,小於1時為縮小,但並沒有提到其正負。

通常情況下,scale的兩個引數均是正數,如果為負數,則為映象效果。比如有如下程式碼:

content.translate(myCanvas.width/2, myCanvas.height/2);
content.scale(-1, 1);
content.translate(myCanvas.width/2, myCanvas.height/2);
content.drawImage(img, 10, 10);

請注意,在做映象變換之前一定要先平移畫布原點到合適的位置,否則由於畫布預設以左上角為原點,映象變換後的影像會在畫布之外導致不可見。

這種以圖片垂直中線為對稱軸左右映象的變換叫做水平映象變換,變換後的結果如圖1-8所示。

enter image description here
圖1-8 水平映象變換

另一種變換叫做垂直映象變換,這種變換以影像的水平中線為對稱軸,變換方法為:

content.scale(1, -1);

垂直映象變換同樣需要先平移畫布原點,此處程式碼中不再重複敘述。垂直變換後的結果如圖1-9所示。

enter image description here
圖1-9 垂直映象變換

2.4影像旋轉

canvas通過rotate方法進行影像旋轉操作。rotate方法以畫布原點作為旋轉中心,以弧度作為旋轉角度單位,正數代表順時針旋轉。

context.rotate(angle);

如果想要以度作為角度單位,需要使用轉換公式degree*Math.PI/180,如以下程式碼使畫布順時針旋轉30°:

context.rotate(30*Math.PI/180);

如果希望以影像中心作為旋轉中心,首先需要將畫布原點移至影像中心:

context.translate(imgCenterX, imgCenterY);

同樣,rotate隻影響畫布座標,不影響已畫在畫布上的影像。

下列程式碼以影像中心為旋轉中心,將影像順時針旋轉了45°:

context.translate(myCanvas.width/2, myCanvas.height/2);
context.rotate(45*Math.PI/180);
context.translate(-myCanvas.width/2, -myCanvas.height/2);
context.drawImage(img, 10, 10);

旋轉後的結果如圖1-10所示。

enter image description here
圖1-10 利用rotate旋轉影像

2.5影像轉置

影像的轉置操作是將影像每行水平排列的畫素順次垂直排列,直觀地說就是以影像自左上角至右下角的對角線為對稱軸翻轉影像1

1 此處說法並不嚴謹,僅為讓讀者更易領悟轉置的定義,實際上只有正方形的影像轉置操作才是以影像自左上角至右下角的對角線為對稱軸翻轉的。

canvas沒有為影像轉置專門提供方法,但我們可以利用旋轉和映象組合的方法實現影像轉置的目的。影像的轉置可以分解為水平翻轉後再順時針旋轉90°,或是垂直翻轉後再逆時針旋轉90°。下面我們利用順時針旋轉90°後再水平翻轉實現影像轉置的操作:

context.translate(myCanvas.width/2, myCanvas.height/2);
context.scale(-1, 1);
context.rotate(90*Math.PI/180);
context.translate(-myCanvas.width/2, -myCanvas.height/2);
context.drawImage(img, 10, 10);

轉置後的結果如圖1-11所示。

enter image description here
圖1-11 利用scalerotate實現影像的轉置

細心的讀者可能會發現,轉置操作應該是水平翻轉後再逆時針旋轉90°,但為何此處要順時針旋轉90°卻能得到正確的結果呢?原因很簡單,因為翻轉把畫布的座標軸也翻轉了,在原有座標系下的逆時針旋轉在新的座標系下就變成了順時針旋轉,此處還請讀者仔細品味。

另外,如果是先旋轉再翻轉,則至少需要改變一種操作的變換方向,比如保持順時針旋轉90°的同時,需要進行垂直映象的操作,而不再是水平映象操作。下面的程式碼與上面的程式碼實現的同樣的效果:

context.translate(myCanvas.width/2, myCanvas.height/2);
context.rotate(90*Math.PI/180);
context.scale(1, -1);
context.translate(-myCanvas.width/2, -myCanvas.height/2);
context.drawImage(img, 10, 10);

3.影像傅立葉變換

上一節中我們瞭解了影像的幾何變換,幾何變換是空間域中對影像的處理方法,理解起來很容易,處理起來也很形象。在影像處理中還有一種方法是在頻域上對影像進行變換,影像的頻域變換沒有空間域變換直觀,理解起來有一定的難度,本節會用最通俗易懂的方式向讀者講解有關頻域處理影像的方法,如果讀者需要深入瞭解有關內容,可以參見《Visual C++數字影像處理典型演算法及實現》第五章的內容。

3.1影像傅立葉變換的意義

由於影像中紅色、綠色和藍色三個顏色通道是相互獨立的,所以可以將一幅彩色影像看成是三張獨立的灰度影像,這也是影像處理通常只討論灰度影像處理方法的原因。同樣,下面我們也講只討論灰度影像的處理。

對於一幅灰度影像,給出一個影像上畫素點的座標(x, y),我們都可以找到這個畫素點的灰度值z,於是我們可以將一幅影像抽象成一個函式z=f(x, y)。很顯然,抽象出來的函式是一個離散型函式,這種抽象思想很重要,它將對影像的處理演變成了對函式的處理,從而使得影像處理這一問題變成了純粹的數學問題。

由於每一幅影像都唯一地對應一個函式f(x, y),所以這一函式的性質可以反映出所對應影像的性質。傅立葉變換就是把一個離散函式變成無限多個正弦函式的疊加,這些正弦函式的頻率體現了離散函式的性質,從而體現影像的性質,這就是影像傅立葉變換的意義。

3.2空間域特性在頻域中的表現

雖然影像在頻域上比較抽象,但我們依然可以通過對照影像空間域的特性在頻域中的表現,來直觀感受頻率所體現的影像特性。

對於一維函式y=g(x),經過傅立葉變換後,就得到了傅立葉變換的頻率譜,此頻率譜以傅立葉展開得到的一系列正弦函式的頻率為橫座標,對應正弦函式的幅度為縱座標。對於影像轉換來的函式z=f(x, y),是一個二維函式,那麼影像傅立葉變換的頻率譜就應該是一個三維的影像。

清楚了這一點,我們就可以繼續思考了。先來考慮一幅最簡單的影像——純色影像。對於一幅純色影像,其灰度值處處相等,其轉換為函式後為z=C,對應的函式影像為與xOy面平行的平面,且此平面過點(0, 0, C)。顯然其傅立葉變換的結果就是函式本身z=C(頻率為0,幅度為C,其他頻率分量正弦波的幅度均為0),那麼其歸一化頻率譜就是(0, 0, 1)處的一個點,其他位置無影像。

圖1-12是同一幅影像分別在空間域和頻域上的表現形式。

enter image description here
圖1-12 同一幅影像分別在空間域和頻域上的表現形式

圖中左側為影像在空間域中畫素灰度的分佈,這也是正常情況下影像的表現形式。右側為此影像在頻域中的表現形式,圖中中心處為座標原點,所以影像中心代表低頻,影像四周代表高頻,顏色越白代表能量越高,顏色越黑代表能量越低。值得注意的是,在頻譜影像中體現出了負頻率(2、3、4象限影像),這只是數學上計算得出的結果,並沒有實際的物理意義。

對於一幅影像來說,通常其頻譜都是低頻能量高,高頻能量少,為了找出其中的原因,讓我們來探索下頻域中的頻率到底與空間域中影像的表現形式是如何對應起來的。下面請將圖1-13與圖1-12對照來看。

enter image description here
圖1-13 邊緣模糊影像的頻率譜

圖1-13中的影像是將圖1-12經過高斯模糊得到的,可以發現模糊後的影像的頻率中高頻部分消失了,由此可見,影像頻譜高頻部分體現了影像中顏色變化明顯的特性,這些顏色變化明顯的部分在空間域上就對應於影像中圖形的邊界或者噪點。

下面我們再來將圖1-14與圖1-12對照來看。

enter image description here
圖1-14 灰度動態範圍壓縮後影像的頻率譜

可以看出,如果壓縮影像灰度的動態範圍,但保留影像的細節,則頻率譜表現為低頻部分被濾除,由此可見頻率譜低頻部分描述的是影像平滑區域的灰度動態範圍。

除此之外,頻率譜還在其它一些特性上與空間域影像有對應關係。空間影像旋轉一定角度後,頻率譜也相應旋轉,如圖1-15。

enter image description here
圖1-15 影像在空間域中旋轉後頻率譜也相應旋轉

當影像被放大後,影像頻率譜的網格會縮小,如圖1-16所示。

enter image description here

圖1-16 影像被放大後頻率譜的網格會縮小

知道這些性質後,影像在頻域上抽象的表現形式就變得直觀起來了。上面都是簡單幾何圖形的例子,通常自然界中照片的頻率譜顯得更加複雜和混亂,圖1-17展示了自然界中一幅照片的頻率譜。

enter image description here
圖1-17 自然界中照片的頻率譜

3.3快速傅立葉變換

影像有空間域到頻域的轉換需要進行傅立葉變換,對於離散傅立葉變換的時間複雜度為O(N^2),如果N比較大,這將是巨大的計算量,從而無法讓計算機實時進行處理。快速傅立葉變換完成了離散傅立葉變換所做的工作,並把時間複雜度壓縮到O(NlogN)。

快速傅立葉變換的演算法有很多種,目前使用比較廣泛的是蝶形演算法,蝶形演算法是將整個變換的計算過程分解為多個級,每個級又分解為多個組,每個組又包含多個單元。圖1-18給出了一個單元的計算過程。

enter image description here
圖1-18 蝶形運算中一個單元的計算過程

其中X1=x1+x2*WX2=x1-x2*W

圖1-19給出了一個長度為8的陣列,進行蝶形快速傅立葉變換計算過程的流程圖。

enter image description here
圖1-19 蝶形快速傅立葉變換計算流程圖,圖片來自cnx.org

從圖中我們可以看出對於長度為8的陣列,我們將計算過程分為3個級,自左向右第1級有4個組,每組包含一個計算單元;第2級有2個組,每組包含2個計算單元;第3級有1個組,包含4個計算單元。

為論述與程式碼保持一致,下面提到的級序數i、組序數j和計算單元序數k的取值均從0開始。對於長度為2^r的陣列,進行蝶形快速傅立葉變換計算時分為r個級,第i級有2^(r-i-1)個組,第i級中每個組中包含有2^i個計算單元。

計算單元中的係數W是與級數和計算單元序數有關的,對於第i級中每組的第k個計算單元的係數為Wn[2^(r-1-i)*k],其中r為計算過程的總級數。

Wn是一個陣列,其值與所變換陣列的長度有關。對於長度為2^r的陣列,Wn的長度為2^(r-1),Wn[k]=cos(-2π*k/2^r)+sin(-2π*k/2^r)i,其中i為虛數單位。

細心的讀者也許會發現,進行蝶形快速傅立葉變換時,左側數列的位序與右側最終結果的位序並不相同,所以在進行變換前需要先對原有數列重新排列,我們稱之為倒位序排列。下面是JavaScript實現蝶形快速傅立葉演算法的完整程式碼:

function fft(dataArray) {
    // 複數乘法
    this.mul = function(a, b) {
        if(typeof(a)!=='object') {
            a = {real: a, imag: 0}
        }
        if(typeof(b)!=='object') {
            b = {real: b, imag: 0}
        }
        return {
            real: a.real*b.real-a.imag*b.imag,
            imag: a.real*b.imag+a.imag*b.real
        };
    };

    // 複數加法
    this.add = function(a, b) {
        if(typeof(a)!=='object') {
            a = {real: a, imag: 0}
        }
        if(typeof(b)!=='object') {
            b = {real: b, imag: 0}
        }
        return {
            real: a.real+b.real,
            imag: a.imag+b.imag
        };
    };

    // 複數減法
    this.sub = function(a, b) {
        if(typeof(a)!=='object') {
            a = {real: a, imag: 0}
        }
        if(typeof(b)!=='object') {
            b = {real: b, imag: 0}
        }
        return {
            real: a.real-b.real,
            imag: a.imag-b.imag
        };
    };

    // 倒位序排列
    this.sort = function(data, r) {
        if(data.length <=2) {
            return data;
        }
        var index = [0,1];
        for(var i=0; i<r-1; i++) {
            var tempIndex = [];
            for(var j=0; j<index.length; j++) {
                tempIndex[j] = index[j]*2;
                tempIndex[j+index.length] = index[j]*2+1;
            }
            index = tempIndex;
        }
        var datatemp = [];
        for(var i=0; i<index.length; i++) {
            datatemp.push(data[index[i]]);
        }
        return datatemp;
    };

    var dataLen = dataArray.length;
    var r = 1; // 迭代次數
    var i = 1;
    while(i*2 < dataLen) {
        i *= 2;
        r++;
    }
    var count = 1<<r; // 相當於count=2^r

    // 如果資料dataArray的長度不是2^N,則開始補0
    for(var i=dataLen; i<count; i++) {
        dataArray[i] = 0;
    }

    // 倒位序處理
    dataArray = this.sort(dataArray, r);

    // 計算加權係數w
    var w = [];
    for(var i=0; i<count/2; i++) {
        var angle = -i*Math.PI*2/count;
        w.push({real: Math.cos(angle), imag: Math.sin(angle)});
    }

    for(var i=0; i<r; i++) { // 級迴圈
        var group = 1<<(r-1-i);
        var distance = 1<<i;
        var unit = 1<<i;
        for(var j=0; j<group; j++) { // 組迴圈
            var step = 2*distance*j;
            for(var k=0; k<unit; k++) { // 計算單元迴圈
                var temp = this.mul(dataArray[step+k+distance], w[count*k/2/distance]);
                dataArray[step+k+distance] = this.sub(dataArray[step+k], temp);
                dataArray[step+k] = this.add(dataArray[step+k], temp);
            }
        }
    }
    return dataArray;
}

上面是一維快速傅立葉的演算法,快速傅立葉變換的逆變換用JavaScript實現的完整程式碼如下:

function ifft(dataArray) {
    for(var i=0, dataLen=dataArray.length; i<dataLen; i++) {
        if(typeof(dataArray[i])!='object'){
            dataArray[i] = {
                real: dataArray[i],
                imag: 0
            }
        }
        dataArray[i].imag *= -1;
    }
    dataArray = fft(dataArray);
    for(var i=0, dataLen=dataArray.length; i<dataLen; i++) {
        dataArray[i].real *= 1/dataLen;
        dataArray[i].imag *= -1/dataLen;
    }
    return dataArray;
}

由於灰度影像是一個二維陣列,所以我們需要用到二維傅立葉變換。二維傅立葉變換可以通過一維傅立葉變換得到,首先對二維陣列的每一行進行一維傅立葉變換,並用變換後的結果代替原有的資料,然後再對經過行變換後的二維陣列的每一列進行一維傅立葉變換,並用變換後的結果代替原有的資料。下面是JavaScript實現二維傅立葉變換的完整程式碼:

function fft2(dataArray, width, height) {
    var r = 1;
    var i = 1;
    while(i*2 < width) {
        i *= 2;
        r++;
    }
    var width2 = 1<<r;
    var r = 1;
    var i = 1;
    while(i*2 < height) {
        i *= 2;
        r++;
    }
    var height2 = 1<<r;

    var dataArrayTemp = [];
    for(var i=0; i<height2; i++) {
        for(var j=0; j<width2; j++) {
            if(i>=height || j>=width) {
                dataArrayTemp.push(0);
            }
            else {
                dataArrayTemp.push(dataArray[i*width+j]);
            }
        }
    }

    dataArray = dataArrayTemp;
    width = width2;
    height = height2;

    var dataTemp = [];
    var dataArray2 = [];
    for(var i=0; i<height; i++) {
        dataTemp = [];
        for(var j=0; j<width; j++) {
            dataTemp.push(dataArray[i*width+j]);
        }
        dataTemp = fft(dataTemp);
        for(var j=0; j<width; j++) {
            dataArray2.push(dataTemp[j]);
        }
    }
    dataArray = dataArray2;
    dataArray2 = [];
    for(var i=0; i<width; i++) {
        var dataTemp = [];
        for(var j=0; j<height; j++) {
            dataTemp.push(dataArray[j*width+i]);
        }
        dataTemp = fft(dataTemp);
        for(var j=0; j<height; j++) {
            dataArray2.push(dataTemp[j]);
        }
    }
    dataArray = [];
    for(var i=0; i<height; i++) {
        for(var j=0; j<width; j++) {
            dataArray[j*height+i] = dataArray2[i*width+j];
        }
    }
    return dataArray;
}

影像經過二維傅立葉變換所得到的結果就是前面提到的頻率譜。需要注意的一點是,由於影像畫素的排列是自左向右自上向下的,即影像的座標原點在左上角,且縱向正方向向下,這導致變換後得到的頻譜與前面有所不同——低頻出現在影像四周,高頻出現在影像中心。如圖1-20左側所示,左側是直接轉換後得到的頻譜,右側是將原點移至中心的頻譜。

enter image description here
圖1-20 未經處理的頻譜高頻在影像中心

4.影像增強

影像增強包括影像的平滑處理、去噪點、尋找邊緣和影像銳化等。處理的方法有兩種,一種是在空間域中對影像的灰度值重新計算,另一種是先將影像轉換到頻域上,對影像的頻譜進行運算,然後再利用逆變換將影像還原到空間域中。本節將從空間域和頻域兩種處理方法講解影像增強有關的內容。

4.1卷積運算

卷積運算是空間域中處理影像最常用的計算方法,比如模板運算(mask)就是一種卷積運算。對於連續函式f(x)g(x),其卷積運算所得的新的函式F(x)有如下定義:

F(x)=f*g=\int_{-\infty}^{\infty} f(x).g(t-x)\, dt

對於離散函式f(x)g(x),其卷積定義為:

F(x)=f*g=\sum_t f(x).g(t-x)

擴充套件到二維離散函式I(x, y)G(x, y)的卷積,有定義:

I_{\sigma}(x,y)=I*G=\sum_i \sum_j I(x,y).G(i-x,j-y)

如果有

G(x,y)=\frac{1}{2\pi\rho^2}e^{-(x^2+y^2)/2\rho^2}

則上式的操作就變成了高斯模糊處理。

但在實際操作中,我們往往並不是在整個影像空間域中做卷積運算,因為那樣計算量是非常巨大的。比如對於一個100*100的影像如果在整個空間域做卷積,需要進行1億次乘法和1億次加法,這是不可接受的。為了使計算機可以實時處理影像,通常只在一個給定的視窗中做卷積運算,而不是整個空間域。常用的視窗有3*3、5*5和7*7等。如果使用3*3的視窗處理100*100的影像則只需進行9萬次乘法和9萬次加法,大大提高了影像的處理速度。

4.2模板運算

模板運算可以簡單看成是模板與視窗中畫素的對乘加,比如有模板:

\frac{1}{9}\begin{bmatrix}1&1&1\\1&1&1\\1&1&1\end{bmatrix}

和視窗畫素:

\begin{bmatrix}a&b&c\\d&e&f\\g&h&i\end{bmatrix}

則視窗中心畫素灰度值經計算後將變為\frac{a+b+c+d+e+f+g+h+i}{9},實際就是用一個畫素周圍的8個畫素點,和其自身灰度值的均值,代替原有的灰度值,最後的結果就是影像變得更加平滑,但是邊緣也會顯得更加模糊,如圖1-21所示。

enter image description here
圖1-21 使用模板使影像變得模糊

如果模板為:

\begin{bmatrix}0&1&0\\1&-4&1\\0&1&0\end{bmatrix}

在實現的效果為邊緣檢測,如圖1-22所示。

enter image description here
圖1-22 使用模板進行邊緣檢測

利用JavaScript實現模板運算的完整演算法如下:

function mask(maskArray, dataArray, width, height) {
    var maskWidth = maskArray.length;
    var maskHeight = maskArray[0].length;
    var xStart = (maskWidth-1)/2;
    var xEnd = width-xStart;
    var yStart = (maskHeight-1)/2;
    var yEnd = height-yStart;
    var maskXStart = -(maskWidth-1)/2;
    var maskXEnd = -maskXStart;
    var maskYStart = -(maskHeight-1)/2;
    var maskYEnd = -maskYStart;
    var temp=[],tempSum,x,y,i,j,tempMaskArray,index=0;
    for(y=0; y<height; y++) {
        for(x=0; x<width; x++) {
            if(x>xStart && x<xEnd && y>yStart && y<yEnd) {
                tempSum  = 0;
                for(j=maskYStart; j<=maskYEnd; j++) {
                    tempMaskArray = maskArray[j-maskYStart];
                    for(i=maskXStart; i<=maskXEnd; i++) {
                        tempSum += dataArray[(j+y)*width+i+x]*
                                tempMaskArray[i-maskXStart];
                    }
                }
                temp[index] = Math.round(tempSum);
            }
            else {
                temp[index] = dataArray[index];
            }
            index++;
        }
    }
    return temp;
}

4.3中值濾波

中值濾波不再是像模板運算那樣計算加權平均值,其思想是用視窗畫素灰度值的中間值代替相應畫素點的灰度值。中值濾波的好處是使影像變得平滑的同時,保留了畫素點直接灰度值的梯度。

利用JavaScript實現中值濾波的完整程式碼如下:

function median(filterWidth, filterHeight, dataArray, width, height) {
    var temp = [];
    for(var i=0; i<dataArray.length; i++) {
        temp.push(dataArray[i]);
    }
    for(var x=(filterWidth-1)/2; x<width-(filterWidth-1)/2; x++) {
        for(var y=(filterHeight-1)/2; y<width-(filterHeight-1)/2; y++) {
            var tempArray = [];
            for(var i=-(filterWidth-1)/2; i<=(filterWidth-1)/2; i++) {
                for(var j=-(filterHeight-1)/2; j<=(filterHeight-1)/2; j++) {
                    tempArray.push(temp[(j+y)*width+i+x]);
                }
            }
            // 泡沫排序,找出中值
            do {
                var loop = 0;
                for(var i=0; i<tempArray.length-1; i++) {
                    if(tempArray[i]>tempArray[i+1]) {
                        var tempChange = tempArray[i];
                        tempArray[i] = tempArray[i+1];
                        tempArray[i+1] = tempChange;
                        loop = 1;
                    }
                }
            }while(loop);
            dataArray[y*width+x] = tempArray[Math.round(tempArray.length/2)];
        }
    }
    return dataArray;
}

圖1-23是通過中值濾波去除影像噪點的結果。

enter image description here
圖1-23 利用中值濾波去除影像噪點

4.4頻域影像增強

頻域影像增強的思想是先將影像轉換到頻域,通過轉移函式H得到新的影像頻譜,最後再轉換到空間域:

I => F => F*H => I'

對於不同的轉移函式H,可以得到不同的增強效果。低通濾波器可以使影像變得更加平滑,常見的低通濾波器有巴特沃斯低通濾波器和梯形低通濾波器。n階巴特沃斯低通濾波器對應的轉換函式為H(u,v)=\frac{1}{1+{[D_0/D(u,v)]}^{2n}},其中D(u, v)為點(u, v)到頻域原點的距離。圖1-24為1階巴特沃茲低通濾波器處理影像的結果,D_0取圖片寬度的1/16。

enter image description here
圖1-24 巴特沃茲低通濾波器處理影像

高通濾波器保留影像的高頻部分而濾除了低頻部分,巴特沃斯高通濾波器是一種常見的高通濾波器,n階巴特沃斯高通濾波器對應的轉換函式為H(u,v)=\frac{1}{1+{[D(u,v)/D_0]}^{2n}},其中D(u, v)為點(u, v)到頻域原點的距離。圖1-25為1階巴特沃茲高通濾波器處理影像的結果,D_0取圖片寬度的1/16。

enter image description here
圖1-25 巴特沃茲高通濾波器處理影像

相關文章