user2950509
user2950509

Reputation: 1057

Converting RGB to RGBW

I know this has already been asked, but the anwser given there doesn't work. I've spent over an hour looking for a formula or algorithm, but have found nothing. As a result, I've started writing my own algorithm to convert RGB to RGBW in the most efficient way possible. This is what I've currently got:

        //'Ri', 'Gi', and 'Bi' correspond to the Red, Green, and Blue inputs.
        var M = Math.Max(Ri, Math.Max(Gi, Bi)); //The maximum value between R,G, and B.
        int Wo =0; //White output
        int Ro=0; //Red output
        int Go=0; //Green output
        int Bo=0; //Blue output

        int av = 0; //Average between the two minimum values
        int hR = 0; //Red with 100% hue
        int hG = 0; //Green with 100% hue
        int hB = 0; //Blue with 100% hue

        //These 4 lines serve to figure out what the input color is with 100% hue.
        float multiplier = 255.0f / M;
        hR = Convert.ToInt32(Ri * multiplier);
        hG = Convert.ToInt32(Gi * multiplier);
        hB = Convert.ToInt32(Bi * multiplier);

        //Depending on the maximum value, get an average of the least used colors, weighted for their importance in the overall hue.
        //This is the problematic part
        if (M == Ri)
           av = (Bi*hB + Gi*hG) / (hB+hG);
        else if (M == Gi)
            av = (Ri*hR + Bi*hB) / (hR+hB);
        else if (M == Bi)
            av = (Gi*hG + Ri*hR) / (hG+hR);

        //Set the rgbw colors
        Wo = av;
        Bo = Bi - av;
        Ro = Ri - av;
        Go = Gi - av;
        if (Wo < 1) Wo = 0;
        if (Bo < 1) Bo = 0;
        if (Ro < 1) Ro = 0;
        if (Go < 1) Go = 0;
        if (Wo > 255) Wo = 255;
        if (Bo > 255) Bo = 255;
        if (Ro > 255) Ro = 255;
        if (Go > 255) Go = 255;

It works fine if the color I'm dealing with is a primary color, but not in any other case. What would make it work everywhere? Am I even on the right track?

EDIT: Here's a .gif of the issue I'm running into. the RGBW values are all the way at the bottom enter image description here

Upvotes: 7

Views: 12978

Answers (7)

iamh2o
iamh2o

Reputation: 11

This repo can do the job for you: https://github.com/iamh2o/rgbw_colorspace_converter/

