Converting RGB to RGBW
Asked Answered
S

7

7

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

Sladen answered 28/10, 2016 at 19:44 Comment(9)
I wonder if your integer divisions are part of the problem. Don't you want the multiple to be a float/decimal and truncate/round the final values?Longhorn
What do you mean by "doesn't work"?Seraphic
@Longhorn I don't think so. I'm pretty sure .NET deals with that. I get the same result by dividing with doubles or floats and then converting to an integer, but I get the same thing.Sladen
@Paul Abbott I've updated the post with a gif showing the issue.Sladen
int multiplier = 255 / M; - does this really want to be integer division?Plasticize
Change multiplier to float and see if it doesn't get you a lot closer.Longhorn
I've updated that integer division with a float (yeah, stupid mistake), but it hasn't changed much. it still looks the same as in the .gif.Sladen
float multiplier = 255 / M; is still integer division as both 255 and M are integers. Change it to float multiplier = 255.0 / M;Plasticize
Work through your formula by hand with values where you know what the result should be and see if it gets the right answer. Though again, your calculation is all being done with integer divisions. All your variables are integers.Plasticize
S
13

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 :)

Sladen answered 29/10, 2016 at 11:25 Comment(1)
This solution gives the exact same results than the direct conversion: Wo = Min(Ri, Gi, Bi); Ro = Ri - Wo; Go = Gi - Wo; Bo = Bi - Wo; Am I missing something here?Pinworm
S
9

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);
}
Scandic answered 11/1, 2021 at 19:17 Comment(0)
P
1

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

Plasticize answered 28/10, 2016 at 20:0 Comment(1)
Fixed, and still nothing. I'm pretty sure the issue comes from the actual logic I'm using. If you look at what happens when I try saturating/de-saturating the turquoise, I've got a white output of 255 when it should be 0 (There's no white in fully saturated turquoise). It should be behaving like when I saturate/de-saturate the blue.Sladen
W
1

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.

Winnah answered 22/4, 2020 at 4:44 Comment(0)
A
1

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)
Attaint answered 18/8, 2021 at 7:48 Comment(0)
U
0

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.

Uni answered 16/12, 2017 at 11:40 Comment(1)
However if I have a very high CRI white led, would having the RGB channels at full not deteriorate the quality of the light?Sladen
G
0

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...

Geostrophic answered 5/7, 2020 at 21:34 Comment(2)
See my answer below - I put an algorithm that takes into account color tempScandic
@DanielMurphy thank you! The one thing that I'm still struggling (after almost a year) is that the intensity of W-white is not equal the the intensity of RGB-white. And the simple solution of multiplying your constants with the relative intensity of the white LED breaks down in the low intensities (probably because I'm not taking gamma correction into account)...Geostrophic

© 2022 - 2024 — McMap. All rights reserved.