在各大平台上的 App 商店裡,影像處理相關的應用程式總是在熱門排行榜上佔有一席之地,Web Platform 當然也不落人後的提供建構影像處理 APP 的相關功能。在這篇文章裡,我們將介紹如何利用 HTML5 的
getUserMedia() API 來建構一個影像處理應用程式的架構,接著我們會介紹在 Web Platform 上有哪些工具可以用來實作影像處理的功能。
基礎架構:擷取網路攝影機影像並取出影像資料
下面的這個範例程式是用來介紹一個在 Web Platform 上的影像處理應用的事的基本架構。在讀者按下 Play 鍵以後,瀏覽器會要求你授權使用網路相機,接著會出現兩個影像畫面。左邊的影像是夠過相機擷取到的原始影像,右邊的畫面是處理完的結果。但因為現在沒有對影像做任何處理,所以左邊的畫面和右邊的畫面會一模一樣。
小提醒:以下的範例程式都需要有網路相機才可以執行!
相信熟悉謀智台客的讀者們對
getUserMedia() 這個 API 一定不陌生,不熟悉的讀者也可以參考台客們所寫的文章[[1]](https://tech.mozilla.com.tw/#ref-1)[[2]](https://tech.mozilla.com.tw/#ref-2)。間單來說,HTML5 的
getUserMedia() API 可以用來擷取電腦上的攝影機或是麥克風的硬體資源,而且
getUserMedia() API 是一個非同步的(asynchronous)的 API,其結果會透過 callback function 來傳回一個
MediaStream 物件。
getUserMedia() 這個 API 的使用方法如下:
navigator.getUserMedia(
{video: true, audio: false} , // 要求相機資源,不要麥克風資源
onSuccess, // 成功得到相機資源後呼叫該函數
onFail // 若失敗,呼叫該函數
);
12345
navigator.getUserMedia( {video: true, audio: false} , // 要求相機資源,不要麥克風資源 onSuccess, // 成功得到相機資源後呼叫該函數 onFail // 若失敗,呼叫該函數);
在 [[2]](https://tech.mozilla.com.tw/#ref-2) 裡,Randy 為我們介紹如何將得到的影像資料透過錄影 API 來錄成一的檔案。而在這個範例裡,我們將得到的影像資料(也就是
MediaStream 物件)導入一個
video 元素來秀在畫面上。在以下這段程式碼裡,我們在
getUserMedia() 成功擷取相機資源後的 callback function 裡面將
MediaStream 物件導入一個
video 元素裡:
function onSuccess(stream) {
localMediaStream = stream;
video.src = window.URL.createObjectURL(localMediaStream);
video.onloadeddata = initCanvases();
video.play();
}
123456
function onSuccess(stream) { localMediaStream = stream; video.src = window.URL.createObjectURL(localMediaStream); video.onloadeddata = initCanvases(); video.play();}
現在我們可以把攝影機得到的影像畫在一個
video 元素裡了,那我們要怎麼進一步的得到影像的資料呢?以現在 HTML5 的架構來說,我們無法直接從
video 元素得到影像資料,我們需要把
video 元素再倒入一個
canvas 元素中,然後透過該
canvas 元素取得一個
ImageData 物件。從這裡開始就是每個影像處理應用程式的重點了,每個應用程式就可以發揮創意來修改得到的影像資料,有關操作
ImageData 物件裡的資料的方法我們將會在文章的後半段介紹,現在先讓我們略過。當影像修改完畢後,我們可以再將
ImageData 物件再畫到另外一個
canvas 元素裡,並且秀在銀幕上。最後我們會透過
requestAnimationFrame() API 來要求下一個影像畫面。接下來的這段範例程式碼為我們示範以上的流程:
function processOneFrame() {
// 將 video 元素的影像畫到一個 canvas 元素裡
ctxOperate.drawImage(video, 0, 0, video.width, video. height);
// 從 canvas 元素裡拿出一個 ImageData 物件
var imageData = ctxOperate.getImageData(0, 0, video.width, video.height);
// 處理得到的 ImageData 物件
// 這裏先跳過。
......
// 將處理好的 ImageData 物件畫到另外一個 canvas 元素裡並且秀在畫面上
ctxResult.putImageData(imageData, 0, 0);
// 要求下一個影像畫面
if (isContinuous) {
requestAnimationFrame(processOneFrame);
} else {
return;
}
}
123456789101112131415161718192021
function processOneFrame() { // 將 video 元素的影像畫到一個 canvas 元素裡 ctxOperate.drawImage(video, 0, 0, video.width, video. height); // 從 canvas 元素裡拿出一個 ImageData 物件 var imageData = ctxOperate.getImageData(0, 0, video.width, video.height); // 處理得到的 ImageData 物件 // 這裏先跳過。 ...... // 將處理好的 ImageData 物件畫到另外一個 canvas 元素裡並且秀在畫面上 ctxResult.putImageData(imageData, 0, 0); // 要求下一個影像畫面 if (isContinuous) { requestAnimationFrame(processOneFrame); } else { return; }}
套用特效到影像資料上
現在我們知道一個在 Web Platform 上的影像處理應用程式的基本架構長什麼樣子了,接下來我們來介紹如何實作影像處理的功能。其實在 Web Platform 上你可以有很多種選擇來實作影像處理的功能,以下列出三種方法:
- 透過 CSS 的 filters 來叫瀏覽器幫你套用特效
- 將影像原始資料取出再透過 JavaScript 來修改影像資料
- 將影像原始資料送進 GPU 裡再透過 WebGL 來修改影像資料
這些方法都有其優缺點,讓我們先來比較一番。首先,透過 CSS filters 是最簡單的方法,但也是最被侷限的方法。理由很簡單,因為 CSS 所提供的 fiter 的種類有限,雖然可以將所提供的 filters 組合起來使用,但能變化出來的花樣還是有所限制。再來,透過 JavaScript 是最通用的方法,但有可能是效能最差的方法。一旦我們可以取得影像的原始資料,想必聰明的讀者們一定可以透過撰寫 JavaScript 來任意的操作影像資料並且得到想要的結果。但是身為一個直譯語言,JavaScript 還是有其在效能上的局限,因此若是所想要的功能需要大量的計算的話,透過 JavaScript 可能會算很久。最後,透過在 GPU 上對影像的每個像素點做平行運算可以大幅度的增加效能,但並不是每種想要實作的功能都適合做平行運算。假設我們想要實作的功能是對影像中的每個像素點套用相同的運算,且套用在每個像素點的運算是互相獨立的(也就是說套用在某像素點的運算不會需要其他像素點的運算結果)的話,那該功能可能就很適合透過 WebGL 在 GPU 上實作。但若是我們想要實作的功能會因為像素點所在的位置或是時間有所不同,或是對不同像素點的計算有相依性的話,那該功能就不適合透過 WebGL 來實作在 GPU 上,相反的,該功能較適合透過 JavaScript 來實作。
簡單的分析三種方法的特性後,接著就來實作出一個特效。在這裡我們先介紹透過 CSS 以及透過 JavaScript 的兩種方法,在未來的文章裡,我們再專門介紹透過 WebGL 來操作影像資料的方法。而我們挑選來實作的功能是 CSS 裏有提供的一個 filter,叫做
invert()(反轉)。這個特效的概念很簡單,就是要讓得到的影像用相反的顏色來表現。實作上也很簡單,首先我們將影像用 RGB 顏色空間表現,也就是說影像上的每個像素點會分別用一個紅色(R)值,一個綠色(G)值以及一個藍色(B)值來表示,每個色彩值會落在 0 到 255 之間。而所謂的反轉一個像素點的顏色,就是用 255 去分別減掉紅綠藍的數值。就這麼簡單!
使用 CSS 來套用特效
如之前所說的,
invert() 是一個 CSS 提供的特效,所以要使用 CSS 來實作該特效很簡單。首先我們先宣告一個 CSS 類別,並且在該類別裡呼叫
invert() 函數(及各個瀏覽器上可能的變形):
.invert {
-webkit-filter: invert(1);
-moz-filter: invert(1);
-o-filter: invert(1);
-ms-filter: invert(1);
filter: invert(1);
}
1234567
.invert { -webkit-filter: invert(1); -moz-filter: invert(1); -o-filter: invert(1); -ms-filter: invert(1); filter: invert(1);}
接著我們再將該類別套用在我們用來秀出結果的
canvas() 元素上就完成了:
canvasResult.classList.add('invert'); // 套用 CSS 特效
1
canvasResult.classList.add('invert'); // 套用 CSS 特效
透過 JavaScript 來實做特效
要透過 JavaScript 來實作反轉特效也很簡單,還記得我們之前介紹的
ImageData 物件嗎?一個
ImageData 裡面有三個性質,分別是影像寬(
ImageData::width ),影像高(
ImageData::height )及影像資料(
ImageData::data )。其中的
ImageData::data 是一個一維的長陣列,每個像素的資料以一組 RGBA 的顏色來表示,每個顏色用一個 unsigned char 來儲存(所以其值落在 0 到 255 之間)。讓我們直接用範例裡的程式碼來解釋如何操作
ImageData 裡的資料:
function processOneFrame() {
ctxOperate.drawImage(video, 0, 0, video.width, video. height);
var imageData = ctxOperate.getImageData(0, 0, video.width, video.height);
// 實作“反轉”特效
var numPixels = video.width video.height; // 算出總共有幾個像素點
for (var i = 0; i < numPixels; ++i) {
var r = imageData.data[4i+0]; // 讀取 R
var g = imageData.data[4i+1]; // 讀取 G
var b = imageData.data[4i+2]; // 讀取 B
r = 255 - r; // 反轉 R
g = 255 - g; // 反轉 G
b = 255 - b; // 反轉 B
imageData.data[4i+0] = r; // 寫入新的 R
imageData.data[4i+1] = g; // 寫入新的 G
imageData.data[4*i+2] = b; // 寫入新的 B
// var a = imageData.data[4*i+3]; // 讀取 A
}
ctxResult.putImageData(imageData, 0, 0);
if (isContinuous) {
requestAnimationFrame(processOneFrame);
} else {
return;
}
}
12345678910111213141516171819202122232425262728
function processOneFrame() { ctxOperate.drawImage(video, 0, 0, video.width, video. height); var imageData = ctxOperate.getImageData(0, 0, video.width, video.height); // 實作“反轉”特效 var numPixels = video.width video.height; // 算出總共有幾個像素點 for (var i = 0; i < numPixels; ++i) { var r = imageData.data[4i+0]; // 讀取 R var g = imageData.data[4i+1]; // 讀取 G var b = imageData.data[4i+2]; // 讀取 B r = 255 - r; // 反轉 R g = 255 - g; // 反轉 G b = 255 - b; // 反轉 B imageData.data[4i+0] = r; // 寫入新的 R imageData.data[4i+1] = g; // 寫入新的 G imageData.data[4i+2] = b; // 寫入新的 B // var a = imageData.data[4i+3]; // 讀取 A } ctxResult.putImageData(imageData, 0, 0); if (isContinuous) { requestAnimationFrame(processOneFrame); } else { return; }}
藉由以上的三個範例,希望大家可以初步的了解到如何在 Web Platform 上實作一個簡單的影像處理應用程式。謀智台客也會努力的撰寫下一篇文章來介紹如何透過 WebGL 來操作 GPU 對影像資料做運算!
Reference
[1] 謀智台客|過年不忘長知識!啥?瀏覽器也可以開視訊會議!
[2] 謀智台客|標準化的錄影API
[3] MDN|CSS filters
