Цветовые палитры для пространства HSV

«Готовь сани летом» — именно этой пословице мы с другом решили последовать, начав разрабатывать новогоднюю гирлянду на широко известных RGB светодиодах со встроенным драйвером WS2812B.
После того, как было решено, что все программные манипуляции с цветом будем производить в цветовой модели HSV, встал вопрос о конвертере HSV->RGB. Ибо формата отличного от RGB светодиоды не понимают. Использовать готовые конвертеры или тупо переложить в код готовые формулы преобразований, чтобы просто оперировать стандартным пространством HSV — неинтересно. Душа требовала какой-нибудь изюминки. Ну и в общем-то что хотела, то и получила.


О цветовых пространствах

Начать следует наверное с маленького пояснения о цветовых пространствах. Например, можно провести аналогию с радиотехникой. Я думаю, что все кто сидит на we.ee знакомы с обработкой сигналов. И знают, что один и тот же сигнал можно рассматривать через призму разных математических моделей — во временной области, в частотной или вообще в пространстве изображений (Лаплас). И каждое из представлений способно предложить определенные удобства, которые не могут дать другие модели. Аналогично и с цветами. Думаю каждый знаком с цветовой моделью RGB и знает, что для получения необходимого цвета нужно смешать три составляющих (Red, Green, Blue) в определенных пропорциях. Вроде ничего сложного, но это до тех пор, пока не понадобится, например, делать плавное радужное перетекание цветов. Не, ну конечно это реализуемо, ну а если допустим параллельно нужно управлять общей яркостью? Уже чего-то не весело. Но тут на помощью приходят другие цветовые модели.

HSV

HSV — Hue, Saturation, Value. А если по-русски то: оттенок(H), насыщенность(S), значение(V, или по-другому яркость). Как видно, здесь уже нет явных компонент цвета, с которыми непонятно что делать в сложных ситуациях, а вполне человеческие характеристики, которыми удобно оперировать.
Любой, кто пользовался растровым редактором сталкивался с этой цветовой моделью. Вот например окно выбора цвета из одной весьма уважаемой программы.



По горизонтальной оси слева направо изменяется H[0;360гр], по вертикальной S[0;100%], шкала рядом регулирует яркость B[0;100%] (это Brightness, тоже самое что и Value).
Ну а теперь попробуем сделать радужный перелив цветов — легко, просто инкрементируем постоянно значение оттенка(H). Нужно изменить яркость? Проще простого, изменим значение V.

Конвертер HSV->RGB


К сожалению, работа с цветовыми моделями, отличными от RGB, подразумевает обратный переход после всех манипуляций, т.к. RGB-светодиоды по другому и не умеют. Как я уже говорил, готовых конвертеров тьма, а формулы перевода можно хоть на той же википедии посмотреть. Так в чем же «изюм»? А в том, что можно сделать свои собственные цветовые палитры сделать! По умолчанию подразумевается палитра вида: красный, желтый, зеленый, голубой, синий, фиолетовый, красный. Вдоль которой мы можем перемещаться изменяя H.
Но можно сделать палитру в синих тонах например (по горизонтали H, по вертикали S).



А можно например реализовать радугу специально для магистра Йоды: «Где Фазан Сидит, Желает Знать Охотник Каждый».



В общем простор для фантазии безграничный.
И под это дело была написана аппаратнонезависимая микробиблиотека на Си, содержащая пару типов и аж одну функцию =)
Вот содержимое hsv_color_pallete.h
/**
  ******************************************************************************
  * file    hsv_color_pallete.h
  * author  1essor1
  * version V1.0.0
  * date    05.08.2015
  * brief   This file contains all the types and functions prototypes  
  *         for work with color palletes
  ******************************************************************************
  */ 

#ifndef __hsv_color_pallete_h
#define __hsv_color_pallete_h

/* Includes ------------------------------------------------------------------*/
#include <stdint.h>  
  
/* Exported types ------------------------------------------------------------*/
typedef struct 
{
    uint8_t R;  /* Red */
    uint8_t G;  /* Green */
    uint8_t B;  /* Blue */                  
} RGB_TypeDef;

