RGB #
Красный (Red, R), зелёный (Green, G) и синий (Blue, B) являются основными цветами: смешивая эти три цвета в разных пропорциях можно получить плюс-минус все остальные цвета:
Этот наглядный "двухмерный" случай с кругами вы тоже скорее всего видели. Если раскручивать тему дальше, то можно задаться интенсивностью каждого цвета и получить итоговый цвет как функцию от трёх переменных, или же трёхмерное цветовое пространство RGB. Если интенсивности всех трёх цветов равны нулю - получится чёрный цвет, если все три максимальны - белый, а всё что между - оттенки:
На картинке выше интенсивность каждого цвета представлена диапазоном 0-255. Знакомое число, не правда ли? Всё верно, в большинстве применений диапазон каждого цвета кодируется одним байтом, потому что это удобно с точки зрения программирования и достаточно с точки зрения глаза: три цвета - три байта - 256*256*256 == 16.8
миллионов оттенков. Да, именно эта цифра часто фигурирует в рекламах смартфонов и телевизоров и именно столько оттенков мы можем получить из 8-бит ШИМ:
analogWrite(PIN_R, 10);
analogWrite(PIN_G, 130);
analogWrite(PIN_B, 210);
RGB24 (RGB888) #
Для хранения и передачи RGB цвета как единого числа часто используется формат RGB24 - три байта цвета объединяются в один тип данных (в данном случае ближайший uint32_t
) в порядке RRGGBB
. Результирующее число из трёх значений каналов красного, зелёного и синего можно получить так:
uint32_t RGB24(uint8_t r, uint8_t g, uint8_t b) {
return ((uint32_t)r << 16) | ((uint32_t)g) << 8 | b;
}
Либо более быстрая реализация без сдвигов:
uint32_t RGB24(uint8_t r, uint8_t g, uint8_t b) {
return (union { uint8_t bytes[4]; uint32_t hex; }){b, g, r, 0 }.hex;
}
Для "распаковки" цвета обратно в три переменные можно так же использовать сдвиги:
// col - rgb24 (uint32_t)
uint8_t r = col >> 16;
uint8_t g = col >> 8;
uint8_t b = col;
Либо более быстрый вариант:
// col - rgb24 (uint32_t)
uint8_t r = ((uint8_t*)&col)[2];
uint8_t g = ((uint8_t*)&col)[1];
uint8_t b = ((uint8_t*)&col)[0];
Либо опять же через union
:
union RGB {
uint32_t hex;
struct {
uint8_t b, g, r, a;
} ch;
};
// col - rgb24 (uint32_t)
RGB rgb{col};
//Serial.println(rgb.ch.r);
//Serial.println(rgb.ch.g);
//Serial.println(rgb.ch.b);
RGB16 (RGB565) #
В embedded часто используется формат RGB565 - цвет кодируется двумя байтами (16 бит), из которых первые 5 бит - красный, далее 6 зелёных и 5 синих. Преобразовать из RGB24 можно так:
uint16_t rgb888to565(uint32_t col) {
return ((col >> 8) & 0b1111100000000000) | ((col >> 5) & 0b0000011111100000) | ((col >> 3) & 0b0000000000011111);
}
Из трёх каналов цвета - так:
uint16_t rgb888to565(uint8_t r, uint8_t g, uint8_t b) {
return ((r & 0b11111000) << 8) | ((g & 0b11111100) << 3) | ((b & 0b11111000) >> 3);
}
Расширить обратно до RGB888 можно так, с потерей младших битов яркости:
uint32_t rgb565to888(uint16_t col) {
uint8_t r = (col >> 8) & 0b11111000;
uint8_t g = (col >> 3) & 0b11111100;
uint8_t b = (col << 3);
return (union { uint8_t bytes[4]; uint32_t hex; }){b, g, r, 0xff}.hex;
}
Либо, согласно мануалу от Apple - так, этот вариант выполняется дольше, но масштабирует до полной 8-бит яркости канала (например красный 11111
превратится в 11111111
):
uint32_t rgb565to888full(uint16_t col) {
uint8_t r = ((col >> 11) * 255 + 15) / 31;
uint8_t g = (((col >> 5) & 0b00111111) * 255 + 31) / 63;
uint8_t b = ((col & 0b00011111) * 255 + 15) / 31;
return (union { uint8_t bytes[4]; uint32_t hex; }){b, g, r, 0xff}.hex;
}
Распаковать обратно в каналы можно так:
// col - rgb565 (uint16_t)
uint8_t r = (col & 0b1111100000000000) >> 8;
uint8_t g = (col & 0b0000011111100000) >> 3;
uint8_t b = (col & 0b0000000000011111) << 3;
RGB wheel #
В цветовом пространстве RGB можно получить "радугу" - плавный переход между цветами в порядке цветов радуги, для этого нужно подавать сигналы согласно следующей закономерности:
Вариант реализации для 8 бит радуги (значение 0-255):
uint32_t raibow8(uint8_t value) {
uint8_t shift, r, g, b;
if (value > 170) {
shift = (value - 170) * 3;
r = shift;
g = 0;
b = 255 - shift;
} else if (value > 85) {
shift = (value - 85) * 3;
r = 0;
g = 255 - shift;
b = shift;
} else {
shift = value * 3;
r = 255 - shift;
g = shift;
b = 0;
}
// либо забрать как каналы
return ((uint32_t)r << 16) | ((uint32_t)g) << 8 | b;
}
Либо вариант с 0-1530 значениями "радуги":
uint32_t wheel1530(uint16_t value) {
uint8_t r, g, b;
switch (value) {
case 0 ... 255:
r = 255;
g = value;
b = 0;
break;
case 256 ... 510:
r = 510 - value;
g = 255;
b = 0;
break;
case 511 ... 765:
r = 0;
g = 255;
b = value - 510;
break;
case 766 ... 1020:
r = 0;
g = 1020 - value;
b = 255;
break;
case 1021 ... 1275:
r = value - 1020;
g = 0;
b = 255;
break;
case 1276 ... 1530:
r = 255;
g = 0;
b = 1530 - value;
break;
}
// либо забрать как каналы
return ((uint32_t)r << 16) | ((uint32_t)g) << 8 | b;
}
HSV #
Ещё одно полезное цветовое пространство - HSV (Hue Saturation Value) - цвет, насыщенность, яркость:
Быстрый вариант преобразования, не очень точный:
uint32_t HSVfast(uint8_t h, uint8_t s, uint8_t v) {
uint8_t r, g, b;
uint8_t value = ((24 * h / 17) / 60) % 6;
uint8_t vmin = (long)v - v * s / 255;
uint8_t a = (long)v * s / 255 * (h * 24 / 17 % 60) / 60;
uint8_t vinc = vmin + a;
uint8_t vdec = v - a;
switch (value) {
case 0: r = v; g = vinc; b = vmin; break;
case 1: r = vdec; g = v; b = vmin; break;
case 2: r = vmin; g = v; b = vinc; break;
case 3: r = vmin; g = vdec; b = v; break;
case 4: r = vinc; g = vmin; b = v; break;
case 5: r = v; g = vmin; b = vdec; break;
}
// либо забрать как каналы
return ((uint32_t)r << 16) | ((uint32_t)g) << 8 | b;
}
И медленный, но более красивый:
uint32_t HSV(uint8_t h, uint8_t s, uint8_t v) {
float R, G, B;
float H = h / 255.0;
float S = s / 255.0;
float V = v / 255.0;
int i = int(H * 6);
float f = H * 6 - i;
float p = V * (1 - S);
float q = V * (1 - f * S);
float t = V * (1 - (1 - f) * S);
switch (i % 6) {
case 0: R = V, G = t, B = p; break;
case 1: R = q, G = V, B = p; break;
case 2: R = p, G = V, B = t; break;
case 3: R = p, G = q, B = V; break;
case 4: R = t, G = p, B = V; break;
case 5: R = V, G = p, B = q; break;
}
uint8_t r = R * 255.0;
uint8_t g = G * 255.0;
uint8_t b = B * 255.0;
// либо забрать как каналы
return ((uint32_t)r << 16) | ((uint32_t)g) << 8 | b;
}