项目要求提取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')
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
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
}
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]
}
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
}
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]
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
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)
return Math.pow(
L_item * L_item + C_item * C_item + H_item * H_item + RT * C_item * H_item,
0.5
)
}
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
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
}
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')
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
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
}
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哦,上面已经贴过完整代码了 如果没能成功运行,一定要好好看看是哪里弄错了,一般来讲是没啥问题的
|