typedef struct 
{
    uint8_t H;  /* Hue */
    uint8_t S;  /* Saturation */
    uint8_t V;  /* Value */                
} HSV_TypeDef;

typedef enum
{
    RAINBOW,      /* Standart rainbow from red to purple*/
    YODA_RAINBOW, /* Rainbow with colors in other order*/
    SUNNY,        /* Yellow, orange, red mix */
    COLDY,        /* Cyan, light blue and blue mix */
    GREENY,       /* Hues of green and yellow */
    RUSSIA        /* Russian Federation flag colors =) */
} CPallete_Name_TypeDef;

/* Exported constants --------------------------------------------------------*/
/* Exported macro ------------------------------------------------------------*/
/* Exported variables --------------------------------------------------------*/
/* Exported functions --------------------------------------------------------*/
void hsv2rgb(HSV_TypeDef* HSV, RGB_TypeDef* RGB, CPallete_Name_TypeDef name);

#endif /*__hsv_color_pallete_h*/

Внутреннее устройство библиотеки вполне прозрачно. Есть массив палитр, а по сути массив структур в несколько слоев, которые заполняются данными во время компиляции вполне очевидными макросами. Ну и есть функция преобразования, которая принимает ссылки на структуры источника и приемника данных, а также имя палитры, и затем творит свои темные преобразовательные дела. Вообщем зачем долго говорить, вот содержимое hsv_color_pallete.c
/**
  ******************************************************************************
  * file    hsv_color_pallete.c
  * author  1essor1
  * version V1.0.0
  * date    05.08.2015
  * brief   This file contains customized color palletes and functions 
  *         for colors convertations 
  ******************************************************************************
  */ 

/* Includes ------------------------------------------------------------------*/
#include "hsv_color_pallete.h"

/* Private types -------------------------------------------------------------*/ 
typedef struct 
{
    RGB_TypeDef *Colors;
    uint8_t ColorsTotal;                 
} CPallete_TypeDef;

/* Private constants ---------------------------------------------------------*/
/* Private macro -------------------------------------------------------------*/
/* Colors            R   G   B*/
#define WHITE      {255,255,255}
#define RED        {255,  0,  0}
#define ORANGE     {255,128,  0}
#define YELLOW     {255,255,  0}
#define GREENLIME  {191,255,  0}
#define LIGHTGREEN {128,255,  0}
#define GREEN      {  0,255,  0}
#define CYAN       {  0,255,255}
#define LIGHTBLUE  {  0,128,255}
#define BLUE       {  0,  0,255}
#define PURPLE     {128,  0,255}

/* Color palletes
 *
 * Strongly recomended to set _COLORS_TOTAL with one of this values:
 * 2, 4, 8, 16, 32, 64, 128
 */       

/*RAINBOW*/
    #define RAINBOW_COLORS_TOTAL    8
    RGB_TypeDef rainbow_colors[RAINBOW_COLORS_TOTAL] = 
                    {RED, ORANGE, YELLOW, GREEN, CYAN, BLUE, PURPLE, RED};                  
    #define _RAINBOW  {rainbow_colors, RAINBOW_COLORS_TOTAL}  

/*YODA_RAINBOW*/  
    #define YODA_RAINBOW_COLORS_TOTAL    8
    RGB_TypeDef yoda_rainbow_colors[YODA_RAINBOW_COLORS_TOTAL] = 
                    {CYAN, PURPLE, BLUE, YELLOW, GREEN, ORANGE, RED, CYAN};                  
    #define _YODA_RAINBOW  {yoda_rainbow_colors, YODA_RAINBOW_COLORS_TOTAL}  

/*SUNNY*/
    #define SUNNY_COLORS_TOTAL      4
    RGB_TypeDef sunny_colors[SUNNY_COLORS_TOTAL] = 
                    {YELLOW, ORANGE, RED, YELLOW};                  
    #define _SUNNY  {sunny_colors, SUNNY_COLORS_TOTAL} 

