Оптимизация преобразования 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.
    Поделиться публикацией
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 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 и обратно намного проще.

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