[Перевод] Алгоритм генерирования цветовых палитр

npsqwx0pfiklizr7ipaoeoiocik.png

Ищете красивую цветовую палитру для сайта? Недавно установили дома RGB-подсветку, или хотите покрасить комнату в новые цвета? Или купили клавиатуру с цветной подсветкой и хотите использовать её по полной? В какой бы ситуации вы ни оказались, наверняка постоянно настраиваете цветовые схемы.

Будучи программистом, я быстро написал несколько строк кода для генерирования случайных цветовых палитр. Сразу почуяв, что такой подход может дать не лучшие результаты, я за пару минут реализовал кнопку «перезагрузки» палитры. Мне представлялось, что для получения прекрасной схемы просто нужно немного удачи и терпения.

Я ошибался. Генерирование палитр из случайных цветов — отстой. Время от времени красивый цвет соседствует с уродливым, грязным оттенком коричневого или жёлтого. Подборки цветов получаются всегда либо слишком тёмные, либо слишком светлые и малоконтрастные, либо наборы состоят из очень похожих цветов. Нужно было придумать другое решение.

Цветовые пространства


Начнём с теории. Сегодня для классификации цветов широко используются такие цветовые пространства:

sRGB


RGB означает Red Green Blue. Так работают дисплеи: они излучают свет в трёх цветовых каналах, которые смешиваются в разной пропорции для получения всевозможных цветов. Значение в каждом канале варьируется от 0 до 255. R:0, G:0, B:0 (или #000000 в шестнадцатеричном выражении) — это чёрный цвет, а R:255, G:255, B:255 (или #ffffff) — белый.

CIE Lab


2wquvx5sk5muisnxaiz_suoadyw.pngЦветовое пространство CIE Lab шире sRGB и включает в себя все цвета, воспринимаемые человеком. Оно создавалось с расчётом на универсальность восприятия. Иными словами, расстояние между цветами соответствует субъективной разнице: если значения двух цветов близки друг к другу, то и выглядят они похоже. С другой стороны, два далеко расположенных друг от друга цвета также воспринимаются как совсем непохожие. В CIE Lab для насыщенных цветов выделено больше места, чем для тёмных и светлых. К слову, для человеческого глаза очень тёмный зелёный почти неотличим от чёрного. Кроме того, это цветовое пространство трёхмерное: L означает светлоту (от 0.0 до 1.0), a и b (примерно от -1.0 до 1.0) — цветовые каналы.

HCL


h5pjnxhcfpy_hau5oep_pjghq_m.pngЕсли RGB описывает, как дисплей отображает цвета, а CIE Lab — как мы их воспринимаем, то HCL — это цветовое пространство, которое ближе всего описывает то, как мы думаем о цветах. Оно тоже трёхмерное, H означает цветовой тон (hue) (от 0 до 360 градусов), С — насыщенность (chroma) и L — яркость (luminance) (оба параметра измеряются от 0,0 до 1,0).

Для вычислений рекомендую использовать CIE Lab, а для представления палитр пользователю — HCL. При желании можно преобразовать значения из этих пространств в RGB.

Разложение цветового пространства


iqaq3hljpo3n3w7zwugsyvy21na.gifПоскольку мне нужно было получить набор уникальных, индивидуальных цветов, сначала отбросим те, что выглядят очень похожими. Цветовое пространство будет трёхмерным, и для разделения таких низкоразмерных наборов данных прекрасно подходит алгоритм кластеризации методом k-средних. Он пытается разложить данные (в нашем случае — цветовое пространство) на k отдельных областей. И затем палитра собирается из центральных точек кластеров в этих областях. На гифке показано двумерное отображение работы алгоритма в трёхмерном пространстве CIE Lab.

Пишем код


С помощью реализованного на Go алгоритма метода k-средних задача решается всего в несколько строк кода. Сначала подготовим значения цветов в пространстве CIE Lab:

var d clusters.Observations
for l := 0.2; l <= 0.8; l += 0.05 {
    for a := -1.0; a < 1.0; a += 0.1 {
        for b := -1.0; b < 1.0; b += 0.1 {
            d = append(d, clusters.Coordinates{l, a, b})
        }
    }
}


Я уже подобрал пару параметров и ввёл определённые ограничения для генерируемых цветов. В этом примере мы выкинем цвета слишком тёмные (яркость 0,8).

Разложим только что созданное цветовое пространство:

km := kmeans.New()
clusters, _ := km.Partition(d, 8)


Функция Partition возвратит кусочки восьми кластеров. У каждого кластера есть точка Center, представляющая собой отдельный цвет в заданном пространстве. Её координаты легко можно преобразовать в шестнадцатеричное RGB-значение:

col := colorful.Lab(c.Center[0], c.Center[1], c.Center[2])
col.Clamped().Hex()


Помните, что CIE Lab шире RGB, и значит некоторые Lab-значения нельзя преобразовать в RGB. Такие значения можно с помощью Clamped преобразовать в наиболее близкие цвета RGB-пространства.

Полный код

package main

import (
    "github.com/muesli/kmeans"
    "github.com/muesli/clusters"
    colorful "github.com/lucasb-eyer/go-colorful"
)

func main() {
    // Create data points in the CIE L*a*b* color space
    // l for lightness, a & b for color channels
    var d clusters.Observations
    for l := 0.2; l <= 0.8; l += 0.05 {
        for a := -1.0; a <= 1.0; a += 0.1 {
            for b := -1.0; b <= 1.0; b += 0.1 {
                d = append(d, clusters.Coordinates{l, a, b})
            }
        }
    }

    // Partition the color space into 8 clusters
    km := kmeans.New()
    clusters, _ := km.Partition(d, 8)

    for _, c := range clusters {
        col := colorful.Lab(c.Center[0], c.Center[1], c.Center[2])
        fmt.Println("Color as Hex:", col.Clamped().Hex())
    }
}


Набор из восьми (не очень) случайных цветов, сгенерированный этим кодом:

drz4qedyfmtp5tedhuincbfvyly.png

Определяем собственное цветовое пространство


Добавим больше контроля над генерированием цветов. Мы легко можем управлять данными, используемыми для дальнейших вычислений, тем самым подбирая цветовое пространство под свои нужды. Сгенерируем пастельную палитру:

func pastel(c colorful.Color) bool {
    _, s, v := col.Hsv()
    return 0.2 <= s && s <= 0.4 && 0.7 <= v && v <= 1.0
}

for l := 0.0; l <= 1.0; l += 0.05 {
    for a := -1.0; a <= 1.0; a += 0.1 {
        for b := -1.0; b <= 1.0; b += 0.1 {
            col := colorful.Lab(l, a, b)

            if col.IsValid() && pastel(col) {
                d = append(d, clusters.Coordinates{l, a, b})
            }
        }
    }
}


Ещё одно цветовое пространство — HSV, буквы в названии означают оттенок (hue), насыщенность (saturation) и яркость (value). В этом пространстве пастельные цвета обычно имеют высокие значения яркости и низкие значения насыщенности.

Вот что получилось:

eypitssqzzdykt5-kkyq5vfijq4.png

Также вы можете фильтровать цвета по их насыщенности (chroma) и яркости, чтобы получить набор «тёплых» тонов:

func warm(col colorful.Color) bool {
        _, c, l := col.Hcl()
        return 0.1 <= c && c <= 0.4 && 0.2 <= l && l <= 0.5
}


Результат:

ya8pg4sgrit501zyn4pram7mwss.png

Пакет gamut


Я работаю над библиотекой под названием gamut, в которой соберу все описанные здесь кусочки в один удобный пакет на Go, позволяющий генерировать и управлять цветовыми палитрами и темами. Вы уже можете его попробовать, но он пока в работе.

© Habrahabr.ru