/*COLDY*/
    #define COLDY_COLORS_TOTAL      4
    RGB_TypeDef coldy_colors[COLDY_COLORS_TOTAL] = 
                    {CYAN, LIGHTBLUE, BLUE, CYAN};                  
    #define _COLDY  {coldy_colors, COLDY_COLORS_TOTAL} 

/*GREENY*/
    #define GREENY_COLORS_TOTAL     4
    RGB_TypeDef greeny_colors[GREENY_COLORS_TOTAL] = 
                    {LIGHTGREEN, GREENLIME, YELLOW, LIGHTGREEN};                  
    #define _GREENY  {greeny_colors, GREENY_COLORS_TOTAL} 

/*RUSSIA*/
    #define RUSSIA_COLORS_TOTAL     4
    RGB_TypeDef russia_colors[RUSSIA_COLORS_TOTAL] = 
                    {WHITE, BLUE, RED, WHITE};                  
    #define _RUSSIA  {russia_colors, RUSSIA_COLORS_TOTAL} 

/* Private variables ---------------------------------------------------------*/
/*List of color palletes must be the same order as in CPallete_Name_TypeDef*/
const CPallete_TypeDef cpallete[] = 
{
    _RAINBOW,
    _YODA_RAINBOW, 
    _SUNNY, 
    _COLDY, 
    _GREENY, 
    _RUSSIA
}; 

/* Private functions ---------------------------------------------------------*/
/* Exported functions --------------------------------------------------------*/

/**
  * brief   Convert HSV values to RGB in the current color pallete
  * params  
    *       HSV : struct to get input values
    *       RGB : struct to put calculated values
    *       name: color pallete name
  * retval  none
  */
void hsv2rgb(HSV_TypeDef* HSV, RGB_TypeDef* RGB, CPallete_Name_TypeDef name)
{
    uint8_t tempR;
    uint8_t tempG;
    uint8_t tempB;
    int16_t diff;

    uint8_t sector_basecolor;
    uint8_t next_sector_basecolor;
    uint8_t hues_per_sector;  
    uint8_t hue_in_sector;  

    hues_per_sector = 256 / cpallete[name].ColorsTotal;
    hue_in_sector = HSV->H % hues_per_sector; 

    sector_basecolor = HSV->H / hues_per_sector;
    if(sector_basecolor == (cpallete[name].ColorsTotal-1))
        next_sector_basecolor = 0;
    else next_sector_basecolor = sector_basecolor + 1;
    
    /* Get Red from Hue */
    diff = ((cpallete[name].Colors[next_sector_basecolor].R-cpallete[name].Colors[sector_basecolor].R)/hues_per_sector)*hue_in_sector; 
    if((cpallete[name].Colors[sector_basecolor].R + diff) < 0)
        tempR = 0;
    else if((cpallete[name].Colors[sector_basecolor].R + diff) > 255)
        tempR = 255;
    else tempR = (uint8_t)(cpallete[name].Colors[sector_basecolor].R + diff); 

    /* Get Green from Hue */
    diff = ((cpallete[name].Colors[next_sector_basecolor].G-cpallete[name].Colors[sector_basecolor].G)/hues_per_sector)*hue_in_sector; 
    if((cpallete[name].Colors[sector_basecolor].G + diff) < 0)
        tempG = 0;
    if((cpallete[name].Colors[sector_basecolor].G + diff) > 255)
        tempG = 255;
    else tempG = (uint8_t)(cpallete[name].Colors[sector_basecolor].G + diff); 

    /* Get Blue from Hue */
    diff = ((cpallete[name].Colors[next_sector_basecolor].B-cpallete[name].Colors[sector_basecolor].B)/hues_per_sector)*hue_in_sector; 
    if((cpallete[name].Colors[sector_basecolor].B + diff) < 0)
        tempB = 0;
    if((cpallete[name].Colors[sector_basecolor].B + diff) > 255)
        tempB = 255;
    else tempB = (uint8_t)(cpallete[name].Colors[sector_basecolor].B + diff); 

    /* Saturation regulation */
    tempR = (255-((255-tempR)*(HSV->S))/255);
    tempG = (255-((255-tempG)*(HSV->S))/255);
    tempB = (255-((255-tempB)*(HSV->S))/255);

    /* Value (brightness) regulation to get final result */
    RGB->R = (tempR*(HSV->V))/255;
    RGB->G = (tempG*(HSV->V))/255;
    RGB->B = (tempB*(HSV->V))/255;
}

