使用 canvas 实现一个简单的图片抠图工具

807 阅读2分钟

Canvas 是 HTML5 新增的一个元素,功能非常强大,通过它可以用于在网页上绘制图形、动画等。这里通过使用 Canvas 将用户上传的图片,根据选择需要抠掉的像素点的RGB数值范围,最后生成抠图后的图片,从而实现了一个简单的图片抠图工具。

上传图片

首先是上传图片,这个没什么好讲的,直接看代码:

<div class="block">
  <div class="title">原图上传</div>
  <div class="main">
    <input id="upload-btn" type="file" accept="image/*" />
    <div class="img-box" id="origin"></div>
  </div>
</div>

<script lang="javascript">
    let originImgDom
    document.getElementById('upload-btn').addEventListener('change', e => {
        const file = e.target.files[0]
        const reader = new FileReader()
        reader.readAsDataURL(file)
        reader.onload = () => {
          if (!originImgDom) {
            originImgDom = document.createElement('img')
            originImgDom.src = reader.result
            document.getElementById('origin').append(originImgDom)
          } else {
            originImgDom.src = reader.result
          }
        }
    })
</script>

选择图片的 RGB 数值范围

在进行抠图之前,需要选择抠图区域的 RGB 数值范围。先看代码:

<div id="rgb-inputs">
  <div>
    R:
    <select data-key="r">
      <option value="ge">&gt;=</option>
      <option value="le">&lt;=</option>
      <option value="eq">===</option>
    </select>
    <input data-key="r" type="number" min="0" max="255" />
  </div>
  <div>
    G:
    <select data-key="g">
      <option value="ge">&gt;=</option>
      <option value="le">&lt;=</option>
      <option value="eq">===</option>
    </select>
    <input data-key="g" type="number" min="0" max="255" />
  </div>
  <div>
    B:
    <select data-key="b">
      <option value="ge">&gt;=</option>
      <option value="le">&lt;=</option>
      <option value="eq">===</option>
    </select>
    <input data-key="b" type="number" min="0" max="255" />
  </div>
  <div>
    <button id="create-btn">生 成</button>
  </div>
</div>
<script lang="javascript">
const rgbOpts = {
    r: { val: undefined, act: 'ge' },
    g: { val: undefined, act: 'ge' },
    b: { val: undefined, act: 'ge' }
}
document.getElementById('rgb-inputs').addEventListener('change', e => {
    const target = e.target
    const key = target.dataset.key

    if (key && Object.prototype.hasOwnProperty.call(rgbOpts, key)) {
      const tag = target.tagName.toUpperCase()
      if (tag === 'INPUT') {
        rgbOpts[key].val = parseInt(target.value)
      } else if (tag === 'SELECT') {
        rgbOpts[key].act = target.value
      }
    }
})
</script>

这段代码的作用是监听页面上 RGB 数值输入框和比较符号下拉框的变化,然后更新 rgbOpts 对象中对应属性的 val 和 act 值。由于为一个个的 input 绑定事件太繁琐,就使用了事件代理,以及通过dataset 确定每个操作对应的 key 值,再通过 HTML 标签名判断需要赋值到哪个属性上。rgbOpts 对象用于存储用户选择的RGB数值和比较符号,它会被传递给 compare 函数,用于比较图片中的像素点是否符合要求。

实现抠图

function imageMatting(originImage) {
    return new Promise((resolve, reject) => {
      let cvs = document.createElement('canvas')
      const ctx = cvs.getContext('2d')
      const img = new Image()
      img.src = originImage
      img.onload = () => {
        const { width, height } = img
        cvs.width = width
        cvs.height = height
        ctx.drawImage(img, 0, 0, width, height)

        const originImageData = ctx.getImageData(0, 0, width, height)
        const data = originImageData.data

        for (let i = 0; i < data.length; i += 4) {
          const rgbObj = { r: data[i], g: data[i + 1], b: data[i + 2] }
          if (compare(rgbObj)) data[i + 3] = 0
        }
        ctx.putImageData(originImageData, 0, 0)
        resolve(cvs.toDataURL('image/png'))
        cvs = null
      }
      img.onerror = err => {
        reject(err)
      }
    })
}

这段代码首先将图片绘制在 canvas 中,通过 canvas 中的 getImageData 方法获取图片的 RGB 数值。然后遍历取得的 RGB 数值,通过 compare 函数进行比较。

通过比较的结果,对符合要求的图片像素点的 alpha 通道值设为0,从而实现图片抠图的效果。之后再使用 putImageData 方法将图片绘制回 canvas 中,再通过 canvas.toDataURL('image/png') 获得抠图后的图片。

其中的 compare 函数实现如下:

function compare(rgbObj) {
    let isDiff = false
    const compareKeys = Object.keys(rgbOpts).filter(key => rgbOpts[key].val === 0 || rgbOpts[key].val)

    if (compareKeys.length) {
      isDiff = compareKeys.every(key => {
        let flag = false
        const { act, val } = rgbOpts[key]
        if (act === 'eq') {
          flag = rgbObj[key] === val
        } else if (act === 'ge') {
          flag = rgbObj[key] >= val
        } else if (act === 'le') {
          flag = rgbObj[key] <= val
        }
        return flag
      })
    }
    return isDiff
}

