JavaScript提取相近颜色,近似色

项目要求提取canvas中相近的颜色,具体的需求就不介绍了,这里单独抽出来写一篇文章。
在项目里面,这里的canvas其实是地图,这里为了演示就没必要上地图了,直接用canvas加载一张图片

const canvasDom = document.createElement("canvas")
canvasDom.width = 1200
canvasDom.height = 960
document.body.appendChild(canvasDom)
const ctx = canvasDom.getContext("2d")
const img = new Image()
img.onload = () => {
  ctx.drawImage(img, 0, 0)
}
img.src = 'show.jpg'

在这里插入图片描述
这里的图片是我从百度图片上随便找的一张,图片地址

https://www.vcg.com/creative/1030316923

然后就是指定用于比较近似色的颜色了,这里可以直接指定一个颜色,但是按照正常的思路来说,这个颜色应该是从canvas中提取出来的,我们可以框选一个区域,计算这个区域内的平均颜色色值,这里要再次用到我之前写过的框选功能

https://blog.csdn.net/luoluoyang23/article/details/122653363

记得添加一个按钮,将绑定事件移过去,这里也可以参考我上篇文章的代码

https://blog.csdn.net/luoluoyang23/article/details/122763696

看看效果
在这里插入图片描述
接下来就是真正的提取颜色值了,提取canvas上某个点的rgb值要使用

canvasDom.getContext('2d').getImageData(PointX, PointY, 1, 1).data

而我们这里是框选了一个区域,通过循环遍历框选区域内所有的点,然后直接暴力地计算这些点的RGB的平均值(虽然这样做可能并不合理)。在我的上篇文章中,框选函数提供了四个值,框选框的起始点的XY坐标,框宽,框高。这里我们将这四个值写成一个数组传给我们计算平均值的函数,如下

const list = [canvasX, canvasY, canvasWidth, canvasHeight]

我们计算平均值的函数直接根据这个数组来便利框内所有的点

function computedSquareColor(canvasDom, selectedArea) {
  const ctx = canvasDom.getContext('2d')
  //遍历区域内所有像素点,计算rgb值,得到平均rgb值
  let colorR = 0
  let colorG = 0
  let colorB = 0
  for (let i = 0; i < selectedArea[2]; i++) {
    for (let j = 0; j < selectedArea[3]; j++) {
      let mapPointX = selectedArea[0] + i
      let mapPointY = selectedArea[1] + j
      let colorArray = ctx.getImageData(mapPointX, mapPointY, 1, 1).data
      colorR += colorArray[0]
      colorG += colorArray[1]
      colorB += colorArray[2]
    }
  }
  colorR = Math.ceil(colorR / (selectedArea[2] * selectedArea[3]))
  colorG = Math.ceil(colorG / (selectedArea[2] * selectedArea[3]))
  colorB = Math.ceil(colorB / (selectedArea[2] * selectedArea[3]))
  return [colorR, colorG, colorB]
}

在这里插入图片描述
代码的位置可以参考我前面的文章,现在我们看看效果
在这里插入图片描述
注意,这里可能报跨域错误,解决办法参考下面的文章

https://blog.csdn.net/Treeee_/article/details/111996118

检查这个颜色是否是我们提取的颜色
在这里插入图片描述
确实是天空附近的颜色,为了更好的观察,我们在旁边添加一个节点来显示这个颜色
在这里插入图片描述
好了,接下来就是提取近似色了,依旧依赖我们的款选,读框选中每一个点的颜色,然后进行比较,符合近似色条件的点就进行标注,那么近似色的条件是什么呢?这里参考了一篇文章

https://www.jianshu.com/p/a13b96d00c71

里面关于近似色讲得很详细。这里也感谢大大的文章。不过原文是用C++写的,我们需要改写成JavaScript

//用于LAB模型计算
const param_1_3 = 1.0 / 3.0
const param_16_116 = 16.0 / 116.0
const Xn = 0.950456
const Yn = 1.0
const Zn = 1.088754

//色彩矫正
function gamma(colorX) {
  //判断是否不是浮点数
  if (~~colorX === colorX) {
    colorX = parseFloat(colorX + '')
  }
  return colorX > 0.04045
    ? Math.pow((colorX + 0.055) / 1.055, 2.4)
    : colorX / 12.92
}

