本文章主要討論的是如何將一個純色二維碼變成彩色的。css
前段時間公司業務上有這麼一個需求,客戶不喜歡後臺生成的純色二維碼,純藍,純紫,純綠都不行,想要彩色二維碼。而後這個任務都落到我頭上了,由於是圖片處理,那主要思路就是靠canvas,canvas能夠進行像素操做,因此我進行了一些嘗試,也踩了一點小坑,具體記錄以下。算法
第一個坑就是canvas用屬性去給寬高,別用css; canvas
第二個坑,作圖片處理好像得服務器環境,本地是不行的,據說是基於什麼安全考慮,最後我是經過搭本地服務器解決了canvas的報錯。數組
第三個坑,棧溢出,這個目前還沒找到緣由,後面會詳細講安全
主要思路來自於《啊哈!算法!》裏面深度優先搜索和廣度優先搜索的章節,該章節的最後一部分的「寶島探險」實現了給不一樣的區域依次編號,把編號當作染色,實際上是同樣的。服務器
其實所謂的彩色二維碼,不是那種每一個像素點顏色隨機的二維碼。仔細觀察二維碼就會發現,黑色的部分是一塊一塊的,他們分佈在白色當中,就好像島嶼分佈在海里,咱們要作的就是把每一個黑色塊單獨染色。黑色塊的實質就是一個一個相連的黑色像素點。dom
前面也提到,咱們使用canvas是由於能夠進行像素操做,因此咱們的操做實際上是給像素點染色,咱們顯然不但願給背景色染色,因此背景色須要進行一個判斷;前面也提到,背景色好像海洋分割了黑色的顏色塊,那也就是說咱們讀一個像素點進行染色以後,不停的判斷它右側的像素點顏色,當出現背景色的時候就說明到達了邊界,能夠中止右方向的染色,可是每一個像素點其實有四個相鏈接的方向,當一個像素點右邊就是背景色,咱們應該也去嘗試別的方向的可能性,這個就是深度優先搜索,經過遞歸,不斷的驗證當前像素點的下一個位置的顏色,是背景色,那就回來,嘗試別的方向;不是背景色,那就染色,而後對染色以後的這個像素點進行四個方向的驗證。函數
有幾點提一下,判斷是否是背景色,確定得比對rgba的值,因此顏色參數得作處理,另外一個就是像素點信息的數組,每四個元素表明一個像素,因此想要比對正確的像素信息,這部分也要處理。
可能說的有點亂,咱們看一下代碼spa
第一部分,canvascode
// canvas 部分
var canvas = $("canvas")[0]; var ctx = canvas.getContext("2d"); var img = new Image(); img.src = path; //這裏的path就是圖片的地址
第二部分,顏色的處理
// 分離顏色參數 返回一個數組
var colorRgb = (function() { var reg = /^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/; return function(str) { var sColor = str.toLowerCase(); if (sColor && reg.test(sColor)) { if (sColor.length === 4) { var sColorNew = "#"; for (var i = 1; i < 4; i += 1) { sColorNew += sColor.slice(i, i + 1).concat(sColor.slice(i, i + 1)); } sColor = sColorNew; } //處理六位的顏色值
var sColorChange = []; for (var i = 1; i < 7; i += 2) { sColorChange.push(parseInt("0x" + sColor.slice(i, i + 2))); } return sColorChange;
} else { var sColorChange = sColor.replace(/(rgb\()|(\))/g, "").split(",").map(function(a) { return parseInt(a); }); return sColorChange; } } })();
第三部分,給初始參數
爲了不多餘的操做,咱們用一個標記數組來記錄判斷過的位置
// 參數 var bg = colorRgb("#fff"); //忽略的背景色 var width = 220; var height = 220; var imgD; //預留給 像素信息 var colors = ["#368BFF", "#EF2767", "#F17900", "#399690", "#5aa6f7", "#fd417e", "#ffc000", "#59b6a6"]; //染色數組 // 隨機colors數組的一個序號 var ranNum = (function() { var len = colors.length; return function() { return Math.floor(Math.random() * len); } })(); // 標記數組 var book = [];
for (var i = 0; i < height; i++) {
book[i] = [];
for (var j = 0; j < width; j++) {
book[i][j] = 0;
}
}
第四部分,獲取像素信息,對每一個像素點進行遍歷處理,最後扔回canvas
若是標記過,那就跳過,若是沒標記過,那就隨機一個顏色,深度優先搜索並染色
img.onload = function() { ctx.drawImage(img, 0, 0, width, height); imgD = ctx.getImageData(0, 0, width, height); for (var i = 0; i < height; i++) { for (var j = 0; j < width; j++) { if (book[i][j] == 0 && checkColor(i, j, width, bg)) { //沒標記過 且是非背景色
book[i][j] = 1; var color = colorRgb(colors[ranNum()]); dfs(i, j, color); //深度優先搜索
} } } ctx.putImageData(imgD, 0, 0); } // 驗證該位置的像素 不是背景色爲true
function checkColor(i, j, width, bg) { var x = calc(width, i, j); if (imgD.data[x] != bg[0] && imgD.data[x + 1] != bg[1] && imgD.data[x + 2] != bg[2]) { return true; } else { return false; } } // 改變顏色值
function changeColor(i, j, colorArr) { var x = calc(width, i, j); imgD.data[x] = colorArr[0]; imgD.data[x + 1] = colorArr[1]; imgD.data[x + 2] = colorArr[2]; } // 返回對應像素點的序號
function calc(width, i, j) { if (j < 0) { j = 0; } return 4 * (i * width + j); }
關鍵代碼
咱們經過一個方向數組,來簡化一下操做,咱們約定好,嘗試的方向爲順時針,從右邊開始。
// 方向數組
var next = [ [0, 1], //右
[1, 0], //下
[0, -1], // 左
[-1, 0] //上
]; // 深度優先搜索
function dfs(x, y, color) { changeColor(x, y, color); for (var k = 0; k <= 3; k++) { // 下一個座標
var tx = x + next[k][0]; var ty = y + next[k][1]; //判斷越界
if (tx < 0 || tx >= height || ty < 0 || ty >= width) { continue; } if (book[tx][ty] == 0 && checkColor(tx, ty, width, bg)) { // 判斷位置
book[tx][ty] = 1; dfs(tx, ty, color); } } return; }
我遇到的最後一個坑就是當長寬大於220時就會棧溢出,可是小於這個值就不會有問題,具體的緣由還不清楚,猜想多是判斷那裏有問題,致使死循環了。
所有代碼在這裏
1 // 分離顏色參數 返回一個數組
2 var colorRgb = (function() { 3 var reg = /^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/; 4
5 return function(str) { 6 var sColor = str.toLowerCase(); 7 if (sColor && reg.test(sColor)) { 8 if (sColor.length === 4) { 9 var sColorNew = "#"; 10 for (var i = 1; i < 4; i += 1) { 11 sColorNew += sColor.slice(i, i + 1).concat(sColor.slice(i, i + 1)); 12 } 13 sColor = sColorNew; 14 } 15 //處理六位的顏色值
16 var sColorChange = []; 17 for (var i = 1; i < 7; i += 2) { 18 sColorChange.push(parseInt("0x" + sColor.slice(i, i + 2))); 19 } 20 return sColorChange; 21 } else { 22 var sColorChange = sColor.replace(/(rgb\()|(\))/g, "").split(",").map(function(a) { 23 return parseInt(a); 24 }); 25 return sColorChange; 26 } 27 } 28 })(); 29
30 // 驗證該位置的像素 不是背景色爲true
31 function checkColor(i, j, width, bg) { 32 var x = calc(width, i, j); 33
34 if (imgD.data[x] != bg[0] && imgD.data[x + 1] != bg[1] && imgD.data[x + 2] != bg[2]) { 35 return true; 36 } else { 37 return false; 38 } 39 } 40
41 // 改變顏色值
42 function changeColor(i, j, colorArr) { 43 var x = calc(width, i, j); 44 imgD.data[x] = colorArr[0]; 45 imgD.data[x + 1] = colorArr[1]; 46 imgD.data[x + 2] = colorArr[2]; 47 } 48
49
50 // 返回對應像素點的序號
51 function calc(width, i, j) { 52 if (j < 0) { 53 j = 0; 54 } 55 return 4 * (i * width + j); 56 } 57
58 // 方向數組
59 var next = [ 60 [0, 1], //右
61 [1, 0], //下
62 [0, -1], // 左
63 [-1, 0] //上
64 ]; 65
66 // 深度優先搜索
67 function dfs(x, y, color) { 68 changeColor(x, y, color); 69 for (var k = 0; k <= 3; k++) { 70 // 下一個座標
71 var tx = x + next[k][0]; 72 var ty = y + next[k][1]; 73
74 //判斷越界
75 if (tx < 0 || tx >= height || ty < 0 || ty >= width) { 76 continue; 77 } 78
79
80 if (book[tx][ty] == 0 && checkColor(tx, ty, width, bg)) { 81 // 判斷位置
82 book[tx][ty] = 1; 83 dfs(tx, ty, color); 84 } 85
86 } 87 return; 88 } 89
90 /*****上面爲封裝的函數*****/
91
92 /***參數***/
93 var bg = colorRgb("#fff"); //忽略的背景色
94 var width = 220; 95 var height = 220; 96 var imgD; //預留給 像素信息數組
97 var colors = ["#368BFF", "#EF2767", "#F17900", "#399690", "#5aa6f7", "#fd417e", "#ffc000", "#59b6a6"]; //染色數組
98 // 隨機colors數組的一個序號
99 var ranNum = (function() { 100 var len = colors.length; 101 return function() { 102 return Math.floor(Math.random() * len); 103 } 104 })(); 105
106 // 標記數組
107 var book = []; 108 for (var i = 0; i < height; i++) { 109 book[i] = []; 110 for (var j = 0; j < width; j++) { 111 book[i][j] = 0; 112 } 113 } 114
115
116 // canvas 部分
117 var canvas = $("canvas")[0]; 118 var ctx = canvas.getContext("2d"); 119
120 var img = new Image(); 121 img.src = path; //這裏的path就是圖片的地址
122 img.onload = function() { 123 ctx.drawImage(img, 0, 0, width, height); 124 imgD = ctx.getImageData(0, 0, width, height); 125
126 for (var i = 0; i < height; i++) { 127 for (var j = 0; j < width; j++) { 128 if (book[i][j] == 0 && checkColor(i, j, width, bg)) { //沒標記過 且是非背景色
129 book[i][j] = 1; 130 var color = colorRgb(colors[ranNum()]); 131 dfs(i, j, color); //深度優先搜索
132 } 133 } 134 } 135
136 ctx.putImageData(imgD, 0, 0); 137 }
雖然看起來有點長,其實大部分函數都在處理像素點的信息。實現起來,主要就是得對深度優先搜索有所瞭解,每一個像素點都進行深度優先搜索,染過色的天然被標記過,因此當一個新的沒標記過的像素點出現時,天然意味着新的顏色塊。細節方面,就是注意一下imgD.data和像素點序號之間的對應關係,別的也就還好了。不過注意一點就是,由於像素點很小,因此肉眼以爲不相連的色塊也有多是連在一塊兒的,會染成同樣的顏色。
忘了放圖了,這裏放幾張,拿qq截的,把外面的邊框不當心也截了,嘛,湊活看看吧