纯前端实现图片背景透明化

前言

不管是作一些2d的小游戏,或者制做小图标,或者抠图都须要用到这个功能,对图片的背景进行透明化,是咱们常常须要用到的一个功能。css

一般状况下咱们都会去下载PS或者美图秀秀这样的软件去制做。html

可是我真的不想仅仅为了作个透明图像就去下载这些软件,这些软件不只体积大,要下载个半天,放在电脑上也占空间。前端

最重要的是每次我作这个事情,都须要去临时百度一下制做透明图片的方法。git

这些软件当然强大,可是功能的众多或者须要一些基础知识,每每形成了一些门槛。github

简单点说,虽然瑞士军刀很6,可是我如今只须要一把起子,我不想知道什么蒙版图层,不想在一堆什么美颜什么各类滤镜之中找半天,我就想上传个图片点两下就行了。chrome

那么能不能在线对图片进行背景透明化呢?canvas

固然是有的,下面是网址数组

http://www.aigei.com/bgremover/浏览器

你觉得我是来推荐网站的?固然不是。app

我之因此提到这个网站,是由于我之前就是用这个作一些处理的,可是真的不是很给力啊。

我并不知道它的原理,也没有看过它的代码,可是它的缺陷很明显:

  1. 不能对指定颜色进行透明化
  2. 当须要对色差很大的多种颜色进行透明化时,无能为力
  3. 对一些图片的透明化处理不够完美,会出现锯齿,可是又没法进行进一步处理
  4. 对复杂图片彻底无能为力

有问题就解决问题呗,因而就有了今天的小玩意。

做品

与以前的做品同样,直接将功能写在这篇博客里了,因此能够直接在博客园中使用。

使用方法:

  1. 上传图片
  2. 点击图片,将以鼠标点击处的颜色为标准,对色差20以内的颜色进行透明化处理。(若是想调整色差标准,能够在控制台下设置transparentConfig.colorDiff)
  3. 对出现透明化处理有有误的地方,能够开启恢复模式,再次移动鼠标到图片上,此时鼠标会变成红色小方框,小方框区域内会显示原始图像。点击后会将红色小方框区域内的图像恢复为原始图像。(若是想调整小方框尺寸,能够在控制台下经过transparentConfig.setRecoverSize(30)的方式进行修改)
  4. 下载图片,搞定。

固然按照本懒人的惯例,仍是只在chrome浏览器下实现,因此若是您用其它浏览器的话可能没法正常操做。

不过本应用的核心功能与以往同样都是能够在现代浏览器中实现的,只是须要您调一下兼容性。

若是您有闲情逸致,想研究一下的话,这是本项目的 GitHub地址,为了能方便复制进博客园,因此代码是直接写在html中的。

少说废话,如下为应用:

技术点

本应用依然只使用纯前端实现,涉及到的技术点以下:

  1. 获取图片文件
  2. 将文件转换为图片,并放入canvas中
  3. 点击canvas获取点击处的颜色信息
  4. 根据指定颜色,对图像中在色差范围内的颜色进行透明化处理
  5. 自定义鼠标,在鼠标上显示指定区域内原始图像
  6. 对图像上指定区域,进行图像还原操做
  7. 下载图片

其中技术点1,2,7在以前的一篇博客中有涉及到,因此这里就再也不赘述,不了解的能够去看一下我以前写的那篇博客:在博客园里给图片加水印(canvas + drag)

那么,接下来就让咱们看一下具体的实现吧。

点击图片获取点击处的颜色信息

经过type为file类型的input获取到文件,而后经过FileReader读取文件信息后放入到canvas中。

这是前两步所作的工做,如今咱们须要作的是点击图像(其实是canvas)获取到点击处的颜色信息。

首先咱们须要获取到原始图像的像素信息,并保存下来,这一步在图片加载时实现,部分代码以下:

var ctx = document.getElementById('target_canvas').getContext('2d');
imgDataArr = ctx.getImageData(0, 0, imgWidth, imgHeight).data;

 小课堂开始:

canvas的getImageData会获取canvas中指定区域内的图像信息,返回一个ImageData对象。

ImageData对象的data属性的值是一个Uint8ClampedArray对象,而这个对象就是图像的像素信息。