I wrote this module with a friend in such a way that 'color' objects could be instantiated via several color systems, and the object could spit out translations to all the other colorsystems it supports- which after a LOT of research (a key piece being https://www.neltnerlabs.com/saikoled/how-to-convert-from-hsi-to-rgb-white ), we finally nailed the [HSI/HSL/HSV/RGB/HEX] -> RGBW conversion.

There are a ton of packages that have the general colorspace problem solved, but it seems the RGBW case is pretty specific to physical lighting/leds, and not applicable to digital displays, RGBW was not included in any modules I'd looked at.

And the killer feature of this module is that the color objects you instantiate can be manipulated in several color systems depending on your needs (different ones that you created it in), and it will keep all of the translations to the other spaces up to date- and it's super fast, we've not yet had it be a frame rate limiting component.

So something like this would be a loop through the fully bright, fully saturated rainbow (note how the RGB vs the HSV codes are far less amenable to programatic manipulation):

from rgbw_colorspace_converter.colors.converters import RGB

color = RGB(255,0,0)

ctr = 0

while ctr < 10:
     color.hsv_h += .1
     print(f"HSV:{color.hsv}  RGB:{color.rgb}  HSI:{color.hsi} HEX:{color.hex}")
     ctr += 1

# "H" in hsv is actually expressed in 360 degrees, and it is cylindrical. We've normalized it to being between 0-1 (so H=0=H=1 - both are red)
HSV:(0.0, 1.0, 1.0)  RGB:(255, 0, 0)  HSI:(0.0, 1.0, 0.33333) HEX:#ff0000
HSV:(0.1, 1.0, 1.0)  RGB:(255, 153, 0)  HSI:(36.0, 1.0, 0.533328) HEX:#ff9900
HSV:(0.2, 1.0, 1.0)  RGB:(203, 255, 0)  HSI:(72.23529411764707, 1.0, 0.5986868235294117) HEX:#cbff00
HSV:(0.3, 1.0, 1.0)  RGB:(51, 255, 0)  HSI:(108.0, 1.0, 0.399996) HEX:#33ff00
HSV:(0.4, 1.0, 1.0)  RGB:(0, 255, 102)  HSI:(144.0, 1.0, 0.46666199999999997) HEX:#00ff66
HSV:(0.5, 1.0, 1.0)  RGB:(0, 255, 255)  HSI:(180.0, 1.0, 0.66666) HEX:#00ffff
HSV:(0.6, 1.0, 1.0)  RGB:(0, 102, 255)  HSI:(216.0, 1.0, 0.46666199999999997) HEX:#0066ff
HSV:(0.7, 1.0, 1.0)  RGB:(50, 0, 255)  HSI:(251.76470588235296, 1.0, 0.39868882352941176) HEX:#3200ff
HSV:(0.8, 1.0, 1.0)  RGB:(204, 0, 255)  HSI:(288.0, 1.0, 0.599994) HEX:#cc00ff
HSV:(0.9, 1.0, 1.0)  RGB:(255, 0, 152)  HSI:(324.2352941176471, 1.0, 0.5320208235294118) HEX:#ff0098
HSV:(1.0, 1.0, 1.0)  RGB:(255, 0, 0)  HSI:(0.0, 1.0, 0.33333) HEX:#ff0000


# AND- you can access the other color systems with
# color.rgbw, color.hsi, color.hsl /// All of
# which change as the RGB or HSV properties are changed 
# (you can even change them in different spaces on the same object)

Upvotes: 1

Daniel Murphy
Daniel Murphy

Reputation: 882

I made an algorithm that takes into account the color temperature of your "white" LED (as this can vary based on the strip - mine was warm-white, which is 4500k). Gist is here, code is below, and blog post with more context is Ouch! My Eyes! Arduino WS2812B Setup & RGBW Transformation (Cabinet Light Pt. 3).

// Reference, currently set to 4500k white light:
// https://andi-siess.de/rgb-to-color-temperature/
const uint8_t kWhiteRedChannel = 255;
const uint8_t kWhiteGreenChannel = 219;
const uint8_t kWhiteBlueChannel = 186;

// The transformation has to be normalized to 255
static_assert(kWhiteRedChannel >= 255 ||
              kWhiteGreenChannel >= 255 ||
              kWhiteBlueChannel >= 255);

CRGBW GetRgbwFromRgb2(CRGB rgb) {
  uint8_t r = rgb.r;
  uint8_t g = rgb.g;
  uint8_t b = rgb.b;

  // These values are what the 'white' value would need to
  // be to get the corresponding color value.
  double whiteValueForRed = r * 255.0 / kWhiteRedChannel;
  double whiteValueForGreen = g * 255.0 / kWhiteGreenChannel;
  double whiteValueForBlue = b * 255.0 / kWhiteBlueChannel;

  // Set the white value to the highest it can be for the given color
  // (without over saturating any channel - thus the minimum of them).
  double minWhiteValue = min(whiteValueForRed,
                             min(whiteValueForGreen,
                                 whiteValueForBlue));
  uint8_t Wo = (minWhiteValue <= 255 ? (uint8_t) minWhiteValue : 255);

  // The rest of the channels will just be the original value minus the
  // contribution by the white channel.
  uint8_t Ro = (uint8_t)(r - minWhiteValue * kWhiteRedChannel / 255);
  uint8_t Go = (uint8_t)(g - minWhiteValue * kWhiteGreenChannel / 255);
  uint8_t Bo = (uint8_t)(b - minWhiteValue * kWhiteBlueChannel / 255);

  return CRGBW(Ro, Go, Bo, Wo);
}

Upvotes: 9

Jack Jansen
Jack Jansen

Reputation: 81

I came across this topic while researching my own RGB to RGBW conversion (which doesn't work yet:-), and it seems I miss one important factor in this conversion and that is the color temperature of the white LED. The color temperature of "RGB-white" is probably 6500 (at least that seems to be true for all RGB leds I've come across), and you seem to be treating your white LED channel as if it also has that color temperature, but 4000 or 5000 is much more common for RGBW LEDs...

Upvotes: 0

user13377750
user13377750

Reputation: 11

I took a different approach. I just check for the lowest of the values and make that the white channel, then subtract that value from the red, green and blue channels. This is a bit simplified, but it works well and is easy to implement. 128,128,0 becomes 128,128,0,0 64,64,128 becomes 0,0,64,64, etc. This also allows for color correction to be easily implemented. Say blue is a little strong (typical) constrain the blue channel to something lower like 250 instead of 255 when you build the rgbw values. I can't say that this would work well in C or C++, but it works in ADA very well.

Upvotes: 1

Giannis
Giannis

Reputation: 1

I might be a little off here, but as an electronics engineer I see leds outputing light and then code XD. Anyway outputing white can be done by "lighting" all rgb or white channels, but keeping the same intensity (and the well being of the leds from my point of view) all 4 channels can be mixed to output the same white as the overdriven white channel. I doubt this is of any issue at this point, but it might be helpful later.

Upvotes: 0

user2950509
user2950509

Reputation: 1057

I've finally figured out how to convert RGB to RGBW, turns out my previous method was completely wrong:

//Get the maximum between R, G, and B
float tM = Math.Max(Ri, Math.Max(Gi, Bi));

//If the maximum value is 0, immediately return pure black.
if(tM == 0)
   { return new rgbwcolor() { r = 0, g = 0, b = 0, w = 0 }; }

//This section serves to figure out what the color with 100% hue is
float multiplier = 255.0f / tM;
float hR = Ri * multiplier;
float hG = Gi * multiplier;
float hB = Bi * multiplier;  

//This calculates the Whiteness (not strictly speaking Luminance) of the color
float M = Math.Max(hR, Math.Max(hG, hB));
float m = Math.Min(hR, Math.Min(hG, hB));
float Luminance = ((M + m) / 2.0f - 127.5f) * (255.0f/127.5f) / multiplier;

//Calculate the output values
int Wo = Convert.ToInt32(Luminance);
int Bo = Convert.ToInt32(Bi - Luminance);
int Ro = Convert.ToInt32(Ri - Luminance);
int Go = Convert.ToInt32(Gi - Luminance);

//Trim them so that they are all between 0 and 255
if (Wo < 0) Wo = 0;
if (Bo < 0) Bo = 0;
if (Ro < 0) Ro = 0;
if (Go < 0) Go = 0;
if (Wo > 255) Wo = 255;
if (Bo > 255) Bo = 255;
if (Ro > 255) Ro = 255;
if (Go > 255) Go = 255;
return new rgbwcolor() { r = Ro, g = Go, b = Bo, w = Wo };

enter image description here

Any optimization ideas are more than welcome :)