//RGB转换成XYZ
function RGB2XYZ(colorR, colorG, colorB) {
  let colorX = 0.4124564 * colorR + 0.3575761 * colorG + 0.1804375 * colorB
  let colorY = 0.2126729 * colorR + 0.7151522 * colorG + 0.072175 * colorB
  let colorZ = 0.0193339 * colorR + 0.119192 * colorG + 0.9503041 * colorB
  return [colorX, colorY, colorZ]
}

//XYZ转换成LAB
function XYZ2LAB(colorX, colorY, colorZ) {
  colorX = colorX / Xn
  colorY = colorY / Yn
  colorZ = colorZ / Zn

  let fX =
    colorX > 0.008856
      ? Math.pow(colorX, param_1_3)
      : 7.787 * colorX + param_16_116
  let fY =
    colorY > 0.008856
      ? Math.pow(colorY, param_1_3)
      : 7.787 * colorY + param_16_116
  let fZ =
    colorZ > 0.008856
      ? Math.pow(colorZ, param_1_3)
      : 7.787 * colorZ + param_16_116
  let colorL = parseFloat('116') * fY - parseFloat('16')
  colorL = colorL > parseFloat('0.0') ? colorL : parseFloat('0.0')
  let colorA = parseFloat('500') * (fX - fY)
  let colorB = parseFloat('200') * (fY - fZ)
  return [colorL, colorA, colorB]
}

//彩度计算
function computeCaidu(colorA, colorB) {
  return Math.pow(colorA * colorA + colorB * colorB, 0.5)
}

//色调角计算
function computeSeDiaoJiao(colorA, colorB) {
  if (colorA === 0) return 90

  const h = (180 / Math.PI) * Math.atan(colorB / colorA)
  let hab
  if (colorA > 0 && colorB > 0) {
    hab = h
  } else if (colorA < 0 && colorB > 0) {
    hab = 180 + h
  } else if (colorA < 0 && colorB < 0) {
    hab = 180 + h
  } else {
    hab = 360 + h
  }
  return hab
}

//比较色值近似度,使用CIEDE2000色差公式
function differenceColor(firstColor, secondColor) {
  let L1 = firstColor[0]
  let A1 = firstColor[1]
  let B1 = firstColor[2]
  let L2 = secondColor[0]
  let A2 = secondColor[1]
  let B2 = secondColor[2]

  //《现代颜色技术原理及应用》p88参考常量
  let delta_LL, delta_CC, delta_hh, delta_HH
  let kL, kC, kH
  let SL, SC, SH, T
  kL = parseFloat('1')
  kC = parseFloat('1')
  kH = parseFloat('1')
  let mean_Cab = (computeCaidu(A1, B1) + computeCaidu(A2, B2)) / 2
  let mean_Cab_pow7 = Math.pow(mean_Cab, 7)
  //权重,色值规律变化时人眼观察并不是规律的,因为人眼对不同通道颜色感知不同,增加权重缓解这个问题
  let G =
    0.5 * (1 - Math.pow(mean_Cab_pow7 / (mean_Cab_pow7 + Math.pow(25, 7)), 0.5))
  let LL1 = L1
  let aa1 = A1 * (1 + G)
  let bb1 = B1
  let LL2 = L2
  let aa2 = A2 * (1 + G)
  let bb2 = B2
  let CC1 = computeCaidu(aa1, bb1)
  let CC2 = computeCaidu(aa2, bb2)
  let hh1 = computeSeDiaoJiao(aa1, bb1)
  let hh2 = computeSeDiaoJiao(aa2, bb2)
  delta_LL = LL1 - LL2
  delta_CC = CC1 - CC2
  delta_hh = computeSeDiaoJiao(aa1, bb1) - computeSeDiaoJiao(aa2, bb2)
  delta_HH = 2 * Math.sin((Math.PI * delta_hh) / 360) * Math.pow(CC1 * CC2, 0.5)

  //计算加权函数
  let mean_LL = (LL1 + LL2) / 2
  let mean_CC = (CC1 + CC2) / 2
  let mean_hh = (hh1 + hh2) / 2
  SL =
    1 +
    (0.015 * Math.pow(mean_LL - 50, 2)) /
      Math.pow(20 + Math.pow(mean_LL - 50, 2), 0.5)
  SC = 1 + 0.045 * mean_CC
  T =
    1 -
    0.17 * Math.cos(((mean_hh - 30) * Math.PI) / 180) +
    0.24 * Math.cos((2 * mean_hh * Math.PI) / 180) +
    0.32 * Math.cos(((3 * mean_hh + 6) * Math.PI) / 180) -
    0.2 * Math.cos(((4 * mean_hh - 63) * Math.PI) / 180)
  SH = 1 + 0.015 * mean_CC * T

  //计算RT
  let mean_CC_pow7 = Math.pow(mean_CC, 7)
  let RC = 2 * Math.pow(mean_CC_pow7 / (mean_CC_pow7 + Math.pow(25, 7)), 0.5)
  let delta_xita = 30 * Math.exp(-Math.pow((mean_hh - 275) / 25, 2))
  let RT = -Math.sin((2 * delta_xita * Math.PI) / 180) * RC

  let L_item, C_item, H_item
  L_item = delta_LL / (kL * SL)
  C_item = delta_CC / (kC * SC)
  H_item = delta_HH / (kH * SH)

  //参考常量E00
  return Math.pow(
    L_item * L_item + C_item * C_item + H_item * H_item + RT * C_item * H_item,
    0.5
  )
}

