Пользователь
0,0
рейтинг
20 января 2013 в 03:06

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


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

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

Хранится и передаётся цвет пикселя в 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
карма
98,8
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое

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

  • 0
    Ееее. Ура HSV на светодиодах!
    Когда делал лампу настроения, по одной из статей, ужаснулся коду на выбор цвета и количеству условий в RGB. Перешел на HSV и код стал значительно проще.
    Я правда не стал все так круто продумывать и сделал Hue-0...360, Sat-0...255, Val-0...255 и арифметика была не целочисленная. Но я по контроллерам не спец и оптимизация там не понадобилась. Заработало и хорошо.
    • +2
      Я когда делал HSV сделал Hue 0..1530 (255 * 6) и получилась целочисленная арифметика. Основная идея была такая: 6 участков показывает рост и спад вторичных цветов, если они будут расти 0..255 и спадать 0..255, то мало того что будут получаться целые числа, так ещё и плавность повысится.

      P.S. Я делал с S = 255, V = 255, т. е. менять можно было только оттенок, но переделать не очень сложно.
      • 0
        void set_led_color(unsigned int hue)
        {
            // sector specifies which colors are primary and secondary
            unsigned char sector = hue / 0xff;
        
            // primary color is always full
            const unsigned char primary = 0xff;
        
            // calculate secondary color value from hue
            unsigned char secondary = abs(sector % 2 * 0xff - hue % 0xff);
        
            // тут идёт 6 кейсов sector и установка PWM на полученные значения
        }
        
      • 0
        Описанный вами случай подходит и под HSL при L = Lmax/2, S = Smax.

        Действительно, при S = 255, V = 255 удобно.

        По поводу кода:
        Странно, что у вас вместо byte используется unsigned char.
        Вместо sector % 2 можно использовать (sector & 1), вместо hue % 255 — byte (hue).

        По поводу «переделать несложно»:
        На самом деле при этом возникают неприятные эффекты. V — это то значение на которое идёт домножение primary и secondary. Как только оно не 255 — появляется дополнительное деление/умножение в secondary. Более того, если вы начнёте уменьшать S — третья компонента цвета будет расти пропорционально (255-S)*V/255, и это же слагаемое появится и в secondary, а то что там было придётся домножать на S/255. В общем, при любом минимальном понижении S и V такое высокое разрешение по тону станет ненужным. А под конкретную задачу — реализация удачная, не спорю :)

        Не берусь сходу дописать код для HSV, но там могут случиться небольшие потери значений при делении.
  • 0
    Я сейчас статью на хабру пишу, где HSL-пространство, но за HSV однозначно привет однополчанам! Hue вы интересно считаете.
    • 0
      У HSL есть та же проблема, что и у HSV но в два раза сильнее: в HSL три стороны куба проецируются на конус, а три — на круг, образуя его основание, а у HSL — два конуса. Фактически, у вас две зоны (в окрестностях L = 0 и L = Lmax), где будет много слабо отличающихся друг от друга значений.
  • +1
    TeX-коды в топик включил в порядке эксперимента. Если есть способ делать это удобнее или правильнее — намекните в ПМ.

    Что такое ПМ я не знаю, но если уж вы используйте TeX, то желательно соблюдать математические традиции вместо программерского жаргона типа двоеточия для диапазона и звёздочки для умножения.

    __attribute__ ((__packed__)) — лишние. Eсли вам нужна оптимизация по размеру, то разумно так и указать это в опциях для компилятора. «Пакование» структур в явном виде применяется в более специфичных случаях (например, когда нужно обеспечить бинарную переносимость структуры в независимости от опций компилятора).

    В-третьих, причём тут привязка к AVR и, тем более, Arduino?

    Ну это я попридирался просто. Сама идея использования пространства HSV вместо RGB в некоторых случаях разобрана и, надеюсь, защитит многих от изобретения своих велосипедов.
    • 0
      ПМ это личное сообщение — private message.

      С математическими традициями у меня проблема — и тройка в дипломе, и с FFT уже месяц разбираюсь, чтобы понять. Но попробую учесть Ваш совет.

      __attribute__ ((__packed__)) для RGB_t прописан для совместимости вот с этим кодом — ассемблерной вставкой для time-critical вывода значений на светодиодную ленту (см. также).

      Arduino упомянут как самый популярный девайс на хабре, использующий AVR.
      Собственно, я не уверен что тот же код будет актуальным для контроллеров на ARM — вряд ли там есть необходимость экономить память, циклы процессора, отказываться от float-point и делений (из того же блога — про скорость деления на AVR).

      • +1
        Arduino упомянут как самый популярный девайс на хабре, использующий AVR [...]

        Ваш код — на чистейшем С (не считая __attribute__), поэтому совершенно ни к чему не привязан, да и мир не заканчивается на AVR и ARM. А FPU лишены большинство микроконтроллеров.

        __attribute__ ((__packed__)) для RGB_t прописан для совместимости вот с этим кодом [...]

        Опять же — какая разница для чего вы его там используете, если в статье к этому привязки нет. Код у вас просто демонстрирует переход от HSV к RGB модели c целочисленной арифметикой.

        Короче, зачем искусственно ограничивать общность статьи?
        • 0
          Исправил заголовок топика и убрал packed, спасибо за замечание.
  • 0
    А нельзя ли осветить железную часть проекта? Схему? Фото? И возможно ли собрать это дома? Я так понимаю, что у Arduino три аналоговых вывода, которые усиливаются транзисторами, но я, например, никогда не смогу спроектировать такую схему (с обвязками и разделением по питанию). По этому было бы интересно посмотреть и другую сторону.
    • 0
      Так как электротехник из меня слабый — схем нет, я ничего не монтирую. Проводами соединена Arduino, радиомодуль nRF24L01+, метр ленты на WS2811 и питание.

      Фото будет, даже видео, но вряд ли на хабре — светодиодный жонглёрский реквизит тема очень узкая, мне кажется фаер-шоу и похожие искусства вряд ли будут интересны хабровчанам.
      • +3
        Не стоит недооценивать хабр :) Если там будут интересные технические решения, то такие узкоспецифические проекты вызывают еще больший интерес.
        • 0
          Из интересных технических решений осталось использование в качестве альтернативной цветовой модели давно забытых Web-safe colors и градиентов на их основе, они пришлись ко двору :)
  • 0
    Для Arduino использую HSV2RGB
    Ссылка на форум где можно скачать HSV2RGB
    • 0
      Там сразу несколько реализаций. Первые две (_360, _384) используют double и деление, _Binary — только деление, _Adv и _Adv1 — чистые, _Adv2 — снова деление.
      Adv и Adv1 можно дооптимизировать по точности, коду и скорости разом, но если вас они устраивают — особой нужды в этом нет :)
      • 0
        Я Ваш код еще не успел попробовать в работе
        Завтра буду на работе тестировать.
  • +1
    А как насчёт цветовой модели YCbCr? Подойдёт ли она для решения вашей задачи? Она более стандартная и используются как в аналоговой части, так и в кодировке цифрового видео. Там преобразования в RGB и обратно намного проще.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.