Upvotes: 13

ChrisF
ChrisF

Reputation: 137108

I think the problem is here:

    if (M == Ri)
       av = (Bi*hB + Gi*hG) / (hB+hG);
    else if (M == Gi)
        av = (Ri*hR + Bi*hB) / (hR+hB);
    else if (M == Bi)
        av = (Gi*hG + Ri*hR) / (hG+hR);

You are doing integer divisions here. All your variables are integers so the division will be integer division.

    if (M == Ri)
       av = (int)((float)(Bi*hB + Gi*hG) / (hB+hG));
    else if (M == Gi)
        av = (int)((float)(Ri*hR + Bi*hB) / (hR+hB));
    else if (M == Bi)
        av = (int)((float)(Gi*hG + Ri*hR) / (hG+hR));

This will do a floating point division and should get you the answer you need. You still might find you have rounding errors - in that case change float to double.

Changing the multiplier calculation to float:

float multiplier = 255.0 / M;

only addressed half the issue, but now causes another complication as your tests:

if (M == Ri)
else if (M == Gi)
else if (M == Bi)

are now unlikely to ever be true. You'll need to add a rounding error factor into the test.

if (Math.Abs(M - Ri) < epsilon)
else if (Math.Abs(M - Gi) < epsilon)
else if (Math.Abs(M - Bi) < epsilon)

where epsilon is a suitable small value (normally I'd suggest 10e-6, but you can experiment.

Additionally, if M isn't close enough to any of the RGB values you don't set av - it always stays set to zero. This will also give you the result you're seeing where everything is black

Upvotes: 1

Related Questions