Uint8ClampedArray看名字能够了解到,它是一个定型数组,里面的值都是0-255范围以内的值。

假如咱们有一个图片只有四个像素,长2px,宽2px。左上角的像素和右下角的像素为黑色,而右上角和左下角的像素为白色。

以下图:

那么这张图片会以怎样的形式存储在Uint8ClampedArray数组中呢?

首先咱们了解到白色的RGBA值为rgba(255,255,255,255),黑色的RGBA值为rgba(0,0,0,255)。

那么这张图片的分解为rgba值,分别为

rgba(0,0,0,255)           rgba(255,255,255,255)
rgba(255,255,255,255)     rgba(0,0,0,255)

那么颜色值将会以从左到右,从上至下的方式存储到Uint8ClampedArray数组中,以下:

[0,0,0,255,255,255,255,255,255,255,255,255,0,0,0,255]

小课堂讲解完毕,回到正题。

如今咱们已经拿到了原始图像的像素信息了,并存放在了imgDataArr这个Uint8ClampedArray数组中。

如何获取鼠标点击处的像素信息呢?代码以下:

   /**
      * 获取图像数据中指定偏移处的颜色信息
      */
    function getColorInfo(imgDataArr, offsetX, offsetY) {
      var pos = canvasInfo.width * 4 * offsetY + offsetX * 4;
      return {
        rValue: imgDataArr[pos],
        gValue: imgDataArr[pos + 1],
        bValue: imgDataArr[pos + 2],
        aValue: imgDataArr[pos + 3]
      }
    }

    /**
     * 非恢复模式下,点击canvas,以点击处颜色为标准,去掉颜色色差在指定色差范围内的颜色
     */
    function transparetModeCanvasClick(e) {
      if (imgDataArr.length === 0) {
        return;
      }
      if (resultImgDataArr.length === 0) {
        resultImgDataArr = imgDataArr.slice(0)
      }
      var clickColorInfo = getColorInfo(resultImgDataArr, e.offsetX, e.offsetY)
      ...
    }

咱们会给canvas绑定回调函数为transparetModeCanvasClick的click事件,那么,在鼠标点击canvas后,咱们就能够获取到鼠标相对于canvas左上角的点击位置。

imgDataArr里面保存的是原始的图像像素信息,以后还会用到,因此这里不作处理。

那么就copy数据到当前像素信息数组resultImgDataArr中。

而后获取像素信息时须要计算像素在一维数组中的位置:

var pos = canvasInfo.width * 4 * offsetY + offsetX * 4;

根据上面的表达式获得点击的那个像素在一维数组中的位置,若是有仔细阅读以前Uint8ClampedArray存储像素信息的方式,这个表达式应该不难理解。

判断颜色与指定颜色的色差,并作透明化处理

在获取到点击像素的颜色信息后,咱们须要去遍历整个canvas的像素信息,对于色差小于指定范围的颜色作透明化处理。代码以下:

        /**
         * 获取图像数据指定位置颜色与指定颜色的色差
         */
        function getColorDiff(imgDataArr, pos, colorInfo) {
          var value = Math.pow(imgDataArr[pos] - colorInfo.rValue, 2) +
            Math.pow(imgDataArr[pos + 1] - colorInfo.gValue, 2) +
            Math.pow(imgDataArr[pos + 2] - colorInfo.bValue, 2);

          return Math.pow(value, 0.5);
        }
        /**
         * 设置图像数据指定位置为透明色
         */
        function setTransparent(imgDataArr, pos) {
          imgDataArr[pos] = 0;
          imgDataArr[pos + 1] = 0;
          imgDataArr[pos + 2] = 0;
          imgDataArr[pos + 3] = 0;
        }
        /**
         * 非恢复模式下,点击canvas,以点击处颜色为标准,去掉颜色色差在指定色差范围内的颜色
         */
        function transparetModeCanvasClick(e) {
          if (imgDataArr.length === 0) {
            return;
          }
          if (resultImgDataArr.length === 0) {
            resultImgDataArr = imgDataArr.slice(0)
          }
          var clickColorInfo = getColorInfo(resultImgDataArr, e.offsetX, e.offsetY)

          // 若是是透明颜色则不作处理
          if (clickColorInfo.aValue === 0) {
            return;
          }

          var ctx = document.getElementById('target_canvas').getContext('2d');
          for (var pos = 0, len = canvasInfo.width * canvasInfo.height * 4; pos < len; pos = pos + 4) {
            if (getColorDiff(resultImgDataArr, pos, clickColorInfo) < transparentConfig.colorDiff) {
              setTransparent(resultImgDataArr, pos);
            }
          }
          ctx.putImageData(new ImageData(resultImgDataArr, canvasInfo.width, canvasInfo.height), 0, 0);
          setCanvasImgToDownloadLink();
        }

 色差计算公式为将rgb三个值的颜色相减,将他们的平方和进行开方便可。