Саму математику преобразования расписывать пока не буду, но если кому-то будет интересно или непонятно, то сделаю это. Там все просто, и по сути к таким расчетам можно прийти чисто эмпирически, если посидеть в графическом редакторе и подергать ползунки HSV, с целью оценить их влияние на RGB.

Ну а в итоге мы получаем кастомизированное цветовое пространство HSV, которое открывает широчайшие просторы для создания эффектных программ свечения RGB светодиодов и лент.

Обновление от 30.12.2012:

Для более удобной интеграции в другие проекты, а также для возможного развития провел небольшую модификацию проекта:
  • провел рефакторинг имен;
  • изменил внутреннюю структуру: цвета и цветовые палиты теперь в отдельном хедере;
  • ввёл doxygen-совместимые комментарии;
  • добавил пример работы;

Проект теперь живет в новом репозитории ColorFlow на BitBucket.
  • +12
  • 06 августа 2015, 10:55
  • 1essor1

Комментарии (25)

RSS свернуть / развернуть
Спасибо!

github, etc…?
0
Вечером ссылку на bitbucket припишу в конец статьи.
0
Очень интересно. Как раз завтра собирался писать функцию преобразования hsv->rgb, а тут такой подарок!
0
  • avatar
  • SOVA
  • 06 августа 2015, 13:47
Да наздоровье) Только здесь нет стандартной hsv палитры если что, в моей версии RAINBOW добавлен оранжевый цвет.
0
ИМХО, в вычислении diff лучше сначала умножить, а потом разделить, потому что теряется точность.
0
  • avatar
  • SOVA
  • 06 августа 2015, 15:20
Кстати, я учитывал это когда делал поправку на насыщенность и яркость. А вот про оттенок забыл)
0
И в вычислении Saturation regulation напутано.
0
  • avatar
  • SOVA
  • 06 августа 2015, 15:36
Почему?
0
Лучше так:
tempR = 255 — (((uint16_t)(255 — tempR) * HSV->S) / 255);
RGB->R = (uint8_t)(((uint16_t)tempR * HSV->V) / 255);
-1
  • avatar
  • SOVA
  • 06 августа 2015, 15:45
Юзай u8, u16, выглядит гораздо симпатичнее, и меньше писанины
-1
Про порядки умножения и явного приведения типов. Разве компилятор сам не разберется как надо вычислить и привести к типу переменной слева от «равно»?
Просто не заметил никакого эффекта что от первого, что от второго.
0
По умолчанию используются целые числа, хз для чего автор делает приведение типов
0
Тьфу блин, не автор а юзер «SOVA»
0
Прочел статью с удовольствием, она навеяла некоторые воспоминания связанные с цветомузыкальными устройствами которые были популярны с середины 70-х и до конца 80-х. Тогда о таких технологиях и элементной базе даже и не могли мечтать.
0
Видел реализацию попроще подобного кода в атмеловской гирлянде для WS2812B год назад:

