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

Fer*_*Fer 6 css twitter image colors

让我先澄清问题陈述。看看这条推文:

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

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

在此处输入图片说明

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

在此处输入图片说明

截图中的例子是乐观的,因为有很多背景太亮的情况。即使在我的截图中看到的看似正面的例子中,大部分时间它也没有通过 AA 或 AAA 对比度检查。

我目前的做法:

  • 每个图像一次,运行一个 JS 来计算图像中所有像素的平均颜色。请注意,平均颜色不一定是有意义的颜色,例如在蜘蛛的边缘情况下,平均颜色接近白色。
  • 我将 RGB 值存储在数据库中
  • 在渲染页面(服务器端)时,我使用公式动态设置图像标题的背景颜色

我的公式是将 RGB 转换为 HSL,然后特别处理 S 和 L 值。给他们一个缺口,使用最小/最大值来设置阈值。我试过无数组合。

然而,这似乎是一场永无止境的斗争,因为颜色暗度和对比度受人类感知的影响。

因此,我对 Twitter 似乎如何解决这一问题感到好奇,尤其是两个方面:

  1. 寻找有意义的主题颜色(与平均色或主色不同)
  2. 以一种保持可识别(色调)但对比度足以在其上放置浅色文本的方式对这种有意义的颜色进行调色,同时至少通过 AA 对比度检查。

我四处搜索,但找不到有关其实施的任何信息。有人知道他们这样做吗?或者其他经过验证的方法可以端到端地解决这个难题?

Cy *_*nol 3

我看了一下 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);
    });
});
Run Code Online (Sandbox Code Playgroud)
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; }
Run Code Online (Sandbox Code Playgroud)
<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>
Run Code Online (Sandbox Code Playgroud)

在确定图像的主色时,代码会忽略白色、黑色和灰色像素,尽管降低了颜色的亮度,但仍为我们提供了更鲜艳的饱和度。大多数图像的计算颜色与 Twitter 的原始颜色非常接近。

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