C++ 編寫 WebAssembly初探(二)
上一篇(環境搭建,簡單接入): C++編寫WebAssembly初探
這一次,我們嘗試使用WebAssembly來做簡單的圖片處理。
我們選取一種最基本的影象處理——高斯模糊來嘗試實現。原理可參考 高斯模糊和卷積濾波簡介
js向wasm傳遞陣列
與傳遞number不同,傳遞陣列時,需要js將陣列拷貝到 wasm記憶體 中,並通過傳遞指標(資料在記憶體中的位置),讓wasm通過訪問記憶體的具體位置,來獲取或修改陣列。
另外,不同於js,wasm的記憶體管理由開發者進行控制,我們需要手動分配和釋放記憶體。
這裡的過程是,首先我們獲得表示圖片畫素的陣列,將這個陣列複製到wasm記憶體,再呼叫wasm模組處理這些畫素資料,處理完後js重新讀取這塊記憶體,並將處理過的圖片畫到canvas上。
// 被處理的圖片 const srcImg =document.getElementById('srcImg'); srcImg.onload = () => { // onload時將圖片畫到canvas上,以獲得畫素資料 const { clientWidth = 0, clientHeight = 0 } = srcImg; var canvas = document.getElementById("drawerCanvas"); canvas.width = clientWidth; canvas.height = clientHeight; var ctx = canvas.getContext("2d"); ctx.drawImage(srcImg, 0, 0, clientWidth, clientHeight); // 獲得畫素資料 const imageData = ctx.getImageData(0, 0, clientWidth, clientHeight); // 處理資料 const resImageData = wasmProcess(imageData, clientWidth, clientHeight); // 將處理後的圖片資料畫到canvas上 ctx.putImageData(resImageData, 0, 0); } // 將js的typedarray複製到wasm的堆記憶體 function copyToHeap(typedArray) { const numBytes = typedArray.byteLength; const ptr = Module._malloc(numBytes); const heapBytes = new Uint8Array(Module.HEAPU8.buffer, ptr, numBytes); heapBytes.set(new Uint8Array(typedArray.buffer)); return heapBytes; } // 釋放一塊wasm記憶體 function freeHeap(heapBytes) { Module._free(heapBytes.byteOffset); } // 圖片處理的函式 function wasmProcess(imgData, width, height) { const heapBytes = copyToHeap(imgData.data); // 呼叫c++暴露的方法。其中heapBytes.byteoffset傳遞的是wasm記憶體中陣列的指標 Module.ccall( 'easyBlur', 'number', ['number', 'number', 'number', 'number', 'number'], [heapBytes.byteOffset, width, height, 3, 3] ); // 從wasm記憶體讀取出處理後的資料 const newData = new Uint8ClampedArray(heapBytes); // 釋放wasm記憶體 freeHeap(heapBytes); const newImageData = new ImageData(newData, width, height); return newImageData; }
簡單的高斯模糊演算法實現
這裡取最簡單的濾波器,即矩陣所有項都相等的濾波器。要使得濾波器的各項和為1,則每一項的值為1 / (cw*ch).
如一個3*3的濾波器為 [0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11].我們可以簡單地通過改變cw和ch來調整模糊的強度,cw和ch越大,擴散程度越大,則模糊強度也越大。
另外我們需要觀察ctx.getImageData()得到的陣列格式:獲得的data是一個一維陣列,按照從從左到右,從上到下的順序記錄了圖片每個畫素的值。其中每4個值為一組,分別代表同一個畫素的r, g, b, a四個通道的數值。我們模糊時對每個通道進行單獨處理。
我的程式碼:
#include <cstdint> #include <cmath> // 卷積操作,傳入imageData畫素陣列的指標,imageData寬高,濾波器及濾波器寬高。 void conv(uint8_t *ptr, int width, int height, float* filter, int cw, int ch) { for (int i = ch / 2; i < height - ceil((float)ch / 2) + 1; i++) { for (int j = cw / 2; j < width - ceil((float)cw / 2) + 1; j++) { // rgba取前3個通道進行處理 for (int k = 0; k < 3; k++) { float sum = 0; int count = 0; for (int x = -ch / 2; x < ceil((float)ch / 2); x++) { for (int y = -cw / 2; y < ceil((float)cw / 2); y++) { sum += filter[count] * (float)ptr[((i+x)*width+(y+j))*4+k]; count++; } } ptr[(i*width+j)*4+k] = (uint8_t)sum; } } } } #ifdef __cplusplus extern "C" { #endif // 供js呼叫的函式,傳入畫素陣列的指標,寬高,以及濾波器的寬高 // 這裡為了簡單,預設濾波器矩陣每一項的值相同,即1/(cw*ch)。 void easyBlur(uint8_t *ptr, int width, int height, int cw = 3, int ch = 3) { float* filter = new float[cw * ch]; float value = 1 / (float)(cw * ch); for (int i = 0; i < cw * ch; i++) { filter[i] = value; } conv(ptr, width, height, filter, cw, ch); delete [] filter; } #ifdef __cplusplus } #endif
效果預覽
對於寬度200px左右的圖片,使用長寬為5的濾波器效果如下:
瓶頸
使用js以相同的方法重新實現了一次,發現在圖片較小時js處理的耗時更短,而圖片較大時wasm雖然速度快於js,但處理的時間也非常長,是不能忍受的。
問題的原因很可能是將資料在js記憶體和wasm記憶體之間複製消耗大量的時間,影響效能。所以這種資料量非常大的場景下,wasm雖然優化了計算的時間,但因為資料傳遞的時間大大增加,反而成為了效能的瓶頸。
另外,對於前端來說,自己實現相關的處理演算法,效能遠不如線上一些庫優化得好。
這裡有更多前端可用的 圖片處理庫 。