// HSV to RGB colors
// hue: 0-359, sat: 0-255, val (lightness): 0-255
// adapted from http://funkboxing.com/wordpress/?p=1366
void HSVtoRGB(int hue, int sat, int val, uint8_t * colors) {
    int r, g, b, base;
    if (sat == 0) { // Achromatic color (gray).
        colors[0] = val;
        colors[1] = val;
        colors[2] = val;
    } else {
        base = ((255 - sat) * val) >> 8;
        switch (hue / 60) {
        case 0:
            colors[0] = val;
            colors[1] = (((val - base) * hue) / 60) + base;
            colors[2] = base;
            break;
        case 1:
            colors[0] = (((val - base) * (60 - (hue % 60))) / 60) + base;
            colors[1] = val;
            colors[2] = base;
            break;
        case 2:
            colors[0] = base;
            colors[1] = val;
            colors[2] = (((val - base) * (hue % 60)) / 60) + base;
            break;
        case 3:
            colors[0] = base;
            colors[1] = (((val - base) * (60 - (hue % 60))) / 60) + base;
            colors[2] = val;
            break;
        case 4:
            colors[0] = (((val - base) * (hue % 60)) / 60) + base;
            colors[1] = base;
            colors[2] = val;
            break;
        case 5:
            colors[0] = val;
            colors[1] = base;
            colors[2] = (((val - base) * (60 - (hue % 60))) / 60) + base;
            break;
        }

    }
}
+1
  • avatar
  • tipok
  • 07 августа 2015, 19:48
Насколько я понял, это стандартные формулы перевода переложенные в код. Поэтому и попроще)
0
Оставлю это здесь. Моя реализация плавного перехода цветов в пространстве RGB.


#define FRAME_COUNT_MAX 1016

struct RGB_COLOUR_TYPE
{
    uint8 RED;
    uint8 GREEN;
    uint8 BLUE;
} RGB_ANIMATION_ARRAY [FRAME_COUNT_MAX];

struct RGB_COLOUR_TYPE Red = {255, 0, 0};
struct RGB_COLOUR_TYPE Orange = {252, 179, 0};
struct RGB_COLOUR_TYPE Yellow = {252, 255, 0};
struct RGB_COLOUR_TYPE Green = {0, 255, 0};
struct RGB_COLOUR_TYPE Blue = {0, 226, 251};
struct RGB_COLOUR_TYPE NavyBlue = {0, 0, 255};
struct RGB_COLOUR_TYPE Violet = {200, 0, 255};
struct RGB_COLOUR_TYPE White = {255, 255, 255};
struct RGB_COLOUR_TYPE Black = {0, 0, 0};

struct RGB_COLOUR_TYPE Colour_Transform(struct RGB_COLOUR_TYPE start, struct RGB_COLOUR_TYPE finish, uint8 steps, uint8 step)
{
    struct RGB_COLOUR_TYPE Current_Colour;
    
    if (start.RED > finish.RED)
        Current_Colour.RED = start.RED - (float)(start.RED-finish.RED)/steps * step;
    else 
        Current_Colour.RED = start.RED + (float)(finish.RED-start.RED)/steps * step;
        
    if (start.GREEN > finish.GREEN)
        Current_Colour.GREEN = start.GREEN - (float)(start.GREEN-finish.GREEN)/steps * step;
    else 
        Current_Colour.GREEN = start.GREEN + (float)(finish.GREEN-start.GREEN)/steps * step;
        
    if (start.BLUE > finish.BLUE)
        Current_Colour.BLUE = start.BLUE - (float)(start.BLUE-finish.BLUE)/steps * step;
    else 
        Current_Colour.BLUE = start.BLUE + (float)(finish.BLUE-start.BLUE)/steps * step;       
        
    return Current_Colour;
}

В функцию передаются параметры: start — начальный цвет, finish — конечный цвет, steps — общее количество шагов перехода, step — текущий шаг перехода. Синтаксис функции упрощен для наглядности работы, не оптимален по быстродействию. Функция возвращает цвет для текущего шага перехода.
В таком виде переход одного цвета в другой реализуется следующим кодом (в примере создается массив для вывода плавно перетекающей радуги + белого, 127 градаций перехода):


