二维码改色方案
本文章主要讨论的是如何将一个纯色二维码变成彩色的。
前段时间公司业务上有这么一个需求,客户不喜欢后台生成的纯色二维码,纯蓝,纯紫,纯绿都不行,想要彩色二维码。然后这个任务都落到我头上了,因为是图片处理,那主要思路就是靠canvas,canvas可以进行像素操作,所以我进行了一些尝试,也踩了一点小坑,具体记录如下。
前置知识
一些小坑
第一个坑就是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截的,把外面的边框不小心也截了,嘛,凑活看看吧