这里其实存在一个优化点:

一、一般咱们设为透明色的颜色,每次实际上都比较单一,那么在这里能够设一个临时数组存放已经比较过色差的颜色。再次要对一个颜色比较色差前,能够查看颜色是否在这个数组中,从而避免再次计算色差,所以对于大图像而言,存在不少像素点,这种大量计算能尽可能减小的话可让操做更快。

二、这里的循环也能够进行优化。倒序循环  + Duff's Device能够进行优化。

之因此在这里强调这一点,是由于在这里对大图进行消除单一颜色为透明色时,确实须要消耗更多时间。(不过我还能接受,因此暂时不理了)

至于设置图片为透明色,实际上只须要将

imgDataArr[pos + 3] = 0;

便可。
可是为了和通常声明的透明颜色保持一致,仍是将其余几个值都设为0。

将数据设为透明色的数据以后,须要调用canvas的putImageData方法将整个图像的数据设置到canvas中。

至此,咱们完成了对图像进行透明化处理的整个过程。
可是仍然不够,由于对于复杂图像而言,这种方式处理起来太过粗暴,没法作到精细化的处理。
因此接下来咱们要实现恢复模式,对于处理的很差的地方进行恢复原始图像的操做。

恢复模式的探讨

在恢复模式下,当咱们鼠标移动到canvas上时,鼠标显示为一个小方框,小方框内是原始图像。
当咱们点击鼠标时,小方框内的图像会从新覆盖到当前图像上,从而达到恢复原有图像的效果。

全篇下来,其实在这个地方才开始显得有趣。

 最初方案(隐藏鼠标 + canvas) 

在想到经过这个办法进行精细化操做以后,第一反应是隐藏鼠标,并在移动鼠标时跟随一个小的canvas,这个小canvas中显示的是原始图像。

事实上最开始也是这么作的,若是各位有兴趣的话能够参考我github上的提交方案,上面有这个方案的实现。

虽然这个方案能够实现效果,可是存在一个很明显的性能问题,操做起来会有顿卡的感受。

缘由就是在进行鼠标移动的时候,会频繁的计算图像信息,再写入到小canvas中。

虽而后来加了个防抖函数,并将移动方框放在防抖函数外,使得红色小框能够即时移动,其中的内容不会出现顿卡,可是由于防抖函数的存在,必然会有一点小小的延迟。

做为一个懒人其实我以为作到这里就能够了,由于这样我也能够用了。

然而,纠结了半天仍是改了,这种卡顿实在是蛋疼得紧。

如今的方案(隐藏鼠标 + background-image) 

如今的方案是在鼠标移动时,隐藏鼠标,并在鼠标那里加一个div,div里面设置原始图像的背景图片。

在移动鼠标时,不只会对鼠标的位置进行从新计算(说是位置,实际上用的是translate,而不是top+left),还会对背景图片的位置进行从新计算,这样就能够实现一样的效果了。

经过这种方案,在个人电脑上移动红框和计算恢复图像的方式很流畅,彻底感受不到卡顿。

若是在低配电脑上有卡顿的状况,这里也能够加一个防抖函数来处理。

恢复模式的实现

那么让咱们如今来看一看恢复模式下鼠标在canvas上移动时的代码:

// 根据鼠标的偏移位置获取recover_img位置
function getRecoverImgPos(e) {
  // 给鼠标位置+1,是为了让recover_img不会出如今鼠标下方,从而使得鼠标点击时不会点击在recover_img上
  return {
    x: e.offsetX + 1,
    y: e.offsetY + 1
  }
}

