JavaScript实现纯色二维码转彩色二维码:Canvas像素级处理与DFS算法

作者:小圆点

时间:2026-10-22

浏览:451

本文详解如何利用JavaScript Canvas进行像素级处理,将纯色二维码转换为彩色。核心思路借鉴深度优先搜索(DFS)算法,通过遍历像素点识别黑色连通区域并随机分配颜色。文章还记录了Canvas宽高设置、本地服务器环境及栈溢出等常见坑点,提供了完整的代码实现与注意事项。

文章目录

引言与需求背景

本文主要探讨如何将传统的黑白(或单色)二维码转换为彩色二维码。

近期业务中遇到此类需求,客户对后台生成的纯蓝、纯紫或纯绿等单色二维码不满意,明确要求生成彩色二维码。该任务交由我负责,鉴于涉及图片处理,核心思路依赖于Canvas的像素操作能力。经过多次尝试并排除一些常见错误,现将具体实现过程记录如下。

前置知识与Canvas基础

前置知识

实现此功能需掌握Canvas的几个关键方法:drawImage用于将图片绘制到画布上;getImageData用于获取指定矩形区域内所有像素点的数据,其返回值的data属性是一个一维数组,按从左到右、从上到下的顺序存储像素信息,每个像素占用四个元素,分别对应红(r)、绿(g)、蓝(b)和透明度(a);putImageData则用于将修改后的像素数据重新写回画布。

开发中遇到的常见坑点

一些小坑

第一个注意事项是设置Canvas的宽高时,必须使用HTML属性(width和height),严禁使用CSS样式设置宽高,否则会导致图像拉伸或模糊。

第二个注意事项是图片处理通常需要在服务器环境下运行,本地直接打开文件可能会因安全策略(跨域或同源策略)报错。解决方案是搭建本地服务器来运行代码。

第三个注意事项是栈溢出问题,具体原因将在后文详细分析。

变色算法思路

变色的思路

核心算法灵感来源于《啊哈!算法!》中关于深度优先搜索(DFS)和广度优先搜索(BFS)的章节,特别是其中的“宝岛探险”案例。该案例通过给不同区域编号来模拟染色过程,这与将二维码黑色区域分别染色的思路一致。

具体实现与代码解析

具体实现

所谓的彩色二维码,并非每个像素点颜色随机,而是保持二维码的结构特征。仔细观察可知,黑色部分是由若干块组成的连通区域,分布在白色背景中,如同岛屿分布在海里。我们的目标是将每个独立的黑色连通块单独染上一种颜色。黑色块的本质是相互连接的黑色像素点集合。

利用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时会出现栈溢出,而小于该值则正常。具体原因尚不明确,推测可能是判断逻辑存在缺陷导致死循环或递归过深。

全部代码在这里


// 分离颜色参数 返回一个数组
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;
  }
 }
})();

// 验证该位置的像素 不是背景色为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;
}

/*****上面为封装的函数*****/

/***参数***/
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 部分
var canvas = $("canvas")[0];
var ctx = canvas.getContext("2d");

var img = new Image();
img.src = path; //这里的path就是图片的地址
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);
}

总结与注意事项

总结

虽然代码量较大,但大部分函数仅用于处理像素信息。实现关键在于理解深度优先搜索:对每个像素点执行DFS,已染色的像素会被标记,因此遇到新未标记的像素点即意味着新的颜色块。细节方面,需注意imgD.data数组与像素点序号的对应关系。此外,由于像素点极小,视觉上看似不相连的色块在像素层面可能是连通的,因此会被染成相同颜色。

补充图片展示,使用QQ截图,虽包含部分边框,但可参考效果。

引言与需求背景

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章