今天和大家講的就是有關(guān)于:“如何利用canvas實現(xiàn)顏色容差摳圖?”這個問題,那么下面我們利用canvas的getImageData,我們可以獲取到一張圖片每一個像素的信息,而通過對每一個像素信息的對比,我們就可以找到需要消去的像素點。
比如下面這一張圖片,如果我們想要扣去白色部分(粉色是body的背景顏色)。
let canvas = document.querySelector('#canvas');
let context = canvas.getContext('2d');
let img = document.createElement('img');
img.src = './head2.png';
img.onload = function () {
canvas.height = img.height;
canvas.width = img.width;
context.drawImage(img, 0, 0);
cutout(canvas, [255, 255, 255], 0.2); // 對白色進行摳除,容差為0.2
}
function cutout(canvas, color, range = 0) {
let context = canvas.getContext('2d');
let imageInfo = context.getImageData(0, 0, canvas.width, canvas.height);
// pixiArr是一個數(shù)組,每四個數(shù)組元素代表一個像素點,這四個數(shù)組元素分別對應(yīng)一個像素的r,g,b,a值。
let pixiArr = imageInfo.data;
for (let i = 0; i < pixiArr.length; i += 4) {
// 匹配到目標(biāo)像素就將目標(biāo)像素的alpha設(shè)為0
if (testColor([pixiArr[i], pixiArr[i + 1], pixiArr[i + 2]], color, range)) pixiArr[i + 3] = 0;
}
context.putImageData(imageInfo, 0, 0);
}
function testColor(current, target, range) {
for (let i = 0; i < 3; i++) {
if (!((1 - range) * target[i] <= current[i] && (1 + range) * target[i] >= current[i])) return false;
}
return true;
}
testColor(current, target, range) 方法三個參數(shù)分別為 待檢測像素點的rgb數(shù)組 、 目標(biāo)像素點的rgb數(shù)組 和 容差范圍 ,這里的容差只是簡單用r、g、b的值分別乘以(1 + range)和(1 - range)來計算并對比,不同的容差參數(shù)會得到不同的效果↓
range = 0.095
range = 0.1
range = 0.2
當(dāng)然對于底色是標(biāo)準(zhǔn)的純色的圖片就不需要容差了。
邊界處理
但是有時候我們希望有一個邊界,讓摳圖操作不對邊界內(nèi)部的像素造成影響。比如上面的圖片,我們希望不會對人物頭像內(nèi)部的像素造成影響。 如果我們一行一行來看,是不是只要在碰到不是邊界像素的時候停止操作,就可以達到效果了呢?
我們對每一行分別進行掃描,定義一個左指針 left 指向這一行的第一個像素,定義一個右指針 right 指向這一行的最后一個像素,并用一個 leftF 標(biāo)識左邊是否碰到邊界,一個 rightF 標(biāo)識右邊是否碰到邊界,當(dāng)沒碰到邊界時指針就一直向內(nèi)收縮,直到兩個指針都碰到邊界或者左右指針重合就跳到下一行,直到所有行都掃描完畢。
function cutout(canvas, color, range = 0) {
let context = canvas.getContext('2d');
let imageInfo = context.getImageData(0, 0, canvas.width, canvas.height);
let pixiArr = imageInfo.data;
for (let row = 0; row < canvas.height; row++) {
let left = row * 4 * canvas.width; // 指向行首像素
let right = left + 4 * canvas.width - 1 - 3; // 指向行尾像素
let leftF = false; // 左指針是否碰到邊界的標(biāo)識
let rightF = false; // 右指針是否碰到邊界的標(biāo)識
while (!leftF || !rightF) { // 當(dāng)左右指針都為true,即都碰到邊界時結(jié)束
if (!leftF) {
if (testColor([pixiArr[left], pixiArr[left + 1], pixiArr[left + 2]], color, range)) {
pixiArr[left + 3] = 0; // 此像素的alpha設(shè)為0
left += 4; // 移到下一個像素
} else leftF = true; // 碰到邊界
}
if (!rightF) {
if (testColor([pixiArr[right], pixiArr[right + 1], pixiArr[right + 2]], color, range)) {
pixiArr[right + 3] = 0;
right -= 4;
} else rightF = true;
}
if (left == right) { // 左右指針重合
leftF = true;
rightF = true;
};
}
}
context.putImageData(imageInfo, 0, 0);
}
雖然大概完成了我們的需求,但是看一下上面頭發(fā)那為啥會多了一塊白色
因為我們僅僅只進行了行掃描,當(dāng)左指針碰到頭發(fā)時就會停止掃描,但是頭發(fā)弧度里面的就無法被掃描到了,我們還需要進行列掃描,改造一下上面的方法:
function cutout(canvas, color, range = 0) {
let context = canvas.getContext('2d');
let imageInfo = context.getImageData(0, 0, canvas.width, canvas.height);
let pixiArr = imageInfo.data;
for (let row = 0; row < canvas.height; row++) {
let left = row * 4 * canvas.width;
let right = left + 4 * canvas.width - 1 - 3;
let leftF = false;
let rightF = false;
while (!leftF || !rightF) {
if (!leftF) {
if (testColor([pixiArr[left], pixiArr[left + 1], pixiArr[left + 2]], color, range)) {
pixiArr[left + 3] = 0;
left += 4;
} else leftF = true;
}
if (!rightF) {
if (testColor([pixiArr[right], pixiArr[right + 1], pixiArr[right + 2]], color, range)) {
pixiArr[right + 3] = 0;
right -= 4;
} else rightF = true;
}
if (left == right) {
leftF = true;
rightF = true;
};
}
}
// 同理進行列掃描
for (let col = 0; col < canvas.width; col++) {
let top = col * 4; // 指向列頭
let bottom = top + (canvas.height - 2) * canvas.width * 4 + canvas.width * 4; // 指向列尾
let topF = false;
let bottomF = false;
while (!topF || !bottomF) {
if (!topF) {
if (testColor([pixiArr[top], pixiArr[top + 1], pixiArr[top + 2]], color, range)) {
pixiArr[top + 3] = 0;
top += canvas.width * 4;
} else topF = true;
}
if (!bottomF) {
if (testColor([pixiArr[bottom], pixiArr[bottom + 1], pixiArr[bottom + 2]], color, range)) {
pixiArr[bottom + 3] = 0;
bottom -= canvas.width * 4;
} else bottomF = true;
}
if (top == bottom) {
topF = true;
bottomF = true;
};
}
}
context.putImageData(imageInfo, 0, 0);
}
至于top和bottom為啥是那樣計算畫個矩陣圖大概就知道了。
處理后↓
其實還可以先將 pixiArr 包裝為以一個像素點為單位的矩陣
[
[{r: 255, g: 255, b: 255, a: 1}, {r: 255, g: 255, b: 255, a: 1}, {r: 255, g: 255, b: 255, a: 1}],
[{r: 255, g: 255, b: 255, a: 1}, {r: 255, g: 255, b: 255, a: 1}, {r: 255, g: 255, b: 255, a: 1}]
[{r: 255, g: 255, b: 255, a: 1}, {r: 255, g: 255, b: 255, a: 1}, {r: 255, g: 255, b: 255, a: 1}]
]
處理后計算像素下標(biāo)也就會更簡單,列掃描時直接先將這個矩陣旋轉(zhuǎn),再用行掃描處理一遍就行了。這樣處理pixiArr也有利于進一步對算法進行優(yōu)化。
上述方法雖然大概完成了摳圖效果,但是這種簡單處理還會有許多情況沒有考慮到。
比如右邊頭發(fā)這里是行掃描和列掃描都無法觸碰到的區(qū)域↓
下面的衣服也因為顏色和底色一樣且沒有邊界在列掃描中被直接抹去了↓
最后
對于主體和底色區(qū)分度很大的圖片來說,最開始的那種方法就已經(jīng)夠用了。這篇中我采用的容差和邊界處理算法的優(yōu)化空間還很大,但是它們是非常容易實現(xiàn)與理解的,這篇權(quán)當(dāng)做一個引子,各位完全可以根據(jù)自己的能力繼續(xù)去實現(xiàn)邊界與容差算法。
總結(jié)
那么在文章的內(nèi)容就是今天我們要和大家分享的關(guān)于:“如何利用canvas實現(xiàn)顏色容差摳圖?”希望對正在學(xué)習(xí)的大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復(fù)大家的。更多的知識我們可以在 W3Cschool中進行學(xué)些和來了解!