// Rainbow + White   
for (c = 0; c < 128; ++c)
{
    RGB_ANIMATION_ARRAY[c] = Colour_Transform(Red, Orange, 127, c);               
    RGB_ANIMATION_ARRAY[c+127] = Colour_Transform(Orange, Yellow, 127, c);             
    RGB_ANIMATION_ARRAY[c+254] = Colour_Transform(Yellow, Green, 127, c);            
    RGB_ANIMATION_ARRAY[c+381] = Colour_Transform(Green, Blue, 127, c);            
    RGB_ANIMATION_ARRAY[c+508] = Colour_Transform(Blue, NavyBlue, 127, c);            
    RGB_ANIMATION_ARRAY[c+635] = Colour_Transform(NavyBlue, Violet, 127, c);            
    RGB_ANIMATION_ARRAY[c+762] = Colour_Transform(Violet, White, 127, c);
    RGB_ANIMATION_ARRAY[c+889] = Colour_Transform(White, Red, 127, c);
}

С помощью данной функции можно легко реализовать переход из любого цвета (и яркости) в любой другой. Для плавного гашения нужно вторым цветом указать RGB(0,0,0).

Видео работы попробую добавить в ближайшее время.
0
  • avatar
  • ARMag
  • 11 августа 2015, 11:18
Видео

Качество картинки получилось так себе, светодиодная матрица ОЧЕНЬ яркая, засвечивает камеру даже обернутая в бумагу и при дневном освещении. Но суть, думаю, ясна. В жизни выглядит очень красиво, при количестве шагов более 50 на глаз переход плавный и без скачков.
0
Другими словами — обычный LERP между несколькими статично заданными цветами. Он в RGB без проблем делается. HSV в основном применяется затем, чтобы можно было рандомно генерировать насыщенные цвета.
Алсо, условия в функции LERP'а не нужны. По крайней мере в приведенном виде. Достаточно
struct RGB_COLOUR_TYPE Colour_Transform(struct RGB_COLOUR_TYPE start, struct RGB_COLOUR_TYPE finish, uint8 steps, uint8 step)
{
    struct RGB_COLOUR_TYPE Current_Colour;
    Current_Colour.RED = start.RED - (float)(start.RED-finish.RED)/steps * step;
    Current_Colour.GREEN = start.GREEN - (float)(start.GREEN-finish.GREEN)/steps * step;
    Current_Colour.BLUE = start.BLUE - (float)(start.BLUE-finish.BLUE)/steps * step;
    return Current_Colour;
}
0
Да, вы все верно подметили. Это вариант линейной интерполяции для RGB. Функция, которая приведена в сообщении выше развернута для наглядности её работы, о чем написано в тексте. У меня в коде она упрощена до следующей:


struct RGB_COLOUR_TYPE Colour_Transform(struct RGB_COLOUR_TYPE start, struct RGB_COLOUR_TYPE finish, uint8 steps, uint8 step)
{
    float coef = (float) step / steps;
    return (struct RGB_COLOUR_TYPE) {   (start.RED - (float)(start.RED-finish.RED) * coef), 
                                        (start.GREEN - (float)(start.GREEN-finish.GREEN) * coef), 
                                        (start.BLUE - (float)(start.BLUE-finish.BLUE) * coef)   };
}

но, как по мне, работа не особо очевидна с первого взгляда.
0
Вот как раз «развернутая» менее понятна.
-1
Возможно, сказывается ваш большой опыт в программировании. Я руководствовался темы размышлениями, что сайт является образовательным и уровень программирования участников не всегда высокий. В «развернутой» функции просто проследить что из чего вычитается и при каких условия. Ну в любом случае опубликовано три разных варианта, каждый может выбрать себе по душе :)
0
В копилку различных релизаций: bitmap.c, bitmap.h — совсем недавно публиковал этот код в блоге STM32.
0
Как-то попался в руки компилятор ява игр с синтаксисом Паскаля, за вечер набросал приложение а-ля метроном. Вот там использовал рисование интерфейса, отталкиваясь от случайного оттенка, который один раз задавался при запуске, и не менялся, пока программа не перезапускалась. Яркость и насыщенность у каждого элемента интерфейса была своя. Получалось что при любом случайном цвете в начале, всё выглядело ярко, сочно, и гармонично. Очень удобно. Как скины в аудиоплеерах: оттенок меняется, но всё равно остаётся читаемо и ярко
0
Обновил проект. Изменения в конце статьи.
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.