用canvas实现图片背景替换、抠图

在这里插入图片描述

演示地址
代码仓库

关键技术

canvas apielement-ui (el-color-picker、el-upload)

原理解析

1. 用户从本地上传一张图片

我们拿到图片数据并在页面渲染出来,这一步用到了elementui的el-upload

el-upload.avatar-uploader(
  ref="logoUpload",
  accept="image/*",
  action="#",
  :auto-upload="false",
  :on-change="handleStatusChange"
)
  el-button(size="small", type="primary") 点击上传

这里实际上不需要真正上传到服务器,只需要获取到图片的在内存中的url

handleStatusChange(file) {
  // console.log(file);
  this.originImg = URL.createObjectURL(file.raw);
},

拿到url之后可以先把图片用img标签渲染在页面上,这样做的目的是为了获取图片的实际尺寸,方面我们等比例缩放在我们的canvas上。

// 图片加载完成
    loadImg(e) {
      const img = e.target;
      const width = img.offsetWidth;
      const height = img.offsetHeight;
      this.imgWidth = width;
      this.imgHeight = height;
      this.canHeight = (height / width) * this.canWidth;
    }

canvas的width可以根据外层容器来获取

this.$nextTick(() => {
      const contentWidth = document.querySelector(".origin-box").offsetWidth;
      this.canWidth = Math.min(contentWidth - 12, this.canWidth);
});

2. 绘制canvas

绘制图片到canvas,这一步比较简单,

//这里需要缩放一下,因为我们的画布已经被缩放了
this.originCtx.scale(this.canWidth / width, this.canWidth / width);
this.originCtx.drawImage(img, 0, 0);

3. 选中颜色

点击canvas,我们可以拿到该点上的颜色值,获取方式

ctx.getImageData(targetX,targetY,1,1)

拿到的是imagedate对象,{data, width, height},data即为我们要的颜色值。

4. 遍历原图片的颜色值,匹配到选中颜色之后,做对应颜色的替换即可

核心api: getImageData putImageData

//获取data
const data = this.imageData.data || [];
//遍历并替换
for (let i = 0; i < data.length; i += 4) {
        const similar = this.isSimilar(_fromColor, data.slice(i, i + 4));
        if (similar) {
          data[i] = _toColor[0];
          data[i + 1] = _toColor[1];
          data[i + 2] = _toColor[2];
          data[i + 3] = _toColor[3] * 255;
        }
 }
 //绘制到目标容器上
 this.transCtx.putImageData(this.imageData, 0, 0);

isSimilar方法用来判断两个颜色是否相似或相等,这个可以通过参数调整(类似于ps的容差概念),容差值越小,匹配越精准。

颜色值是rgba格式的,即4个数组为一组,数值都在[0,255]之间,由于el-picker返回的rgba,透明度用的是【0,1】表示的,所以要转换到0-255区间

5. undo&redo

撤销、前进、后退功能还是很有必要的,重复替换操作,可返回历史操作步骤。
创建一个队列(这里用数组代替),每次有新的数据变化添加到队列里,用unshift表示入列pop从队列后面删除。可以设置上限10,队列过大会占用较大内存,不建议设置过大。
维护一个index,理解成指针,表示当前回退的数据在队列中的位置。

undo() {
  this.index++;
  this.redrawImg();
},
redo() {
  this.index--;
  this.redrawImg();
},
redrawImg() {
  const preImageData = JSON.parse(this.imgStock[this.index]).data;
  this.imageData = this.transCtx.createImageData(
    this.canWidth,
    this.canHeight
  );
  for (let i = 0; i < this.imageData.data.length; i++) {
    this.imageData.data[i] = preImageData[i];
  }
  this.transCtx.putImageData(this.imageData, 0, 0);
},

需要注意的细节:当回退到某一条历史记录,比如index=5,这时候再执行手动操作替换,此时就“穿越”了,需要把index = 5之前的历史都移除。(可能描述有点绕~)

至此,就完成了核心功能了~

TODO LIST

  1. 增加边缘识别,去除毛边
  2. 增加容差选项
  3. 尝试视频抠图和替换
  4. 。。。

演示地址
代码仓库

欢迎提意见&star!

posted @ 2021-08-26 14:18  阿明先森  阅读(1829)  评论(0编辑  收藏  举报