本系列如今構思成如下4個部分:html
經過這些積累,我封裝了幾個項目中經常使用的功能:前端
以前文章主要介紹了裁剪/旋轉/合成等基礎類型的圖片處理(文字的合成編寫中...🐒🐒🐒),咱們開始來介紹算法類型的圖片處理技術!~~✈️✈️✈️git
這類型的重點主要在於 算法 和 性能 層面,在前端因爲js
及設備性能的限制,一般表現並不理想。在真正的線上業務中,爲了追求更好的用戶體驗,只能運行一些相對比較輕量級的,性能好的算法。由服務端來進行進行,會是更好的選擇。github
Tips: 因爲我對算法方面並無很深的理解,所以本文主要是一些算法外層及基礎原理的講解,不涉及算法自己。但願你們諒解哈~🙈算法
咱們如下面兩個🌰來作初步的瞭解:chrome
萬聖節效果canvas
這個小應用是一個萬聖節活動。人物臉部的木偶妝容確實很炫酷,可是這裏須要複雜的人臉識別,模型比對以及妝容算法,放在前端性能堪憂,所以讓服務端來處理,顯然是更好的選擇。而邊框和背景圖的模糊處理,這類型的處理就比較適合放在前端了,首先性能能接受,並且更具靈活性,能在不一樣入口隨時替換不一樣的邊框素材。api
對於服務端的妝容算法,因爲我對算法並無深刻研究,在這裏就不班門弄斧了,咱們就直接來梳理下前端的部分:app
Tips: 這裏使用的全是像素級別的算法融合,經過基礎類型的合成,一樣能夠實現。dom
圖片算法處理實質原理實際上是 遍歷像素點,對像素點的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
圖:
一、咱們須要先將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);
複製代碼
二、去除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);
複製代碼
三、圖像融合,這裏用到了一個神奇的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);
複製代碼
最終獲得的效果圖:
最後就可使用這種人像圖與任何背景或者素材根據業務需求再作融合了。
算法部分因爲本人才疏學淺,沒辦法深刻闡述,只能分享一些比較粗淺的經驗,指望能在將來往更底層的領域發展。
不過仍是指望能對你們有所啓發。~~有圖片處理興趣的童鞋,能夠隨時與我探討哈,指望獲得大神的指導,也但願js
能在圖片處理領域有更快的發展。
感謝你們的閱讀。~~~😬😬😬😬。