//计算两个RGB颜色近似程度,仅调用该文件中的方法,只是提供调用流程参考
function differenceRGB(rgbA, rgbB) {
  let xyzA = RGB2XYZ(...rgbA)
  let xyzB = RGB2XYZ(...rgbB)
  let labA = XYZ2LAB(...xyzA)
  let labB = XYZ2LAB(...xyzB)
  return differenceColor(labA, labB)
}

这里就不详细介绍源码了,原理还是建议看我上面放的链接,这边只是单纯将C++的代码改写成了JavaScript的代码。
在使用的时候直接调用上面代码中最后一个函数就行了,传入两个数组,数组里面时rgb值([R, G, B])
我这边将其单独写成一个文件,然后再html中引用
在这里插入图片描述
在这里插入图片描述
添加一个按钮,用来框选范围(css按自己喜欢来写)

<button id="screen-square">框选范围</button>

在这里插入图片描述

然后因为要再次使用到截图框的函数,因此这里建议单独将这个函数拆开,在截图结束时截图框的函数需要给一个信号回去,这时候再进行其它操作,这个时候用回调函数最好,这里图省事,直接用一个interval来监听是否截图完成

let screenFinished = false
document.getElementById('screen-button').addEventListener('click', (e) => {
  screenEvent(e)
   const colorInterval = setInterval(() => {
     if (screenFinished) {
       const list = [canvasX, canvasY, canvasWidth, canvasHeight]
       selectColor = computedSquareColor(canvasDom, list)
       document.getElementById("show-color").style.backgroundColor = 'rgb(' + selectColor + ')'
       clearInterval(colorInterval)
     }
   }, 100)
 })
 ···
 function screenEvent(e) {
    screenFinished = false
    const mousedownEvent = (e) => {
 ···
	 window.removeEventListener("mousedown", mousedownEvent)
	 document.body.removeChild(divDom)
	 screenFinished = true
 ···

注意代码位置
现在,我们便直接写截选范围的点击事件

document.getElementById('screen-square').addEventListener('click', (e) => {
   screenEvent(e)
   const squareInterval = setInterval(() => {
     if (screenFinished) {
       const squareDom = document.createElement('canvas')
       squareDom.width = canvasWidth
       squareDom.height = canvasHeight
       squareDom.style.position = 'absolute'
       squareDom.style.left = canvasX
       squareDom.style.top = canvasY
       document.body.appendChild(squareDom)

       const list = [canvasX, canvasY, canvasWidth, canvasHeight]
       squareAnalyse(canvasDom, squareDom, list)

       clearInterval(squareInterval)
     }
   }, 100)
 })

这里的代码添加了一个canvas框,用于绘制识别结果
那么我们最后的工作就是编写squareAnalyse函数了,它主要完成这么几件事情
1、遍历框选范围内所有点的rgb值,将其与选中的颜色进行色值近似度计算,筛选符合条件的点
2、将1中符合条件的点在新的canvas上进行绘制,用于展示给用户
这个函数对canvas的应用有一定熟练度的要求,我这里不详细展开了,我自己也是半桶水,先直接上源码

async function squareAnalyse(canvasDom, squareDom, selectedArea) {
   let alw = 15
   let stride = 1

   //canvasDom是原图,squareDom是用来绘制的
   let squareCtx = squareDom.getContext('2d')
   let ctx = canvasDom.getContext('2d')

   //填充背景
   squareCtx.fillStyle = 'rgba(255,255,255,0.5)'
   squareCtx.fillRect(0, 0, canvasWidth, canvasHeight)

   let squareImgData = squareCtx.getImageData(0, 0, canvasWidth, canvasHeight)
   await areaTotalAnalyse()
   squareCtx.putImageData(squareImgData, 0, 0)

   //对整个区域进行整体的识别,会以面的形式标记整个区域
   async function areaTotalAnalyse() {
     for (let i = 0; i < selectedArea[2]; i += stride) {
       for (let j = 0; j < selectedArea[3]; j += stride) {
         let mapPointX = selectedArea[0] + i
         let mapPointY = selectedArea[1] + j
         let rgbaArray = ctx.getImageData(mapPointX, mapPointY, 1, 1).data
         let rgbArray = [rgbaArray[0], rgbaArray[1], rgbaArray[2]]
         if ((await differenceRGB(selectColor, rgbArray)) <= alw) {
           squareImgData = await editCanvas(i, j, canvasWidth, squareImgData)
         }
       }
       console.log('循环中')
     }
     return squareImgData
   }

   //改变canvas像素点颜色
   async function editCanvas(i, j, canvasWidth, squareImgData) {
     squareImgData.data[4 * j * canvasWidth + 4 * i] = 0
     squareImgData.data[4 * j * canvasWidth + 4 * i + 1] = 0
     squareImgData.data[4 * j * canvasWidth + 4 * i + 2] = 0
     squareImgData.data[4 * j * canvasWidth + 4 * i + 3] = 255
     return squareImgData
   }
 }

最上方有两个变量,alw是容差,与色值近似度计算有关,通俗点说,如果alw是15,那么就是允许比较的两个颜色色值有15%的差距。
stride是步幅,这个就默认为1就行
这样,这个功能就让我们写完了,现在来看看效果
在这里插入图片描述
这里截取了树的一部分,然后框选了右侧延申的马路,在容差15的情况下,最后测试的结果还是不错的,成功提取了相近的一部分颜色(因为有阴影,所以空白部分比较多,这只是色值比较,不是AI)
还有一点需要注意的是,我们截图的范围是直接拿的clientX,而我们在页面最上方添加了一行按钮,所以这个时候这个这个位置坐标并不能很准确的对应canvas上对应的点坐标,大家要注意自己处理一下,减去上方的距离就行了,下面是完整代码

<!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>
    html,
    body {
      margin: 0;
      padding: 0;
    }

    #screenshot {
      border: 3px solid white;
    }

    #box {
      width: 100%;
      height: 5px;
    }

    #screen-button {
      float: left;
    }

    #show-color {
      float: left;
      width: 20px;
      height: 20px;
      margin-top: 2px;
      margin-left: 10px;
      margin-right: 10px;
    }
  </style>
