Оптимизация преобразования HSV в RGB для микроконтроллеров

в 23:06, , рубрики: arduino, hsl, Программинг микроконтроллеров, светодиодная лента, метки: , ,

В качестве хобби я занимаюсь светодиодным реквизитом и столкнулся с интересной задачей — показать что-то «красивое» на управляемой светодиодной ленте вместо традиционной радуги, не тратя на это половину памяти микроконтроллера и значительную часть процессорного времени.

Пиксели светодиодной ленты отличаются от пикселей экрана отсутствием фоновой подсветки. Чёрный пиксель не будет выглядеть «чёрным» — он сольётся с фоном, а в движении фактически будет «прозрачным», но если добавить хотя бы единицу к любому цветовому каналу — этот пиксель будет светиться. В свою очередь, «серый» пиксель от белого будет отличаться только яркостью и будет казаться более тусклым, но всё же именно «белым».

Хранится и передаётся цвет пикселя в 24-bit RGB, но значительная часть этого цветового диапазона (ненасыщенные и яркие цвета) не слишком репрезентабельна в отдельных светодиодах. Кроме того, строить симпатичные градиенты в модели RGB не получится — смешивание RGB-цветов даёт не интуитивно-очевидный результат (жёлтый + синий = серый, а хочется — зелёный). Модели HSL и HSV подойдут лучше, но стандартные реализации используют нецелочисленную арифметику. Удобно будет использовать модель, которая сможет компактно хранить параметры цвета и быстро считать их RGB-значения, не используя числа с плавающей запятой и деление на произвольное число — речь идёт о микроконтроллере и сложные алгоритмы нам ни к чему, а деление (кроме небольших степеней двойки) и вовсе противопоказано.

Решение

Для своих нужд я использую модель HSV (HSB) с определёнными диапазонами для каждой из координат (немного magic numbers).

  • Hue — тон, цикличная угловая координата.
  • Value, Brightness — яркость, воспринимается как альфа-канал, при V=0 пиксель не светится, при Vmax=17 — светится максимально ярко, в зависимости от H и S.
  • Saturation. С отсутствием фона, значения S=0 дадут не серый цвет, а белый разной яркости, поэтому параметр W=Smax - S можно называть Whiteness — он отражает степень «белизны» цвета. При W=0, S=Smax=15 цвет полностью определяется Hue, при S=0, W=Wmax=15 цвет пикселя будет белым.

Математика модели строится на целочисленном делении на одну шестую максимального значения тона (размер одного сектора), поэтому в качестве Hmax удобно взять максимальное значение равное 6 * 2^x, например 48 или 96. Это позволит удобно вычислять RGB-цвет, а значение меньше 128 позволит строить градиент, который несколько раз содержит полный цветовой круг. В моделях HSV/HSL Hmax=360, в MS Paint — 240, в некоторых библиотеках — 255.

При выборе максимальных значений Bmax=17 и Wmax=15 перемножение B*W даёт результат, лежащий в диапазоне 0..255.

Минимальная конфигурация HSV, простая в расчёте и ограниченная 8-битными значениями, при диапазонах H=0..11, B=0..17 и W=0..3 даёт нам 12*18*4=864 цвета, часть из которых практически повторяется, а часть отстоит довольно далеко друг от друга (справедливости ради замечу, что этим грешат все цветовые модели, оперирующие H — натянуть три стороны куба на конус не исказив длины не смог бы и Меркатор). Цифра кажется скудной в сравнении с 24-битным цветом в типичном мониторе (16,7 млн уникальных цветов), но её достаточно, чтобы разнообразить светодиодный реквизит, в котором раньше и семь цветов вместо одного зачастую были приятным бонусом. Координаты цвета в такой модели можно хранить в двух байтах.

Разумеется, разрешение HSV можно и нужно повышать до удобного. Я использую W=0..15 и 96 тонов, что даёт уже 27,6 тысяч оттенков. Пример кода с такими параметрами (конфигурация модели — max_value, max_whiteness, sixth_hue):

Код

typedef struct {
    uint8_t r;
    uint8_t g;
    uint8_t b;
} RGB_t;

typedef struct {
    uint8_t h;
    uint8_t s;
    uint8_t v;
} HSV_t;

const uint8_t max_whiteness = 15;
const uint8_t max_value = 17;

const uint8_t sixth_hue = 16;
const uint8_t third_hue = sixth_hue * 2;
const uint8_t half_hue = sixth_hue * 3;
const uint8_t two_thirds_hue = sixth_hue * 4;
const uint8_t five_sixths_hue = sixth_hue * 5;
const uint8_t full_hue = sixth_hue * 6;

inline RGB_t rgb(uint8_t r, uint8_t g, uint8_t b) {
    return (RGB_t) {r, g, b};
}

inline HSV_t hsv(uint8_t h, uint8_t s, uint8_t v) {
    return (HSV_t) {h, s, v};
}

const RGB_t black = {0, 0, 0};

RGB_t hsv2rgb(HSV_t hsv) {
    if (hsv.v == 0) return black;
    
    uint8_t high = hsv.v * max_whiteness;//channel with max value    
    if (hsv.s == 0) return rgb(high, high, high);
    
    uint8_t W = max_whiteness - hsv.s;
    uint8_t low = hsv.v * W;//channel with min value
    uint8_t rising = low;
    uint8_t falling = high;
    
    uint8_t h_after_sixth = hsv.h % sixth_hue;
    if (h_after_sixth > 0) {//not at primary color? ok, h_after_sixth = 1..sixth_hue - 1
        uint8_t z = hsv.s * uint8_t(hsv.v * h_after_sixth) / sixth_hue;
        rising += z;
        falling -= z + 1;//it's never 255, so ok
    }
    
    uint8_t H = hsv.h;
    while (H >= full_hue) H -= full_hue;
    
    if (H < sixth_hue) return rgb(high, rising, low);
    if (H < third_hue) return rgb(falling, high, low);
    if (H < half_hue) return rgb(low, high, rising);
    if (H < two_thirds_hue) return rgb(low, falling, high);
    if (H < five_sixths_hue) return rgb(rising, low, high);
    return rgb(high, low, falling);
}

P.S.
TeX-коды в топик включил в порядке эксперимента. Если есть способ делать это удобнее или правильнее — намекните в ПМ.
Если будет интересно — могу отдельно пояснить особенности обсчёта HSV, в частности механику функций rising/falling и функцию обратного расчёта в «такой» HSV.

Автор: Devgru

Источник


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js