/**
 * 恢复模式下,鼠标在canvas上移动,呈现原先图像
 */
function recoverModeCanvasMove(e) {
  if (imgDataArr.length === 0) {
    return;
  }
  var $recoverImg = $("#recover_img");
  var recoverImgPos = getRecoverImgPos(e)
  if (recoverImgPos.x > canvasInfo.width - recoverSize || recoverImgPos.y > canvasInfo.height - recoverSize) {
  $recoverImg.hide();
    return;
  } else {
    $recoverImg.show();
  }
  $recoverImg.css({
    transform: 'translate(' + recoverImgPos.x + 'px,' + recoverImgPos.y + 'px)',
    'background-position': (-recoverImgPos.x - 1) + 'px ' + (-recoverImgPos.y - 1) + 'px'
  });
}

在上面的代码中咱们会根据鼠标的位置从新计算恢复图像所在的div的位置,而后判断是否触边来决定是否隐藏。
接着再来计算其位置。

这里有一个坑点在注释里面也写了,就是恢复图像实际上并无和鼠标重叠,以防止咱们在点击时点到恢复图像上而不是canvas上。

另外若是你们细心查看css样式的话,会发现两个小坑点:

  1. 在样式里面对恢复图像和canvas上的鼠标样式都设置了隐藏,缘由是避免当鼠标拖动过快时鼠标会出如今恢复图像上,出现鼠标闪烁状况
  2. 恢复图像自己就有一个top:1px和left:1px的初始值,这是由于咱们的canvas有一个1px的border,而绝对定位的位置是相对于canvas的父级的。

而后再来看看点击恢复图像的代码:

/**
 * 恢复模式下,点击canvas,将点击处指定范围内图像恢复原样
 */
function recoverModeCanvasClick(e) {
  if (imgDataArr.length === 0) {
    return;
  }
  var recoverImgPos = getRecoverImgPos(e);
  for (var i = 0, ylen = recoverSize; i < ylen; i++) {
    var pos = canvasInfo.width * 4 * (recoverImgPos.y + i) + recoverImgPos.x * 4;
    for (var j = pos, xlen = pos + recoverSize * 4; j < xlen; j++) {
      resultImgDataArr[j] = imgDataArr[j]
    }
  }
  var ctx = document.getElementById('target_canvas').getContext('2d');
  ctx.putImageData(new ImageData(resultImgDataArr, canvasInfo.width, canvasInfo.height), 0, 0);
  setCanvasImgToDownloadLink()
}

一样是先根据鼠标位置获取恢复图像的位置,而后根据偏移量去计算位置。

咱们得注意到虽然咱们的恢复图像是一块完整的相链接的区域,可是在
Uint8ClampedArray数组中的数据并非相链接的,须要咱们去计算。

将最开始咱们保存初始图像像素数组imgDataArr赋值到当前处理的图像像素数组中。

最后将处理好的像素数组以putImageData的方式放入数组便可。

总结

这个小应用如今能够知足个人需求了,可是它依然存在不少不足与改进空间,好比:

  1. 兼容性
  2. 色差的调整,如今是放在控制台之中。即便是拿出来也可能只是一个输入框。实际上这个地方是能够作的更加完美,在应对一个复杂图片时,当咱们点击一个像素后,能够保存这个像素,并出现一个调整色差的滑动条,拖动这个滑动条图像会针对色差在原基础上实时进行透明化和恢复图像的处理。
  3. 恢复图像尺寸的调整,能够经过鼠标滑轮滚动的方式进行处理
  4. 返回操做。能够增长回退功能。
  5. 依然存在的一些小bug以及优化空间,以及仍然不够智能不够完美的图像处理,由于我真的只想点一下而后就自动处理好。

本期分享结束,依然是一个小应用,依然是一堆你可能知道也可能不知道的小知识点。

最后例行说明,右下角的精灵球是点赞,这已是我连续几篇文章说明了,而且那么大几个字已经写明了。
由于有几个园友引用了这个精灵球,因此大家应该也造成了精灵球便是点赞的心理预期和用户习惯了吧。
若是你不当心点错了,请刷新页面,那么精灵球那里会出现取消点赞的按钮。

下次不会再解释了。

相关文章
相关标签/搜索