</head>

<body>
  <div id="box">
    <button id="screen-button">提取颜色</button>
    <div id="show-color"></div>
    <button id="screen-square">框选范围</button>
  </div>
  <br />
  <script src='colorTool.js'></script>
  <script>
    let canvasWidth, canvasHeight
    let canvasX, canvasY
    let selectColor
    let screenFinished = false

    const canvasDom = document.createElement("canvas")
    canvasDom.width = 1000
    canvasDom.height = 800
    document.body.appendChild(canvasDom)
    const ctx = canvasDom.getContext("2d")
    const img = new Image()
    img.onload = () => {
      ctx.drawImage(img, 0, 0)
    }
    img.src = 'show.jpg'

    document.getElementById('screen-button').addEventListener('click', (e) => {
      screenEvent(e)
      const colorInterval = setInterval(() => {
        if (screenFinished) {
          const list = [canvasX, canvasY, canvasWidth, canvasHeight]
          selectColor = computedSquareColor(canvasDom, list)
          document.getElementById("show-color").style.backgroundColor = 'rgb(' + selectColor + ')'
          clearInterval(colorInterval)
        }
      }, 100)
    })

    document.getElementById('screen-square').addEventListener('click', (e) => {
      screenEvent(e)
      const squareInterval = setInterval(() => {
        if (screenFinished) {
          const squareDom = document.createElement('canvas')
          squareDom.width = canvasWidth
          squareDom.height = canvasHeight
          squareDom.style.position = 'absolute'
          squareDom.style.left = canvasX
          squareDom.style.top = canvasY
          document.body.appendChild(squareDom)

          const list = [canvasX, canvasY, canvasWidth, canvasHeight]
          squareAnalyse(canvasDom, squareDom, list)

          clearInterval(squareInterval)
        }
      }, 100)
    })

    function screenEvent(e) {
      screenFinished = false
      const mousedownEvent = (e) => {
        const [startX, startY] = [e.clientX, e.clientY]
        const divDom = document.createElement("div")
        divDom.id = 'screenshot'
        divDom.width = '1px'
        divDom.height = '1px'
        divDom.style.position = "absolute"
        canvasX = startX
        canvasY = startY
        divDom.style.top = startY + "px"
        divDom.style.left = startX + "px"
        document.body.appendChild(divDom)
        const moveEvent = (e) => {
          const moveX = e.clientX - startX
          const moveY = e.clientY - startY
          if (moveX > 0) {
            divDom.style.width = moveX + 'px'
            canvasWidth = moveX
          } else {
            divDom.style.width = -moveX + 'px'
            divDom.style.left = e.clientX + 'px'
            canvasWidth = -moveX
            canvasX = e.clientX
          }
          if (moveY > 0) {
            divDom.style.height = moveY + 'px'
            canvasHeight = moveY
          } else {
            divDom.style.height = -moveY + 'px'
            divDom.style.top = e.clientY + 'px'
            canvasHeight = -moveY
            canvasY = e.clientY
          }
        }
        window.addEventListener("mousemove", moveEvent)
        window.addEventListener("mouseup", () => {
          window.removeEventListener("mousemove", moveEvent)
          window.removeEventListener("mousedown", mousedownEvent)
          document.body.removeChild(divDom)
          screenFinished = true
        })
      }
      window.addEventListener("mousedown", mousedownEvent)
    }

    function computedSquareColor(canvasDom, selectedArea) {
      const ctx = canvasDom.getContext('2d')
      //遍历区域内所有像素点,计算rgb值,得到平均rgb值
      let colorR = 0
      let colorG = 0
      let colorB = 0
      for (let i = 0; i < selectedArea[2]; i++) {
        for (let j = 0; j < selectedArea[3]; j++) {
          let mapPointX = selectedArea[0] + i
          let mapPointY = selectedArea[1] + j
          let colorArray = ctx.getImageData(mapPointX, mapPointY, 1, 1).data
          colorR += colorArray[0]
          colorG += colorArray[1]
          colorB += colorArray[2]
        }
      }
      colorR = Math.ceil(colorR / (selectedArea[2] * selectedArea[3]))
      colorG = Math.ceil(colorG / (selectedArea[2] * selectedArea[3]))
      colorB = Math.ceil(colorB / (selectedArea[2] * selectedArea[3]))
      return [colorR, colorG, colorB]
    }

    async function squareAnalyse(canvasDom, squareDom, selectedArea) {
      let alw = 15
      let stride = 1

      //canvasDom是原图,squareDom是用来绘制的
      let squareCtx = squareDom.getContext('2d')
      let ctx = canvasDom.getContext('2d')

      //填充背景
      squareCtx.fillStyle = 'rgba(255,255,255,0.5)'
      squareCtx.fillRect(0, 0, canvasWidth, canvasHeight)

      let squareImgData = squareCtx.getImageData(0, 0, canvasWidth, canvasHeight)
      await areaTotalAnalyse()
      squareCtx.putImageData(squareImgData, 0, 0)

      //对整个区域进行整体的识别,会以面的形式标记整个区域
      async function areaTotalAnalyse() {
        for (let i = 0; i < selectedArea[2]; i += stride) {
          for (let j = 0; j < selectedArea[3]; j += stride) {
            let mapPointX = selectedArea[0] + i
            let mapPointY = selectedArea[1] + j
            let rgbaArray = ctx.getImageData(mapPointX, mapPointY, 1, 1).data
            let rgbArray = [rgbaArray[0], rgbaArray[1], rgbaArray[2]]
            if ((await differenceRGB(selectColor, rgbArray)) <= alw) {
              squareImgData = await editCanvas(i, j, canvasWidth, squareImgData)
            }
          }
          console.log('循环中')
        }
        return squareImgData
      }

      //改变canvas像素点颜色
      async function editCanvas(i, j, canvasWidth, squareImgData) {
        squareImgData.data[4 * j * canvasWidth + 4 * i] = 0
        squareImgData.data[4 * j * canvasWidth + 4 * i + 1] = 0
        squareImgData.data[4 * j * canvasWidth + 4 * i + 2] = 0
        squareImgData.data[4 * j * canvasWidth + 4 * i + 3] = 255
        return squareImgData
      }
    }
  </script>
</body>

</html>

注意,还有colorTool哦,上面已经贴过完整代码了
如果没能成功运行,一定要好好看看是哪里弄错了,一般来讲是没啥问题的

  • 5
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值