二維碼改色方案


本文章主要討論的是如何將一個純色二維碼變成彩色的。

前段時間公司業務上有這么一個需求,客戶不喜歡后台生成的純色二維碼,純藍,純紫,純綠都不行,想要彩色二維碼。然后這個任務都落到我頭上了,因為是圖片處理,那主要思路就是靠canvas,canvas可以進行像素操作,所以我進行了一些嘗試,也踩了一點小坑,具體記錄如下。

 

前置知識

drawImage方法可以把圖片畫到canvas上,getImageData方法可以獲得一個矩形區域所有像素點的信息,返回值的data屬性是一個一維數組,儲存了所有像素點的信息,一個像素點的信息會占四個元素,分別代表r,g,b和透明度。而像素點在一維數組中的順序是從左到右,從上到下。最后就是putImageData方法,把更改過的像素信息數組重新扔回畫布上。

一些小坑

第一個坑就是canvas用屬性去給寬高,別用css;  

第二個坑,做圖片處理好像得服務器環境,本地是不行的,聽說是基於什么安全考慮,最后我是通過搭本地服務器解決了canvas的報錯。

第三個坑,棧溢出,這個目前還沒找到原因,后面會詳細講

變色的思路

主要思路來自於《啊哈!算法!》里面深度優先搜索和廣度優先搜索的章節,該章節的最后一部分的“寶島探險”實現了給不同的區域依次編號,把編號看成染色,其實是一樣的。

具體實現

其實所謂的彩色二維碼,不是那種每個像素點顏色隨機的二維碼。仔細觀察二維碼就會發現,黑色的部分是一塊一塊的,他們分布在白色當中,就好像島嶼分布在海里,我們要做的就是把每個黑色塊單獨染色。黑色塊的實質就是一個一個相連的黑色像素點。

前面也提到,我們使用canvas是因為可以進行像素操作,所以我們的操作其實是給像素點染色,我們顯然不希望給背景色染色,所以背景色需要進行一個判斷;前面也提到,背景色好像海洋分割了黑色的顏色塊,那也就是說我們讀一個像素點進行染色之后,不停的判斷它右側的像素點顏色,當出現背景色的時候就說明到達了邊界,可以停止右方向的染色,但是每個像素點其實有四個相連接的方向,當一個像素點右邊就是背景色,我們應該也去嘗試別的方向的可能性,這個就是深度優先搜索,通過遞歸,不斷的驗證當前像素點的下一個位置的顏色,是背景色,那就回來,嘗試別的方向;不是背景色,那就染色,然后對染色之后的這個像素點進行四個方向的驗證。


有幾點提一下,判斷是不是背景色,肯定得比對rgba的值,所以顏色參數得做處理,另一個就是像素點信息的數組,每四個元素代表一個像素,所以想要比對正確的像素信息,這部分也要處理。
可能說的有點亂,我們看一下代碼

第一部分,canvas

// 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截的,把外面的邊框不小心也截了,嘛,湊活看看吧

 

 




免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM