JavaScript extracts similar colors and approximate colors

The project requires the extraction of similar colors in canvas, and the specific requirements will not be introduced. Here we will draw out and write an article separately.
In the project, the canvas here is actually a map. In order to demonstrate, there is no need to go on the map. You can directly load a picture with 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'


The picture here is one I found casually from Baidu picture, picture address

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

Then it is to specify the color used to compare similar colors. Here you can directly specify a color, but according to the normal idea, this color should be extracted from canvas. We can frame an area and calculate the average color value in this area. Here we need to use the frame selection function I wrote before again

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

Remember to add a button to move the binding event. Here you can also refer to the code in my last article

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

See the effect

The next step is to extract the real color value, which is used to extract the rgb value of a point on the canvas

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

Here, we frame a region, cycle through all the points in the selected region, and then directly calculate the average RGB of these points (although this may not be reasonable). In my last article, the box selection function provides four values: the XY coordinate of the starting point of the box selection, the box width and the box height. Here, we write these four values into an array and pass it to our function to calculate the average value, as follows

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

Our function for calculating the average value is directly based on this array to facilitate all points in the box

function computedSquareColor(canvasDom, selectedArea) {
  const ctx = canvasDom.getContext('2d')
  //Traverse all pixels in the region, calculate the rgb value, and get the average rgb value
  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]
}


The location of the code can refer to my previous article. Now let's see the effect

Note that cross domain errors may be reported here. Refer to the following articles for solutions

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

Check whether this color is the color we extracted

It is indeed the color near the sky. For better observation, we add a node next to it to display this color

Well, the next step is to extract the approximate color. It still depends on our selection. Select the color of each point in the reading box, and then compare it. The points that meet the approximate color conditions will be marked. What are the conditions for the approximate color? Here is an article for reference

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

It is very detailed about approximate color. Thank you for your great article. However, the original text is written in C + +, and we need to rewrite it into JavaScript

//For LAB model calculation
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

//Color correction
function gamma(colorX) {
  //Determine whether it is not a floating point number
  if (~~colorX === colorX) {
    colorX = parseFloat(colorX + '')
  }
  return colorX > 0.04045
    ? Math.pow((colorX + 0.055) / 1.055, 2.4)
    : colorX / 12.92
}

//Convert RGB to 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 to 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]
}

//Chromaticity calculation
function computeCaidu(colorA, colorB) {
  return Math.pow(colorA * colorA + colorB * colorB, 0.5)
}

//Hue angle calculation
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
}

//Compare the approximation of color value and use CIEDE2000 color difference formula
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]

  //Principle and application of modern color technology p88 reference constant
  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)
  //When the weight and color value change regularly, the human eye observation is not regular, because the human eye perceives the color of different channels differently. Increasing the weight can alleviate this problem
  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)

  //Calculate weighting function
  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

  //Calculate 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)

  //Reference constant E00
  return Math.pow(
    L_item * L_item + C_item * C_item + H_item * H_item + RT * C_item * H_item,
    0.5
  )
}

//Calculate the approximation of two RGB colors, and only call the methods in this file, which only provides a reference for the calling process
function differenceRGB(rgbA, rgbB) {
  let xyzA = RGB2XYZ(...rgbA)
  let xyzB = RGB2XYZ(...rgbB)
  let labA = XYZ2LAB(...xyzA)
  let labB = XYZ2LAB(...xyzB)
  return differenceColor(labA, labB)
}

I won't introduce the source code in detail here. The principle is still suggested to look at the link I put above. Here, I just rewrite the C + + code into JavaScript code.
When using, just call the last function in the above code directly. Pass in two arrays. The rgb value ([R, G, B]) in the array
I'll write it as a separate file and then reference it in html


Add a button to frame the selection range (css writes as you like)

<button id="screen-square">Box selection range</button>

Then, because the function of the screenshot box needs to be used again, it is recommended to separate this function here. At the end of the screenshot, the function of the screenshot box needs to give a signal back, and then carry out other operations. At this time, it is best to use the callback function. It is easy to use here, and an interval is directly used to monitor whether the screenshot is completed

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
 ···

Pay attention to the location of the code
Now, we will directly write the click event of the selection range

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)
 })

The code here adds a canvas box to draw the recognition result
So our last job is to write the squareanalyze function, which mainly completes these things
1. Traverse the rgb values of all points within the selected range, calculate the color value approximation with the selected color, and filter the qualified points
2. Draw the qualified points in 1 on the new canvas for display to the user
This function has certain proficiency requirements for the application of canvas. I won't expand it in detail here. I'm also a half bucket of water myself. Go directly to the source code first

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

   //canvasDom is the original drawing, and squareDom is used to draw
   let squareCtx = squareDom.getContext('2d')
   let ctx = canvasDom.getContext('2d')

   //Fill background
   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)

   //The whole area is identified as a whole, and the whole area will be marked in the form of face
   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('In circulation')
     }
     return squareImgData
   }

   //Change canvas pixel color
   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
   }
 }

There are two variables at the top. Alw is the tolerance, which is related to the calculation of color value approximation. Generally speaking, if alw is 15, there is a 15% difference between the color values of the two colors allowed to be compared.
Stripe is the stride, which defaults to 1
In this way, let's finish this function. Now let's see the effect

Here, a part of the tree is intercepted, and then the road extending on the right is selected. Under the tolerance of 15, the final test result is still good, and a similar part of the color is successfully extracted (because there are shadows, there are more blank parts, which is only the comparison of color values, not AI)
Another thing to note is that the range of our screenshot is clientX taken directly, and we have added a row of buttons at the top of the page, so at this time, the position coordinates can not accurately correspond to the corresponding point coordinates on the canvas. You should pay attention to your own treatment, subtract the distance above, and here is the complete code

<!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">Extract color</button>
    <div id="show-color"></div>
    <button id="screen-square">Box selection range</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')
      //Traverse all pixels in the region, calculate the rgb value, and get the average rgb value
      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 is the original drawing, and squareDom is used to draw
      let squareCtx = squareDom.getContext('2d')
      let ctx = canvasDom.getContext('2d')

      //Fill background
      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)

      //The whole area is identified as a whole, and the whole area will be marked in the form of face
      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('In circulation')
        }
        return squareImgData
      }

      //Change canvas pixel color
      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>

Note that there is also colorTool. The complete code has been pasted on it
If you fail to run successfully, you must take a good look at what is wrong. Generally speaking, there is no problem

Keywords: Javascript Front-end canvas

Added by Brenden Frank on Fri, 04 Feb 2022 15:08:36 +0200