让我首先澄清一下问题陈述.看看这条推文:
https://twitter.com/jungledragon/status/926894337761345538
接下来,在推文中单击图像本身.在出现的灯箱中,它下方的菜单栏采用基于图像本身中实际像素的有意义颜色.即使在这种压力测试中,这对于所有光像素都是一个困难的图像,它在选择整体颜色方面做得很好1)表示图像的内容2)黑暗/对比度足以在其上放置白色文本:
在我知道Twitter有这个之前,我同时实施了一个类似的系统.查看下面的预览:
截图中的示例是乐观的,因为有很多情况下背景太亮.即使在我的截图中看到的看似积极的例子中,大部分时间它都没有通过AA或AAA对比度检查.
我目前的做法:
>每次运行一次,JS运行计算平均颜色
图像中的所有像素.请注意,平均颜色不是
必然是有意义的颜色,例如在边缘的情况下
蜘蛛的平均值接近白色.
>我将RGB值存储在数据库中
>在渲染页面(服务器端)时,我动态设置
使用公式的图像标题的背景颜色
我的公式是将RGB转换为HSL,然后特别操作S和L值.给它们一个缺口,使用最小/最大值来设置阈值.我尝试了无数的组合.
然而,这似乎是一场永无止境的斗争,因为色彩的黑暗和对比会受到人类感知的影响.
因此,我对Twitter似乎如何确定这一点有好奇心,特别是两个方面:
>找到有意义的主题颜色(与平均颜色或主色不同)
>以有意识的颜色(色调)调整有意义的颜色,但对比度足以在其上放置浅色文本,同时至少通过AA对比度检查.
我一直在搜索,但找不到有关其实施的任何信息.谁知道他们这样做了?或其他经过验证的方法来解决这个端到端的难题?
最佳答案 我看了一下Twitter的标记,看看我能找到什么,并且在浏览器的控制台中运行了一些代码之后,看起来Twitter在图像中的平面像素分布上采用了颜色平均值并对每个RGB进行了缩放通道值为64及以下.这提供了一种非常快速的方法来为轻文本创建高对比度背景,同时仍保持合理的颜色匹配.据我所知,Twitter没有进行任何先进的主题颜色检测,但我不能肯定地说.
这是我用来验证这个理论的快速而肮脏的演示.图像周围出现的顶部和左侧边框最初显示Twitter使用的颜色.运行代码段后,将显示带有计算颜色的右下边框. IE用户需要9.
function processImage(img)
{
var imageCanvas = new ImageCanvas(img);
var tally = new PixelTally();
for (var y = 0; y < imageCanvas.height; y += config.interval) {
for (var x = 0; x < imageCanvas.width; x += config.interval) {
tally.record(imageCanvas.getPixelColor(x, y));
}
}
var average = new ColorAverage(tally);
img.style.borderRightColor = average.toRGBStyleString();
img.style.borderBottomColor = average.toRGBStyleString();
}
function ImageCanvas(img)
{
var canvas = document.createElement('canvas');
this.context2d = canvas.getContext('2d');
this.width = canvas.width = img.naturalWidth;
this.height = canvas.height = img.naturalHeight;
this.context2d.drawImage(img, 0, 0, this.width, this.height);
this.getPixelColor = function (x, y) {
var pixel = this.context2d.getImageData(x, y, 1, 1).data;
return { red: pixel[0], green: pixel[1], blue: pixel[2] };
}
}
function PixelTally()
{
this.totalPixelCount = 0;
this.colorPixelCount = 0;
this.red = 0;
this.green = 0;
this.blue = 0;
this.luminosity = 0;
this.record = function (colors) {
this.luminosity += this.calculateLuminosity(colors);
this.totalPixelCount++;
if (this.isGreyscale(colors)) {
return;
}
this.red += colors.red;
this.green += colors.green;
this.blue += colors.blue;
this.colorPixelCount++;
};
this.getAverage = function (colorName) {
return this[colorName] / this.colorPixelCount;
};
this.getLuminosityAverage = function () {
return this.luminosity / this.totalPixelCount;
}
this.getNormalizingDenominator = function () {
return Math.max(this.red, this.green, this.blue) / this.colorPixelCount;
};
this.calculateLuminosity = function (colors) {
return (colors.red + colors.green + colors.blue) / 3;
};
this.isGreyscale = function (colors) {
return Math.abs(colors.red - colors.green) < config.greyscaleDistance
&& Math.abs(colors.red - colors.blue) < config.greyscaleDistance;
};
}
function ColorAverage(tally)
{
var lightness = config.lightness;
var normal = tally.getNormalizingDenominator();
var luminosityAverage = tally.getLuminosityAverage();
// We won't scale the channels up to 64 for darker images:
if (luminosityAverage < lightness) {
lightness = luminosityAverage;
}
this.red = (tally.getAverage('red') / normal) * lightness
this.green = (tally.getAverage('green') / normal) * lightness
this.blue = (tally.getAverage('blue') / normal) * lightness
this.toRGBStyleString = function () {
return 'rgb('
+ Math.round(this.red) + ','
+ Math.round(this.green) + ','
+ Math.round(this.blue) + ')';
};
}
function Configuration()
{
this.lightness = 64;
this.interval = 100;
this.greyscaleDistance = 15;
}
var config = new Configuration();
var indicator = document.getElementById('indicator');
document.addEventListener('DOMContentLoaded', function () {
document.forms[0].addEventListener('submit', function (event) {
event.preventDefault();
config.lightness = Number(this.elements['lightness'].value);
config.interval = Number(this.elements['interval'].value);
config.greyscaleDistance = Number(this.elements['greyscale'].value);
indicator.style.visibility = 'visible';
setTimeout(function () {
processImage(document.getElementById('image1'));
processImage(document.getElementById('image2'));
processImage(document.getElementById('image3'));
processImage(document.getElementById('image4'));
processImage(document.getElementById('image5'));
indicator.style.visibility = 'hidden';
}, 50);
});
});
label { display: block; }
img { border-width: 20px; border-style: solid; width: 200px; height: 200px; }
#image1 { border-color: rgb(64, 54, 47) white white rgb(64, 54, 47); }
#image2 { border-color: rgb(46, 64, 17) white white rgb(46, 64, 17); }
#image3 { border-color: rgb(64, 59, 46) white white rgb(64, 59, 46); }
#image4 { border-color: rgb(36, 38, 20) white white rgb(36, 38, 20); }
#image5 { border-color: rgb(45, 53, 64) white white rgb(45, 53, 64); }
#indicator { visibility: hidden; }
<form id="configuration_form">
<p>
<label>Lightness:
<input name="lightness" type="number" min="1" max="255" value="64">
</label>
<label>Pixel Sample Interval:
<input name="interval" type="number" min="1" max="255" value="100">
(Lower values are slower)
</label>
<label>Greyscale Distance:
<input name="greyscale" type="number" min="1" max="255" value="15">
</label>
<button type="submit">Run</button> (Wait for images to load first!)
</p>
<p id="indicator">Running...this may take a few moments.</p>
</form>
<p>
<img id="image1" crossorigin="Anonymous" src="https://pbs.twimg.com/media/DNz9fNqWAAAtoGu.jpg:large">
<img id="image2" crossorigin="Anonymous" src="https://pbs.twimg.com/media/DOdX8AGXUAAYYmq.jpg:large">
<img id="image3" crossorigin="Anonymous" src="https://pbs.twimg.com/media/DOYp0HQX4AEWcnI.jpg:large">
<img id="image4" crossorigin="Anonymous" src="https://pbs.twimg.com/media/DOQm1NzXkAEwxG7.jpg:large">
<img id="image5" crossorigin="Anonymous" src="https://pbs.twimg.com/media/DN6gVnpXUAIxlxw.jpg:large">
</p>
在确定图像的主色时,代码忽略了白色,黑色和灰色像素,这样即使降低了颜色的亮度,也能使我们获得更加鲜明的饱和度.对于大多数图像,计算出的颜色非常接近Twitter的原始颜色.
我们可以通过改变图像的哪些部分来计算平均颜色来改进这个实验.上面的示例在整个图像中均匀地选择像素,但我们可以尝试仅使用图像边缘附近的像素 – 因此颜色更加无缝地混合 – 或者我们可以尝试从图像中心平均颜色值以突出主体.我将扩展代码,并在我有更多时间后更新此答案.