轉《JavaScript中的圖片處理與合成》
引言:
本系列現在構思成以下4個部分:
通過這些積累,我封裝了幾個專案中常用的功能:
圖片合成 圖片裁剪 人像摳除
之前文章主要介紹了裁剪/旋轉/合成等基礎型別的圖片處理(文字的合成編寫中...???),我們開始來介紹演算法型別的圖片處理技術!~~✈️✈️✈️
這型別的重點主要在於 演算法
js
及裝置效能的限制,通常表現並不理想。在真正的線上業務中,為了追求更好的使用者體驗,只能執行一些相對比較輕量級的,效能好的演算法。由服務端來進行進行,會是更好的選擇。
Tips: 由於我對演算法方面並沒有很深的理解,因此本文主要是一些演算法外層及基礎原理的講解,不涉及演算法本身。希望大家諒解哈~?
我們以下面兩個?來做初步的瞭解:
(一) 萬聖節小應用
效果圖如下:
這個小應用是一個萬聖節活動。人物臉部的木偶妝容確實很炫酷,但是這裡需要複雜的人臉識別,模型比對以及妝容演算法,放在前端效能堪憂,因此讓服務端來處理,顯然是更好的選擇。而邊框和背景圖的模糊處理,這型別的處理就比較適合放在前端了,首先效能能接受,而且更具靈活性,能在不同入口隨時替換不同的邊框素材。
對於服務端的妝容演算法,由於我對演算法並沒有深入研究,在這裡就不班門弄斧了,我們就直接來梳理下前端的部分:
- 傳送原圖給服務端,接受 妝容處理 後的效果圖;
- 下載效果圖後,縮放成合適大小後進行 模糊化處理 ,得到模糊後的結果圖;
- 將結果圖 / 模糊圖 / 邊框進行 畫素級的融合 ;
Tips: 這裡使用的全是畫素級別的演算法融合,通過基礎型別的合成,同樣可以實現。
演算法效能提升
圖片演算法處理實質原理其實是 遍歷畫素點,對畫素點的RGBA值進行改造。對於改造演算法本身,本文就不深入了,不過可以與大家分享下相關的經驗。
眾所周知,一個好的演算法,一個最重要的指標便是效能,而如何提升效能呢?一種是 演算法優化 ,提高迴圈內部的效能或者優化遍歷演算法,演算法中的效能會由於遍歷的存在被放大無數倍。另一種則是 減少畫素點。
畫素點的遍歷是一個演算法的重要效能消耗點,迴圈次數直接決定著演算法的效能。而畫素點的數量與圖片的大小尺寸成正向指數級增長,因此 適當的縮放圖片源後再去處理,對效能的提升十分巨大。例如一張2000*2000
的圖片,畫素點足足有400萬個,意味著需要遍歷400萬次,而把圖片縮小成 800*800
時,迴圈次數為64萬,這裡我做過一個測試:
let st = new Date().getTime();
let imgData = [];
for (let i = 0; i < n * 10000; i += 4) { let r = getRandom(0,255), g = getRandom(0,255), b = getRandom(0,255), a = 1; if (r <= 30 && g <= 30 && b<= 30) a = 0; imgData[i] = r; imgData[i + 1] = g; imgData[i + 2] = b; imgData[i + 3] = a; } let et = new Date().getTime(); let t = et - st; console.log(`${n}萬次耗時:${et - st}ms`, imgData);
測試結果為(mac-chrome-20次取平均):
圖片尺寸 | 畫素數量 | 耗時(ms) | 縮放倍數 | 提升 |
---|---|---|---|---|
2000*2000 |
400萬 | 168 | 1 | 0% |
1600*1600 |
256萬 | 98 | 0.8 | 42% |
1200*1200 |
144萬 | 64 | 0.6 | 62% |
800*800 |
64萬 | 32 | 0.4 | 81% |
400*400 |
16萬 | 10 | 0.2 | 94% |
可以看出圖片的縮小,對效能有非常顯著的提升。這裡有個特點,效能收益會隨著縮放係數的變大而越來越低,當縮放係數為0.8時,效能已經大大提升了42%,而繼續縮放為0.6時,收益便開始大幅降低,只提升了20%。同時縮放圖片意味著質量的下降,所以這裡需要尋找一個 平衡點 ,在不影響結果圖效果的前提下,儘可能地提升效能,這需要根據演算法對圖片質量的要求來定。
另外,對 原圖的裁剪也是個很好的辦法,裁剪掉多餘的背景部分,也能達到減少遍歷次數,提升效能的效果。
模糊演算法
小應用中模糊部分使用的是 StackBlur.js
的模糊演算法,應用程式碼如下:
// 縮放妝容圖;
let srcImg = scaleMid(imgData);
// 建立模糊結果圖的容器;
let blurCvs = document.createElement('canvas'), blurCtx = blurCvs.getContext('2d'); // 先複製一份原圖資料,; let blurImg = blurCtx.createImageData(srcImg.width, srcImg.height); let size = srcImg.width * srcImg.height * 4; for (let i = 0; i < size; i++) { blurImg.data[i] = srcImg.data[i]; } // 縮放成400*400的大小; blurImg = scale(blurImg, 400); // 進行模糊處理; StackBlur.imageDataRGBA(blurImg, 0, 0, blurImg.width, blurImg.height, 1); // 處理完後再放大為800*800; blurImg = scale(blurImg, 800);
影象融合
我們已經準備好合成最終效果圖的所有素材了,模糊背景 / 妝容圖 / 邊框素材,最後一步便是將三者進行融合,融合的原理是 根據最終效果圖分割槽域,在不同區域分別填入對應的素材資料:
// 圖片融合
function mix(src, blur, mtl) { // 最終結果圖為固定800*800;縱向800的資料; for (let i = 0; i < 800; i++) { let offset1 = 800 * i; // 橫向800的資料; for (let j = 0; j < 800; j++) { let offset = (offset1 + j) * 4; // 在特定的位置填入素材; if (i <= 75 || i >= 609 || j <= 126 || j >= 676) { let alpha = mtl.data[offset + 3] / 255.0; mtl.data[offset] = alpha * mtl.data[offset] + (1 - alpha) * blur.data[offset]; mtl.data[offset + 1] = alpha * mtl.data[offset + 1] + (1 - alpha) * blur.data[offset + 1]; mtl.data[offset + 2] = alpha * mtl.data[offset + 2] + (1 - alpha) * blur.data[offset + 2]; mtl.data[offset + 3] = 255; } else { let alpha = mtl.data[offset + 3] / 255.0; let x = i - 75; let y = j - 126; let newOffset = (x * 550 + y) * 4; mtl.data[offset] = alpha * mtl.data[offset] + (1 - alpha) * src.data[newOffset]; mtl.data[offset + 1] = alpha * mtl.data[offset + 1] + (1 - alpha) * src.data[newOffset + 1]; mtl.data[offset + 2] = alpha * mtl.data[offset + 2] + (1 - alpha) * src.data[newOffset + 2]; mtl.data[offset + 3] = 255; } } } return mtl; }
(二) 摳除人像
這是一個基於服務端的人像mask
層,在前端把人像摳出的服務,這樣便可以進一步做背景的融合和切換,現在已經用在多個線上專案中了。
人像摳除)
這裡需要基於由服務端處理後的兩張效果圖:
帶背景的結果圖和mask
圖:
1、我們需要先將mask
圖進行處理:
// 繪製mask;
// mask_zoom: 既為了優化效能所做的縮放係數;
mask = document.createElement('canvas');
maskCtx = mask.getContext('2d'); mask.width = imgEl.naturalWidth * ops.mask_zoom; mask.height = imgEl.naturalHeight * ops.mask_zoom / 2; maskCtx.drawImage(imgEl, 0, - imgEl.naturalHeight * ops.mask_zoom / 2, imgEl.naturalWidth * ops.mask_zoom , imgEl.naturalHeight * ops.mask_zoom);
2、去除mask
的黑色背景,變成透明色,這裡需要用到畫素操作:
// 獲取圖片資料;
let maskData = maskCtx.getImageData(0, 0, mask.width, mask.height);
// 遍歷改造畫素點,將接近黑色的點的透明度改成0; for (let i = 0; i < data.length; i += 4) { let r = data[i], g = data[i + 1], b = data[i + 2]; if (r <= 30 && g <= 30 && b<= 30)data[i + 3] = 0; } // 將改造後的資料重新填回mask層中; maskCtx.putImageData(maskData, 0, 0);
3、影象融合,這裡用到了一個神奇的canvas
方法,相信大家聽過,但並不熟悉 --- globalCompositeOperation
,該值可以修改canvas
的融合模式,有多種融合模式大家可以自行研究,這裡使用的是source-in
;
// 建立最終效果圖容器;
result = document.createElement('canvas');
resultCtx = result.getContext('2d');
result.width = imgEl.naturalWidth;
result.height = imgEl.naturalHeight;
// 先繪製mask圖層做為背景; resultCtx.drawImage(mask, 0, 0, imgEl.naturalWidth, imgEl.naturalHeight); // 修改融合模式 resultCtx.globalCompositeOperation = 'source-in'; // 繪製帶背景的結果圖 resultCtx.drawImage(origin, 0, 0);
最終得到的效果圖:
最後就可以使用這種人像圖與任何背景或者素材根據業務需求再做融合了。