iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 23
2
Modern Web

用 Javascript 當個影像魔術師系列 第 23

Day 23 - Canvas 效能調整 - Webassembly (下)

濾鏡效能調整

進行到這邊之後,目前依照我的電腦情況,來記錄一下目前最耗時的銳利化濾鏡平均需要耗費的處理時間 ( 單一個濾鏡效果做測試 )

濾鏡 時間( 毫秒 100次平均 )
銳利化 65

而因為這個的確是需要大量計算,所以看起來很適合交給 Webassembly 來計算,那接下來就開始吧!

Rust & Webassembly

前面有介紹到在 Rust 裡面使用時,我們有使用 #[wasm_bindgen] 這種用法

#[wasm_bindgen]
pub fn fib(i: u32) -> u32 {
    match i {
        0 => 0,
        1 => 1,
        _ => fib(i-1) + fib(i-2)
    }
}

這個主要是告訴打包過程中,這個 function 將會要匯出被使用,所以可以看出最終打包的成果

index_bg.wasm 會是之前說的二進制的程式,而 index.js 則是定義了有哪些 function 可以使用

/**
* @param {number} i
* @returns {number}
*/
export function fib(i) {
    const ret = wasm.fib(i);
    return ret >>> 0;
}

所以如果沒有加上 #[wasm_bindgen] 的話,是不會出現在這裡的。而除了幫我們匯出之外,因為目前原生的 Webassembly 只支援了 int32int64float32float64,這幾種型式,也就是說如果我們想跟 JS 使用不同型態值的話會遇到很大的麻煩,因此透過 #[wasm_bindgen] 會幫我們產生可以跟 JS 的程式碼,讓我們專注在邏輯上。

詳細 Webassembly 與瀏覽器交互可參考這篇文章

另外我們還會需要使用 web_sys 來讓我們可以傳入原生的 Html Element 以及使用 js_sys 來使用 JS 的原生型別,所以加入在 cargo.toml 加入

[dependencies.web-sys]
version = "0.3.4"
features = [
  'ImageData',
  'console'
]

[dependencies.js-sys]
version = "^0.3"

這樣我們就可以來實際寫我們的 Rust 程式碼了,首先宣告一些我們該使用的東西


extern crate wasm_bindgen;

use wasm_bindgen::prelude::*;

use wasm_bindgen::Clamped;

use web_sys::{ ImageData, console };

use js_sys::Math:: { sqrt, floor };

接著把原本用 Rust 實作一次,這邊卡了蠻久的,主要是因為需要定義好各種型別,為了想跟 JS的程式碼盡可能相似,所以會發現我在下面一直有做強制轉型的動作,不然應該會有更好的寫法。另外要注意的是要記的超出範圍的問題,因為 Rust 在轉成 u8 的形式時,超出範圍的不會回歸到該範圍最大值,如果是 260 轉成 255 時,結果會變成 5 ,所以下面要自己實作 clamp 的操作。

#[wasm_bindgen]
     pub fn convolve(val: ImageData, kernel: &[i16], amount: f32) -> std::result::Result<web_sys::ImageData, wasm_bindgen::JsValue>{
         let imageHeight: u32 = val.height();
         let imageWidth: u32 = val.width();
         let pixelData  = val.data();
         let side: u32 = sqrt(kernel.len() as f64) as u32;
         let half: u32 = floor((side / 2).into()) as u32;
         let mut outputPixelData = val.data().clone();
         for y in 0..imageHeight {
             for x in 0..imageWidth {
                 let dstOff = (y * imageWidth + x) * 4;
                 let mut totalR = 0;
                 let mut totalG = 0;
                 let mut totalB = 0;
                 for row in 0..side {
                     for col in 0..side {
                         let srcY = y + row - half;
                         let srcX = x + col - half;
                         if srcY < 0 || srcY >= imageHeight || srcX < 0 || srcX >= imageWidth {
                                continue
                         }
                        let srcOff = (srcY * imageWidth + srcX) * 4;
                        let weight = kernel[(row * side + col) as usize];
                        let [r, g, b] = [
                        pixelData[srcOff as usize],
                        pixelData[(srcOff + 1) as usize],
                        pixelData[(srcOff + 2) as usize]
                        ];
                        totalR += r as i16  * weight;
                        totalG += g as i16  * weight;
                        totalB += b as i16  * weight;
                     }
                 }
                if amount > 0.0 {
                    outputPixelData[dstOff as usize] =
                        clamp(totalR as f32 * amount + outputPixelData[dstOff as usize] as f32 * (1.0 - amount), 0.0, 255.0 );
                    outputPixelData[(dstOff + 1) as usize] =
                         clamp(totalG as f32 * amount + outputPixelData[(dstOff + 1) as usize] as f32 * (1.0 - amount), 0.0, 255.0 );
                    outputPixelData[(dstOff + 2) as usize] =
                         clamp(totalB as f32 * amount + outputPixelData[(dstOff + 2) as usize] as f32 * (1.0 - amount), 0.0, 255.0 );
                    }
             }
         }
         ImageData::new_with_u8_clamped_array_and_sh(Clamped(&mut outputPixelData), imageWidth, imageHeight)
    }
    

fn clamp(input: f32, min: f32, max: f32) -> u8 {
    if input > max {
    max as u8
    }
    else if input < min {
        min as u8
    } else {
        input as u8
    }
}

最後沒問題的話一樣在原本的 function 裡面使用,測試結果如下

濾鏡 時間( 毫秒 100次平均 )
銳利化 42

目前結果也大約是快了將近 30%,雖然還是沒辦法達成 60 fps,的目標,但也是縮短了許多計算時間。但值得注意的是在把其他計算量比較低的濾鏡如曝光度改成使用 Webassembly ,實際上耗費的時間甚至會比原本還慢一點點,因為在溝通上也是會需要耗上一些額外的成本,所以在使用上也不是所有的計算都交給 Webassembly ,要評估一下。

目前在引用到 Worker 的時候 Webpack 一直會出錯,所以這邊就沒做在畫面上,如果有興趣使用請自行將程式碼手動開啟

小結

列城的俯瞰照片,在馬路上其實都是塵土飛揚,車子經過都是滿滿的風沙

優化的部分到這邊總算結束了,而當初設定的兩個目標

  1. 將複雜的計算任務移到其他線程去處理,讓主線程一直保持著良好的互動狀態 -> 我們將計算移到 Worker,並且直接使用 OffscreenCanvasImageBitmapWorker 中渲染,大約省了 20% 的時間,並且維持主線程 UI 不卡頓
  2. 減少計算時間 -> 我們使用了 Rust 實作 Webassembly,在特定濾鏡上大約省了 30% 的時間

雖然期待最終完整版本應該是用 WebassemblyWorker 中直接渲染,但是因為一直找不到設定哪裡出錯,所以就先跳過了。也希望在 Webpack 5 發布後,未來這些用法也會更佳的直覺,不會現像在一樣需要一堆設定。接下來應該會介紹一些其他影像相關的 library,明天見!


上一篇
Day 22 - Canvas 效能調整 - Webassembly (上)
下一篇
Day 24 - Canvas 常用套件 - fabric js 介紹
系列文
用 Javascript 當個影像魔術師30

尚未有邦友留言

立即登入留言