它首先会对用户输入的 RGB 数值进行过滤,仅比较用户已输入的 RGB 值。然后根据用户选择的比较操作和 RGB 值对图片像素进行比较,返回比较结果。

完整代码

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      body {
        background-color: rgba(0, 0, 0, 0.05);
      }
      .block {
        max-width: 1000px;
        min-height: 100px;
        padding: 10px;
        margin: 5px;
        border: 1px solid #999;
      }
      .block + .block {
        margin-top: 10px;
      }
      .block .title {
        padding-bottom: 10px;
        font-size: 16px;
        font-weight: bold;
      }
      .block .main {
        display: flex;
        align-items: flex-start;
        justify-content: space-between;
      }
      .block .img-box {
        width: 300px;
      }
      .block .img-box img {
        display: block;
        width: 300px;
        object-fit: contain;
      }

      #rgb-inputs {
        flex: 1;
      }
      #rgb-inputs div {
        display: flex;
        align-items: center;
        padding: 5px 0;
      }
      #rgb-inputs div select {
        margin: 0 5px;
      }
    </style>
  </head>
  <body>
    <div class="block">
      <div class="title">原图上传</div>
      <div class="main">
        <input id="upload-btn" type="file" accept="image/*" />
        <div class="img-box" id="origin"></div>
      </div>
    </div>
    <div class="block">
      <div class="title">图片生成</div>
      <div class="desc">通过比较以下 RGB 数值与图片每个像素点上的 RGB 数值,生成对应的图片</div>
      <div class="main">
        <div id="rgb-inputs">
          <div>
            R:
            <select data-key="r">
              <option value="ge">&gt;=</option>
              <option value="le">&lt;=</option>
              <option value="eq">===</option>
            </select>
            <input data-key="r" type="number" min="0" max="255" />
          </div>
          <div>
            G:
            <select data-key="g">
              <option value="ge">&gt;=</option>
              <option value="le">&lt;=</option>
              <option value="eq">===</option>
            </select>
            <input data-key="g" type="number" min="0" max="255" />
          </div>
          <div>
            B:
            <select data-key="b">
              <option value="ge">&gt;=</option>
              <option value="le">&lt;=</option>
              <option value="eq">===</option>
            </select>
            <input data-key="b" type="number" min="0" max="255" />
          </div>
          <div>
            <button id="create-btn">生 成</button>
          </div>
        </div>
        <div class="img-box" id="modify"></div>
      </div>
    </div>

    <script lang="javascript">
      const rgbOpts = {
        r: { val: undefined, act: 'ge' },
        g: { val: undefined, act: 'ge' },
        b: { val: undefined, act: 'ge' }
      }

      document.getElementById('rgb-inputs').addEventListener('change', e => {
        const target = e.target
        const key = target.dataset.key

        if (key && Object.prototype.hasOwnProperty.call(rgbOpts, key)) {
          const tag = target.tagName.toUpperCase()
          if (tag === 'INPUT') {
            rgbOpts[key].val = parseInt(target.value)
          } else if (tag === 'SELECT') {
            rgbOpts[key].act = target.value
          }
        }
      })

      let originImgDom
      document.getElementById('upload-btn').addEventListener('change', e => {
        const file = e.target.files[0]
        const reader = new FileReader()
        reader.readAsDataURL(file)
        reader.onload = () => {
          if (!originImgDom) {
            originImgDom = document.createElement('img')
            originImgDom.src = reader.result
            document.getElementById('origin').append(originImgDom)
          } else {
            originImgDom.src = reader.result
          }
        }
      })

      let modifyImgDom
      document.getElementById('create-btn').addEventListener('click', async () => {
        if (!originImgDom) return
        const img = await imageMatting(originImgDom.src)
        if (!modifyImgDom) {
          modifyImgDom = document.createElement('img')
          modifyImgDom.src = img
          document.getElementById('modify').append(modifyImgDom)
        } else {
          modifyImgDom.src = img
        }
      })

      function imageMatting(originImage) {
        return new Promise((resolve, reject) => {
          let cvs = document.createElement('canvas')
          const ctx = cvs.getContext('2d')
          const img = new Image()
          img.src = originImage
          img.onload = () => {
            const { width, height } = img
            cvs.width = width
            cvs.height = height
            ctx.drawImage(img, 0, 0, width, height)

            const originImageData = ctx.getImageData(0, 0, width, height)
            const data = originImageData.data

            for (let i = 0; i < data.length; i += 4) {
              const rgbObj = { r: data[i], g: data[i + 1], b: data[i + 2] }
              if (compare(rgbObj)) data[i + 3] = 0
            }
            ctx.putImageData(originImageData, 0, 0)
            resolve(cvs.toDataURL('image/png'))
            cvs = null
          }
          img.onerror = err => {
            reject(err)
          }
        })
      }
      function compare(rgbObj) {
        let isDiff = false
        const compareKeys = Object.keys(rgbOpts).filter(key => rgbOpts[key].val === 0 || rgbOpts[key].val)

        if (compareKeys.length) {
          isDiff = compareKeys.every(key => {
            let flag = false
            const { act, val } = rgbOpts[key]
            if (act === 'eq') {
              flag = rgbObj[key] === val
            } else if (act === 'ge') {
              flag = rgbObj[key] >= val
            } else if (act === 'le') {
              flag = rgbObj[key] <= val
            }
            return flag
          })
        }
        return isDiff
      }
    </script>
  </body>
</html>