昨天介紹了 ImageData
的用法,今天來寫個範例,看看如何在 Web worker 中處理 ImageData
,並增進效能吧。
ImageData
操作圖片的個別像素,讓圖片的外圍區域變的透明,最後渲染在 canvas
上。ImageData
的資料型別 Uint8ClampedArray
是一種 TypeArray
,所以我們可以使用其中的 buffer 屬性 取得 ArrayBuffer
,接著將其 轉移(transfer) 到 worker 裡對個別像素進行處理,這部分是希望能利用 Web worker 增進效能的地方。ImageData
的處理,其中一個丟到 worker 處理,另一個單純在主線程運行,最後希望比較兩者之間的差異首先我們先看點擊 使用 worker 及 不使用 worker 按鈕時執行的程式碼,主要是根據點擊的按鈕執行操作 ImageData
的函數,並且在執行前後使用 performance.now()
計算函數執行時間,之後分別執行 renderCanvas
將處理過後的 canvas
畫在畫面上,以及 renderTime
將執行程式的時間顯示在畫面上。
// 根據點擊的按鈕執行操作 ImageData 的函數
const mapClassNameToFunction = {
worker: makeImageBoundaryTransparentWithWorker,
'no-worker': makeImageBoundaryTransparent
};
Array.from(document.querySelectorAll('button')).forEach((button) => {
button.addEventListener('click', async (e) => {
const originImg = document.querySelector('.origin');
// 獲取原本圖片的 url
const imageUrl = originImg.src;
// 透明度
const boundingOpacity =
document.querySelector('.bounding-opacity').value || 0.5;
// 圖片外框影響比例
const boundingRatio =
document.querySelector('.bounding-ratio').value || 0.05;
// 操作 ImageData 函數
const fn = mapClassNameToFunction[e.target.className];
const start = performance.now();
const { canvas, time } = await fn(imageUrl, {
boundingOpacity,
boundingRatio
});
// 將處理過後的 canvas 畫在畫面上
renderCanvas(canvas);
const end = performance.now();
// 計算總耗時
time.total = Math.round(end - start);
// 將執行程式的耗時顯示在畫面上
renderTime(time);
});
});
由於 使用 worker 或是 不使用 worker 操作 ImageData
的邏輯是一樣的,所以下面只說明使用 worker 時執行的邏輯
將原圖使用 ctx.drawImage
畫在 canvas
上後,使用 ctx.getImageData
拿出 ImageData
,由於 getImageData
是個耗時的操作,所以也測量使用 getImageData
前後所花費的時間
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const time = {};
// 測量 drawImage, getImageData 的耗時
start = performance.now();
// 在 canvas 上畫出 img 圖片
ctx.drawImage(img, 0, 0, width, height);
// 取出 ImageData
const imageData = ctx.getImageData(0, 0, width, height);
end = performance.now();
// getImageData 執行時間
time.getImageData = Math.round(end - start);
由於 ImageData
是 TypeArray
型別,可以使用其中的 buffer
屬性將其 轉移 (transfer) 到 worker
中進一步處理,這裡的 option
包含了 boundingOpacity: 調整透明度 及 boundingRatio: 調整圖片外框影響比例
worker.postMessage({ imageData, option }, [imageData.data.buffer]);
在 worker
線程中會執行 makeImageDataTransparent
遍歷 ImageData
中的像素點,改變圖片外圍像素點的透明度,然後再將算完的 newImageData
回傳給主線程,此時會以 newImageDataTime
儲存將圖片外圍像素變透明所耗費的時間
// worker 線程
self.onmessage = async (e) => {
const { imageData, option } = e.data;
const start = performance.now();
// newImageData 是操作後,圖片外圍像素已經變透明的
const newImageData = await makeImageDataTransparent(imageData, option);
const end = performance.now();
self.postMessage({ newImageData, newImageDataTime: end - start }, [
newImageData.data.buffer
]);
};
最後將處理過後的 newImageData
利用 ctx.putImageData
更新回 canvas
上,這裡也一樣測量了執行 ctx.putImageData
所耗費的時間
// 主線程
worker.onmessage = (e) => {
const { newImageData, newImageDataTime } = e.data;
time.imageDataTime = Math.round(newImageDataTime);
start = performance.now();
ctx.putImageData(newImageData, 0, 0);
end = performance.now();
time.putImageData = Math.round(end - start);
};
以下使用 Chrome 瀏覽器呈現
不使用 worker
可以看到每次點擊按鈕後,紅色方塊的動畫都會有明顯的停頓,主要是 操作圖片像素 (ImageData) 花費的時間約 100ms,此時會佔用主線程的資源,造成畫面卡頓
使用 worker
可以看到第一次點擊按鈕時,會有些許卡頓,主要是 取得圖片像素 (getImageData) 時花費約 80ms 的時間導致,但第二次後的點擊按鈕,即使 操作圖片像素 (ImageData) 的耗時約 100ms,但因為這段的運算資源移到 worker 中處理了,所以不會感到紅色方塊有所卡頓
原本單純在主線程中運算 ImageData
操作圖片中的畫素,大約會花費 100ms 的時間處理,所以導致動畫有卡頓的狀況,而將這部分的運算移到 worker 處理後,因為不再佔用主線程資源,可以發現按下按鈕後的動畫是順暢的。
各位可以修改程式碼把 index.html 中改為渲染高解析度的圖片,這時會發現即使使用 worker,動畫卡頓的問題一樣會出現,原因是圖片解析度變高時,getImageData
執行的時間會大幅上升,而 getImageData
並沒有移到 worker
中處理,所以依舊會造成卡頓。
這部分我們希望嘗試使用 OffscreenCanvas,OffscreenCanvas
可以將 DOM
與圖像操作的邏輯解耦,做到在 worker
中呼叫 getImageData
方法,那麼明後天我們將會嘗試使用 OffscreenCanvas
優化處理高解析度圖片的狀況
execution.js
中有一行程式碼:const img = new Image();
img.crossOrigin = "Anonymous";
為什麼這裡需要設定圖片的 crossOrigin
屬性呢?大家可以試試看移除這行後,會直接丟出錯誤 SecurityError Failed to execute 'getImageData' on 'CanvasRenderingContext2D': The canvas has been tainted by cross-origin data.
原因是任何圖片的來源都可能是 跨域(CORS) 的,在自己的網站獲取其他網站中的圖片並打算操作 圖片的像素(ImageData) 時是會有安全性問題的,所以當沒有設定 crossOrigin
時,瀏覽器就會丟出安全性錯誤,而將 crossOrigin
設定成 "Anonymous"
後,會告知瀏覽器說,這個圖片我打算用 CORS
的方式獲取,那麼這時候就會觸發瀏覽器的 CORS
檢查機制,也就是圖片的 response header
必須回傳相關跨域設定:
Access-Control-Allow-Origin "*"
讓瀏覽器知道這個圖片是允許被跨域存取的,如此才有辦法使用 getImageData
方法獲取圖片中的每個像素值
Allowing cross-origin use of images and canvas
How to fix getImageData() error The canvas has been tainted by cross-origin data?
getImageData
時,速度明顯比第二次之後執行 getImageData
慢很多,似乎是因為 Chrome 瀏覽器目前都用 GPU 處理所有跟 canvas
相關的操作,通常來說使用 GPU 對 canvas
的各種操作都會快於 CPU,但有一個例外是 GPU 運行 getImageData
的速度明顯會慢於 CPU,所以 Chrome 決定將第一個 getImageData
的呼叫在 GPU 上運行,而後續的 getImageData
呼叫都在 CPU 上運行,於是就導致了第一次呼叫時速度很慢,但之後就都變快的神奇現象