css – Twitter如何从图像像素数据中提取有意义的主题颜色?

让我首先澄清一下问题陈述.看看这条推文:

https://twitter.com/jungledragon/status/926894337761345538

接下来,在推文中单击图像本身.在出现的灯箱中,它下方的菜单栏采用基于图像本身中实际像素的有意义颜色.即使在这种压力测试中,这对于所有光像素都是一个困难的图像,它在选择整体颜色方面做得很好1)表示图像的内容2)黑暗/对比度足以在其上放置白色文本:

《css – Twitter如何从图像像素数据中提取有意义的主题颜色?》

在我知道Twitter有这个之前,我同时实施了一个类似的系统.查看下面的预览:

《css – 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的原始颜色.

我们可以通过改变图像的哪些部分来计算平均颜色来改进这个实验.上面的示例在整个图像中均匀地选择像素,但我们可以尝试仅使用图像边缘附近的像素 – 因此颜色更加无缝地混合 – 或者我们可以尝试从图像中心平均颜色值以突出主体.我将扩展代码,并在我有更多时间后更新此答案.

点赞