Canvas识别图片内容并使用用户头像拼接

最后更新:

今天 12 月了,本来应该开始做外包的项目,但是好歹不好的长智齿了,
下午去拔了牙之后看着电脑发呆了一个下午,看来真的麻药影响 🧠 脑子。
所以,先写篇文章醒醒脑….

前天初步实现了下使用 canvas 来识别图片内容,今天就把它记录下来。
毕竟 canvas 这部分的内容是真的不懂,现学现卖,欢迎吐槽。📧 MailTo

前几天,领导和我说要制作一个年会的活动页面,需要有用户签到展示、企业形象展示、抽奖小游戏。

其中一个需求如下:

Logo 签到墙

年会的签到墙,使用微信扫码签到,然后后台拿到微信授权之后传给我用户的头像昵称,然后我就在前台展示并且完成一些特效。

需求图

需要用头像图片拼接组成图形以及文字内容。
这个图形和文字是用户上传的一个纯色内容+透明/白色底的图片。👇
会上传的图


我就直接想到用 canvas 来识别图片内容,

前天晚上初步实现了需求,
直接暴力的按照设置的 size 大小
从左上角不断循环识别到右下角,然后保存有内容的坐标点,
再按照坐标来绘制矩形和图片填充。

这边是我实现的效果 👇

识别成栅格
识别后的栅格图
用户头像填充
用户头像填充

识别内容:

const maxWidth = document.body.clientWidth; // 获取视窗宽度
const maxHeight = document.body.clientHeight; // 获取视窗高度

const size = 10; // 设置栅格大小
const points = []; // 坐标数组

const canvas = document.querySelector("canvas"); // 拿到canvas实例
canvas.width = maxWidth; // 设置canvas宽度 不能使用canvas.style.width来设置,会造成内容拉伸
canvas.height = maxHeight; // 设置canvas高度
const ctx = canvas.getContext("2d"); // 获取canvas上下文,这边获取的是二维绘图,还有一个3D内容 "webgl"

let img = new Image(); // 创建图片实例
img.src = require("assets/img/text.jpg"); // 获取设置图片url
let w = maxWidth; // 设置临时宽度,后边给绘制图片的时候会用到
let h = 0; // 设置临时高度
img.onload = () => {
  // img 设置 url 后会立即加载,加载完成后触发 onload 事件
  // 图片加载完成
  h = (w / img.width) * img.height; // 计算图片高度
  ctx.drawImage(img, 0, 0, w, h); // 绘制图片从(0,0)坐标,w为绘制的图片高度,h为绘制的图片高度
  // 开始识别
  for (let x = 0; x <= w - 10; x += size + 1) {
    // x轴开始循环 因为我需要有1像素的间隙所以是 size+1 ,如果不需要间隙则 +=size 即可
    for (let y = 0; y <= h - 10; y += size + 1) {
      // y轴开始循环
      let color = ctx.getImageData(x, y, size, size).data; // 识别区块内容会 一个像素内返回RGBA四个参数
      let count = 0; // 设置计数器
      // 以4个一组开始循环
      for (let i = 0; i < color.length; i += 4) {
        // 如果区块中有颜色内容则 count++,我这边识别的是黑色内容
        if (color[i] <= 100 || color[i + 1] <= 100 || color[i + 2] <= 100) {
          count++;
        }
      }
      // 颜色比例超过10%则记录,其实50%也可以,但是会出现 特殊情况1 的问题,下边会提到,但是内容会相对粗一些
      if (count >= size * size * 0.1) {
        points.push({ x: x, y: y }); // 添加到坐标数组
      }
    }
  }
  ctx.clearRect(0, 0, maxWidth, maxHeight); // 清除绘制的图片
};

绘制栅格图

ctx.fillStyle = "rgba(255,0,0,.55)"; // 设置填充色
// 遍历坐标数组内的所有数据
points.forEach(piont => {
  ctx.fillRect(piont.x, piont.y, size, size); // 绘制矩形
});

填充图片

填充图片和绘制矩形的原理相似所以就不举例了。

考虑到签到的人数有可能达不到坐标的数量,所以在最后可以重新循环用户头像列表来填充满整个栅格区

用户头像填充

以上是简单的使用 canvas 试别图片内容,并且栅格化且使用图片填充。

🎈 尾声

可能遇到的一些问题

打印points一直是空数组

因为时机不正确,图片加载是异步的,你写的同步代码会先执行,需要把绘制栅格的部分代码放到图片 onload 之后。

栅格没有绘制

#1 因为异步加载的问题,points 数组为空,没有没办法绘制。
#2 因为 canvas 容器的高度不够,识别之后绘制的内容不够显示。
#3 我用的 Vue.js 写的 Demo,写笔记的时候忘记去掉 this 了,所以 this.size 会有问题,已经修改了。

加载微信头像或者其他来源的头像出现跨域/403的情况

尝试在绘制图片的时候为 new Image() 出来的图片实例 增加 img.crossOrigin = "Anonymous"; 属性

识别之后底部、右侧出现一整排/列的栅格

尝试调整栅格大小,一般这种情况是因为,识别的栅格太大,最后一排、一列超出的绘制范围拾取不到颜色,各项颜色值都会是0,会被认为是黑色

考虑把笔记重新整理

感觉上每年的年底都会收到邮件来询问这篇笔记的,可能真的要抽时间来完整梳理成文章,而不只是笔记记录一下思路,并且提供一些可预览的DEMO,便于各位学习。
P.S. 去年就这样和小伙伴说过了,但是还是一拖再拖,真的自己是拖延症晚期没救了 😂


📌 附

  • 特殊情况 1:如果识别超过 50% 时,笔画交汇时折角会超过 50%,但是没有处在交汇处的内容可能并没有超过 50%
    笔画交汇
    调小栅格尺寸会避免大多数的这类情况