如果您需要获取单个像素的平均颜色,而不是矩形区域的颜色,请看一下另一个问题:
???? 鼠标悬停时从画布获取像素颜色
正如你所说,你需要获取图像中矩形区域的颜色,我假设您的意思是您需要获取给定区域的平均颜色,而不是单个像素的颜色。
无论如何,两者都是以非常相似的方式完成的:
???? 从图像或画布中获取单个像素的颜色/值
要获取单个像素的颜色,您首先需要将该图像绘制到画布上:
const image = document.getElementById('image');
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
const width = image.width;
const height = image.height;
canvas.width = width;
canvas.height = height;
context.drawImage(image, 0, 0, width, height);
然后获取单个像素的值,如下所示:
const data = context.getImageData(X, Y, 1, 1).data;
// RED = data[0]
// GREEN = data[1]
// BLUE = data[2]
// ALPHA = data[3]
✂️ 获取图像或画布上某个区域的平均颜色/值
你需要使用同样的CanvasRenderingContext2D.getImageData()获取更宽(多像素)区域的值,您可以通过更改其第三个和第四个参数来实现。该函数的签名是:
ImageData ctx.getImageData(sx, sy, sw, sh);
-
sx
:从中提取 ImageData 的矩形左上角的 x 坐标。
-
sy
:从中提取 ImageData 的矩形左上角的 y 坐标。
-
sw
:将从中提取图像数据的矩形的宽度。
-
sh
:将从中提取 ImageData 的矩形的高度。
你可以看到它返回一个ImageData
目的,不管那是什么。这里重要的部分是该对象有一个.data
属性包含我们所有的像素值。
但请注意.data
属性是一维的Uint8ClampedArray,这意味着所有像素的组件都已被展平,因此您将得到如下所示的结果:
假设您有一个如下所示的 2x2 图像:
RED PIXEL | GREEN PIXEL
BLUE PIXEL | TRANSPARENT PIXEL
然后,你会像这样得到它们:
[ 255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 0, 0, 0, 0 ]
| RED PIXEL | GREEN PIXEL | BLUE PIXEL | TRANSPAERENT PIXEL |
| 1ST PIXEL | 2ND PIXEL | 3RD PIXEL | 4TH PIXEL |
✨ 让我们看看它的实际效果
const avgSolidColor = document.getElementById('avgSolidColor');
const avgAlphaColor = document.getElementById('avgAlphaColor');
const avgSolidWeighted = document.getElementById('avgSolidWeighted');
const avgSolidColorCode = document.getElementById('avgSolidColorCode');
const avgAlphaColorCode = document.getElementById('avgAlphaColorCode');
const avgSolidWeightedCOde = document.getElementById('avgSolidWeightedCode');
const brush = document.getElementById('brush');
const image = document.getElementById('image');
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
const width = image.width;
const height = image.height;
const BRUSH_SIZE = brush.offsetWidth;
const BRUSH_CENTER = BRUSH_SIZE / 2;
const MIN_X = image.offsetLeft + 4;
const MAX_X = MIN_X + width - BRUSH_SIZE;
const MIN_Y = image.offsetTop + 4;
const MAX_Y = MIN_Y + height - BRUSH_SIZE;
canvas.width = width;
canvas.height = height;
context.drawImage(image, 0, 0, width, height);
function sampleColor(clientX, clientY) {
const brushX = Math.max(Math.min(clientX - BRUSH_CENTER, MAX_X), MIN_X);
const brushY = Math.max(Math.min(clientY - BRUSH_CENTER, MAX_Y), MIN_Y);
const imageX = brushX - MIN_X;
const imageY = brushY - MIN_Y;
let R = 0;
let G = 0;
let B = 0;
let A = 0;
let wR = 0;
let wG = 0;
let wB = 0;
let wTotal = 0;
const data = context.getImageData(imageX, imageY, BRUSH_SIZE, BRUSH_SIZE).data;
const components = data.length;
for (let i = 0; i < components; i += 4) {
// A single pixel (R, G, B, A) will take 4 positions in the array:
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
const a = data[i + 3];
// Update components for solid color and alpha averages:
R += r;
G += g;
B += b;
A += a;
// Update components for alpha-weighted average:
const w = a / 255;
wR += r * w;
wG += g * w;
wB += b * w;
wTotal += w;
}
const pixelsPerChannel = components / 4;
// The | operator is used here to perform an integer division:
R = R / pixelsPerChannel | 0;
G = G / pixelsPerChannel | 0;
B = B / pixelsPerChannel | 0;
wR = wR / wTotal | 0;
wG = wG / wTotal | 0;
wB = wB / wTotal | 0;
// The alpha channel need to be in the [0, 1] range:
A = A / pixelsPerChannel / 255;
// Update UI:
requestAnimationFrame(() => {
brush.style.transform = `translate(${ brushX }px, ${ brushY }px)`;
avgSolidColorCode.innerText = avgSolidColor.style.background
= `rgb(${ R }, ${ G }, ${ B })`;
avgAlphaColorCode.innerText = avgAlphaColor.style.background
= `rgba(${ R }, ${ G }, ${ B }, ${ A.toFixed(2) })`;
avgSolidWeightedCode.innerText = avgSolidWeighted.style.background
= `rgb(${ wR }, ${ wG }, ${ wB })`;
});
}
document.onmousemove = (e) => sampleColor(e.clientX, e.clientY);
sampleColor(MIN_X, MIN_Y);
body {
margin: 0;
height: 100vh;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
cursor: crosshair;
font-family: monospace;
}
#image {
border: 4px solid white;
border-radius: 2px;
box-shadow: 0 0 32px 0 rgba(0, 0, 0, .25);
width: 150px;
box-sizing: border-box;
}
#brush {
position: absolute;
top: 0;
left: 0;
pointer-events: none;
width: 50px;
height: 50px;
background: magenta;
mix-blend-mode: exclusion;
}
#samples {
position: relative;
list-style: none;
padding: 0;
width: 250px;
}
#samples::before {
content: '';
position: absolute;
top: 0;
left: 27px;
width: 2px;
height: 100%;
background: black;
border-radius: 1px;
}
#samples > li {
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
padding-left: 56px;
}
#samples > li + li {
margin-top: 8px;
}
.sample {
position: absolute;
top: 50%;
left: 16px;
transform: translate(0, -50%);
display: block;
width: 24px;
height: 24px;
border-radius: 100%;
box-shadow: 0 0 16px 4px rgba(0, 0, 0, .25);
margin-right: 8px;
}
.sampleLabel {
font-weight: bold;
margin-bottom: 8px;
}
.sampleCode {
}
<img id="image" src="data:image/gif;base64,R0lGODlhSwBLAPEAACMfIO0cJAAAAAAAACH/C0ltYWdlTWFnaWNrDWdhbW1hPTAuNDU0NTUAIf4jUmVzaXplZCBvbiBodHRwczovL2V6Z2lmLmNvbS9yZXNpemUAIfkEBQAAAgAsAAAAAEsASwAAAv+Uj6mb4A+QY7TaKxvch+MPKpC0eeUUptdomOzJqnLUvnFcl7J6Pzn9I+l2IdfII8DZiCnYsYdK4qRTptAZwQKRVK71CusOgx2nFRrlhMu+33o2NEalC6S9zQvfi3Mlnm9WxeQ396F2+HcQsMjYGEBRVbhy5yOp6OgIeVIHpEnZyYCZ6cklKBJX+Kgg2riqKoayOWl2+VrLmtDqBptIOjZ6K4qAeSrL8PcmHExsgMs2dpyIxPpKvdhM/YxaTMW2PGr9GP76BN3VHTMurh7eoU14jsc+P845Vn6OTb/P/I68iYOfwGv+JOmRNHBfsV5ujA1LqM4eKDoNvXyDqItTxYX/DC9irKBlIhkKGPtFw1JDiMeS7CqWqySPZcKGHH/JHGgIpb6bCl1O0LmT57yCOqoI5UcU0YKjPXmFjMm0ZQ4NIVdGBdZRi9WrjLxJNMY1Yr4dYeuNxWApl1ALHb+KDHrTV1owlriedJgSr4Cybu/9dFiWYAagsqAGVkkzaZTAuqD9ywKWMUG9dCO3u2zWpVzIhpW122utZlrHnTN+Bq2Mqrlnqh8CQ+0Mrq3Kc++q7eo6dlB3rLuh3abPVbbbI2mxBdhWdsZhid8cr0oy9F08q0k5FXSadiyL1mF5z51a8VsQOp3/LlodkBfzmzWf2bOrtfzr48k/1hupDaLa9rUbO+zlwndfaOCURAXRNaCBqBT2BncJakWfTzSYkmCEFr60RX0V8sKaHOltCBJ1tAAFYhHaVVbig3jxp0IBADs=" >
<div id="brush"></div>
<ul id="samples">
<li>
<span class="sample" id="avgSolidColor"></span>
<div class="sampleLabel">avgSolidColor</div>
<div class="sampleCode" id="avgSolidColorCode">rgb(-, -, -)</div>
</li>
<li>
<span class="sample" id="avgAlphaColor"></span>
<div class="sampleLabel">avgAlphaColor</div>
<div class="sampleCode" id="avgAlphaColorCode">rgba(-, -, -, -)</div>
</li>
<li>
<span class="sample" id="avgSolidWeighted"></span>
<div class="sampleLabel">avgSolidWeighted</div>
<div class="sampleCode" id="avgSolidWeightedCode">rgba(-, -, -, -)</div>
</li>
</ul>
⚠️ 注意我使用小数据 URI 来避免Cross-Origin
如果我尝试使用更长的数据 URI,包含外部图像或大于允许的答案,则会出现问题。
????️这些普通的颜色看起来很奇怪,不是吗? (@SMT 的评论)
如果你把画笔移到左上角,你会看到avgSolidColor
几乎是黑色的。这是因为该区域中的大多数像素是完全透明的,因此它们的值恰好或非常接近0, 0, 0, 255
。这意味着对于我们处理的每一个,R
, G
and B
不改变或改变很少,同时pixelsPerChannel
仍然考虑到它们,所以我们最终除以一个小数字(因为我们添加0
对于大多数人来说)是一个很大的值(画笔中的像素总数),这给了我们一个非常接近的值0
(黑色的)。
例如,如果我们有两个像素,0, 0, 0, 255
and 255, 0, 0, 0
,通过观察它们,我们可以期望平均值R
渠道成为255
(因为其中之一是完全透明的)。然而,这将是(0 + 255) / 2 | 1 = 127
。但别担心,我们接下来会看看如何做到这一点。
另一方面,avgAlphaColor
看起来是灰色的。嗯,这实际上不是真的,只是looks灰色,因为我们现在使用 Alpha 通道,这使其半透明并允许我们看到页面的背景,在本例中为白色。
???? Alpha 加权平均值(@SMT 评论的解决方案)
那么,我们能做些什么来解决这个问题呢?好吧,事实证明我们只需要使用 alpha 通道作为我们(现在加权)平均值的权重:
这意味着如果一个像素r, g, b, a
, where a
是在区间内[0, 255]
,我们将像这样更新我们的变量:
const w = a / 255; // w is in the interval [0, 1]
wR += r * w;
wG += g * w;
wB += b * w;
wTotal += w;
请注意像素越透明(w
越接近 0),我们在计算中就越不关心它的值。