背景:有一天接到一個小遊戲,裡面有一個部分是 一起來找茬,一開始準備用設計給的座標來寫,但是發現好像不太符合程式設計師愛鑽研的精神,於是就想著做一個能自動識別的,幾經周折,後來決定用 canvas的畫素來處理這個問題。
熟悉API
在處理圖片找茬前,先囉嗦一下,canvas畫素處理裡面最重要的兩個API ctx.getImageData
和ctx.putImageData
,前者負責獲取canvas畫素資訊,後者負責把畫素資訊繪製到canvas畫布上。
處理畫素前,首先得在畫布上 畫寫東西,我們這裡就以畫兩個圖片為例,如下:
1.繪製圖片
ctx1.drawImage(img1, 0, 0, img1.width, img1.height, 0, 0, cavsW, cavsH);
ctx2.drawImage(img2, 0, 0, img2.width, img2.height, 0, 0, cavsW, cavsH);
複製程式碼
2.獲取畫素的API ctx.getImageData
MDN上的解釋是:
CanvasRenderingContext2D.getImageData()
返回一個ImageData物件,用來描述canvas區域隱含的畫素資料,這個區域通過矩形表示,起始點為(sx, sy)、寬為sw、高為sh。;
sx
: 將要被提取的影象資料矩形區域的左上角 x 座標。
sy
: 將要被提取的影象資料矩形區域的左上角 y 座標。
sw
: 將要被提取的影象資料矩形區域的寬度。
sh
: 將要被提取的影象資料矩形區域的高度。
返回值
一個ImageData
物件,包含canvas給定的矩形影象資料。其中,
ImageData.data
: Uint8ClampedArray 描述了一個一維陣列,包含以 RGBA 順序的資料,資料使用 0 至 255(包含)的整數表示。
ImageData.height
: 無符號長整型(unsigned long),使用畫素描述 ImageData 的實際高度。
ImageData.width
: 無符號長整型(unsigned long),使用畫素描述 ImageData 的實際寬度。
下面,以一個寬高分別為750
和400
的canvas畫布為例:
ctx.getImageData(x,y, caves.width, canvas.height);
// 獲取的是一個包含畫素資訊的物件,如下
ImageData = {
data: Uint8ClampedArray(1200000), // 4 * 750 * 400
width: 750,
height: 400
}
複製程式碼
由於ImageData.data是一維陣列,所以我們需要把canvas的畫素平鋪到一行,對應如下下面為canvas中座標對應的的下標值的對應示意圖
若點A座標為 (x,y),canvas畫布的寬度為width,則A的四個rgba資訊是為第[n, n + 3]個
// 把二維座標座標轉成一緯的序號
n = y * width + x;
A.R = 4n
A.G = 4n + 1
A.B = 4n + 2
A.A = 4n + 3
複製程式碼
3. 繪製畫素資訊到 canvas畫布的API,ctx.putImageData
。
對於ctx.putImageData
, MDN上的解釋是:
CanvasRenderingContext2D.putImageData()
是 Canvas 2D API 將資料從已有的 ImageData 物件繪製到點陣圖的方法。 如果提供了一個繪製過的矩形,則只繪製該矩形的畫素。此方法不受畫布轉換矩陣的影響。
void ctx.putImageData(imagedata, dx, dy);
void ctx.putImageData(imagedata, dx, dy, dirtyX, dirtyY, dirtyWidth, dirtyHeight);
複製程式碼
引數:
ImageData
: 包含畫素值的陣列物件。
dx
: 源影象資料在目標畫布中的位置偏移量(x 軸方向的偏移量)。
dy
: 源影象資料在目標畫布中的位置偏移量(y 軸方向的偏移量)。
dirtyX
: (可選) 在源影象資料中,矩形區域左上角的位置。預設是整個影象資料的左上角(x 座標)。
dirtyY
: (可選) 在源影象資料中,矩形區域左上角的位置。預設是整個影象資料的左上角(y 座標)。
dirtyWidth
: (可選) 在源影象資料中,矩形區域的寬度。預設是影象資料的寬度。
dirtyHeight
: (可選) 在源影象資料中,矩形區域的高度。預設是影象資料的高度。
如果在畫素處理前後,寬高和個數不變,則可以直接,像下面那樣使用
//把imageData2 從左上角繪製繪製,由於大小一樣,因此後面的引數可不屑
ctx.putImageData(imgData2, 0, 0);
複製程式碼
4. 顯示器上的畫素:
畫素的基本使用
理論上我們拿到畫素,我們可以對圖片進行各種操作,下面看看幾個簡單的例子。
在所有動作開始前,先獲取到畫布
let cavs1 = this.$refs.canvas1;
let cavs2 = this.$refs.canvas2;
let ctx1 = cavs1.getContext("2d");
let ctx2 = cavs2.getContext("2d");
let cavsWidth = this.cavsW;
let cavsHeight = this.cavsH;
let imgData1 = ctx1.getImageData(0, 0, cavsWidth, cavsHeight);
// 這一部,處理畫素
let imgData2 = dealImageData(imgData1);
// 處理畫素後,繪製到canvas畫布上
ctx2.putImageData(imgData2, 0, 0);
複製程式碼
當上面的dealImageData
為以下函式方法時,各個效果如下面所示
注意:以下的圖,左邊代表處理前,右邊處理後
- 畫素全部取反色
setReverseColor(imageData) {
let d = imageData.data;
for (let i = 0; i < d.length; i += 4) {
d[i] = d[i] ^ 255;
d[i + 1] = d[i + 1] ^ 255;
d[i + 2] = d[i + 2] ^ 255;
d[i + 3] = d[i + 3] ^ 255;
}
return imageData;
}
複製程式碼
效果如下
- 下面,我們可以在 RGBA四個顏色通道上做處理,看下效果
由於每個畫素有有四個數值標示,所以,如果點A為第n個畫素,則點A在畫素imageData上的位置為,
A.R = 4 * n
A.G = 4 * n + 1
A.B = 4 * n + 2
A.A = 4 * n + 3
複製程式碼
為了取值直觀一些,我封裝了一個可以更具座標獲取當前畫素點畫素資訊的函式,如下:
/**
* 傳入座標,返回當前畫素的畫素資訊
* @param {number} x 橫座標
* @param {number} y 縱座標
* @param {Object} imageData 畫素資訊
* @return {Array} 當前座標的畫素資訊
*/
export const getPixelInfo = (imageData, x, y) => {
let R = y * imageData.width * 4 + 4 * x;
let G = R + 1;
let B = R + 2;
let A = R + 3;
let orderArr = [R, G, B, A];
let pixelInfo = {
R,
G,
B,
A,
orderArr
};
return pixelInfo;
}
複製程式碼
紅色通道(R)設定為255(或者0),程式碼和效果如下
setSingleColor(imageData, item) {
let d = imageData.data;
for (let i = 0; i < d.length; i += 4) {
d[i] = 255;
//d[i] = 0;
}
return imageData;
}
複製程式碼
R = 255 效果:
R = 0 效果:
綠色通道(G)設定為255(或者0),程式碼和效果如下
setSingleColor(imageData, item) {
let d = imageData.data;
for (let i = 0; i < d.length; i += 4) {
d[i+1] = 255;
//d[i+1] = 0;
}
return imageData;
}
複製程式碼
G = 255 效果:
G = 0 效果:
藍色通道(B)設定為255(或者0),程式碼和效果如下
setSingleColor(imageData, item) {
let d = imageData.data;
for (let i = 0; i < d.length; i += 4) {
d[i+2] = 255;
//d[i+2] = 0;
}
return imageData;
}
複製程式碼
B = 255 效果:
B = 0 效果:
透明值(A)設定為255(或者0),程式碼和效果如下
setSingleColor(imageData, item) {
let d = imageData.data;
for (let i = 0; i < d.length; i += 4) {
d[i+3] = 255;
//d[i+3] = 0;
}
return imageData;
}
複製程式碼
A = 255 效果:
A = 0 效果,(相當於透明度為0,因此啥都看不到)
實現原理:
獲取canvas畫布的所有畫素,設定一個固定的掃描區域(長和寬都是R的矩形),然後按照從左往右,從上往下的順序掃描,每經過一個區域的時候,計算出當前區域畫素值不同的個數,連帶當前區域的座標等資訊一起存到一個叫diffPoints的陣列中,然後遍歷陣列就可以查出來圖片不同的區域
;
大體步驟:
- 建立兩個畫布,把需要比對的兩個圖片畫到畫布上。
- 獲取到兩個畫布的畫素資訊,然後遍歷比對他們的差異,並統計他們的座標等差異資訊
大概如下圖
以下面的圖片為例,
掃描他們不同的,過程示例如下
圖中:外面的矩形,代表掃描的區域。裡面的數字代表的是當前區域各個畫素值(每個畫素點有四個)不同的個數和的平方根,之所以求平方,是因為有的數太大顯示不全。
接下來,看看核心程式碼部分,也就是尋找差異的部分
calcArea() {
//計算不同點
let ctx1 = cavsDom1.getContext("2d");
let ctx2 = cavsDom2.getContext("2d");
//獲取畫素資訊
let imgData1 = ctx1.getImageData(0, 0, cavsW, cavsH).data;
let imgData2 = ctx2.getImageData(0, 0, cavsW, cavsH).data;
// 陣列用來儲存各個區域畫素資訊
this.diffPoints = [];
for (let h = 0; h < cavsH - scanR / 2; h += scanStep) {
for (let i = 0; i < cavsW - scanR / 2; i += scanStep) {
//當前區域不同畫素值的個數,(i,h) 即當前區域塊左上角畫素點的座標值
let diffNum = 0;
// 當前區第一個點的下標
let pIndex = h * cavsW * 4 + i * 4;
// 區域內部遍歷畫素值,統計該區域不同畫素的個數
for (let j = 0; j < scanR; j++) {
for (let k = 0; k < scanR * 4; k++) {
let data1 = imgData1[pIndex + j * cavsW * 4 + k];
let data2 = imgData2[pIndex + j * cavsW * 4 + k];
//通過設定容差來判斷是不同色值個數
if ((data1 - data2) ** 2 > 400) {
diffNum++;
}
}
}
// 獲取當前區域中心點的座標
let x = Math.round(i + 0.5 * scanR);
let y = Math.round(h + 0.5 * scanR);
// 虛擬座標
let vX = i;
let vY = h;
this.diffPoints.push({diffNum, x, y, vX, vY});
}
}
},
複製程式碼
為了更直觀一點,我們借用一下上面封裝好的
getPixelInfo
方法,這樣取畫素值更直觀一點
calcArea() {
//計算不同點
let ctx1 = cavsDom1.getContext("2d");
let ctx2 = cavsDom2.getContext("2d");
let imgData1 = ctx1.getImageData(0, 0, cavsW, cavsH);
let imgData2 = ctx2.getImageData(0, 0, cavsW, cavsH);
this.diffPoints = [];
for (let h = 0; h < cavsH - scanR / 2; h += scanStep) {
for (let i = 0; i < cavsW - scanR / 2; i += scanStep) {
let diffNum = 0;
for (let j = 0; j < scanR; j++) {
for (let k = 0; k < scanR; k++) {
let x = h + j;
let y = i + k;
// 獲取點(x,y)的畫素資訊
let pixelArr = getPixelInfo(imgData1, x, y).orderArr;
pixelArr.map(order => {
let disPixel = imgData1.data[order] - imgData2.data[order];
if (disPixel ** 2 > 100) {
diffNum++;
}
});
}
}
let x = Math.round(i + 0.5 * scanR);
let y = Math.round(h + 0.5 * scanR);
// 虛擬座標
let vX = i;
let vY = h;
if (!isNaN(diffNum)) {
this.diffPoints.push({diffNum, x, y, vX, vY});
}
// 獲取當前區域中心點的座標
let x = Math.round(i + 0.5 * scanR);
let y = Math.round(h + 0.5 * scanR);
// 虛擬座標
let vX = i;
let vY = h;
this.diffPoints.push({diffNum, x, y, vX, vY});
}
}
},
複製程式碼
結尾
缺點:
- 比如掃描的半徑(scanR)需要根據不同點的區域稍作調整(一般需要scanR大於不同點的的平均半徑)
- 如果每個不同點的區域平均半徑差異過大會導致 掃描區域取值比較尷尬
雖然有一定的缺點,但是基本可以滿足此次活動的需求,如果大家有更好的辦法,或者有啥疑問,都可以提出來,一起討論交流。
以上是我對圖片找不同的一些總結吧,文中如有錯漏之處,還請大家不吝賜教