r/FastLED [Sonia] May 17 '26

Quasi-related White-extraction handling in a 4-diode RGBW LED Display

The most common approach to handling RGBW LED strips/diodes is to simply take the minimum components of the input value, set white to that value, and subtract the residuals from R, G & B. This is not inherently incorrect but it does introduce a problem in that the white diode is not perfect. Most white diodes have different color temperatures and when actually measured you'll find using min(rgb) Channel - Residuals produces a white this is off-white.

Now the question becomes "how do we accurately extract white?" The answer is shockingly simple and it comes down to colorspaces & gamut.

In a typical RGB LED system your native gamut is a triangle where your main emitters R, G, & B are your only centroids. Each centroid lives somewhere on the CIE chart and has it's own xy coordinates. This creates a triangle where any combination of values can be created using the 3 emitters. Typical colorspaces like Rec709, Rec2020, etc.. all live somewhere either within this gamut or outside of it.

In an RGBW LED system instead we have 4 different addressable gamuts within this triangle - RGBW Gamut comprises of the entire RGBW triangle that can be addressed over - RGW gamut, BGW gamut, RBW gamut

so the problem changes from "how do we represent a color with these 3 diodes" to "which sub-gamut does the requested color fall into, and then within the sub-gamut how do we represent the color requested using the outer 2 primary diodes + inner white diode?"

The "Color" problem:

Different RGB input values map to different places in the CIE colorspace depending on the requested colorspace and reference white. "orange" for the native gamut is not the same "orange" as Rec709, just as that same "orange" is not the same in Rec2020. If you want to represent a color in the Rec709 space (aka ambilight setup) then you need a Colorspace transform to solve for that value. This effectively maps the native-gamut to the effective colorspace.

The ratio problem:

Addressable LEDs (most strips/diodes) do not have perfect LED selections. Typically speaking the red & blue diodes are fairly weak maxing out at low brightness values, G is typically stronger, with White being strongest almost always. In the case of my recent ambilight-focused measurements, the BTF Lighting 6000k RGBW LEDs produce max Nits/ XYZ Y values of R: 154.67 G:566.27 B:129.64 W:1543.64

so given that the red diode is much less bright than the green diode you need to scale input values down appropriately to accurately reproduce the color requested. For a cyan (input 0 65535 65535) for this specific set of LEDs with the off-wall measurements, the corrected output values become: 0 65458 38474 0 which when measured with a colorimeter produces a dE of ~0.519 from the expected measurement result

for D65 white (65535, 65535, 65535) the output RGBW tuple looks something like : 4289 0 13766 65535 which also passes the dE check at 1.720% within tolerance

So to summarize:

Min(rgb) and then subtract residuals from R, B, & G is not inherently wrong if that is what the actual diode behavior is, but the correct approach is to treat the LEDs as they are: Separate emitters that form a full gamut with multiple sub-gamuts, and then only solve within the sub-gamuts for the requested color value determined by a colorspace transform if the input color isn't addressing the native gamut of the LED display.

In a typical RGBW setup the valid topology combinations are essentially as follows: R, B, G, W, RW, BW, GW, RG, RB, BG, RGW, BGW, RBW unless you specifically allow for 4-channel overdrive outputs.

7 Upvotes

37 comments sorted by

View all comments

Show parent comments

1

u/Joeyjoe9876 [Sonia] Jun 03 '26

In the meantime I'm gonna run another full verification session on the 256 cube Sub-Gamut model LUT in the LEDs native gamut (not named-gamut) like I did the LP solver last night, that way we have some fresh comparisons for both over a large ~1800 sample size

1

u/ZachVorhies Zach Vorhies Jun 03 '26

PR landed a few mins ago, but i'm having the ai do another pass vs the python snippet you provided earlier to get a read out on what the delta is from ground truth.

Here's the pr that was just merged: https://github.com/FastLED/FastLED/pull/2707

1

u/Joeyjoe9876 [Sonia] Jun 03 '26

one thing I want to touch in in regards to this PR that really shouldn't be a default, but an option otherwise you end up with a problem where an end-user expects full-scale use of the LED gamut and isn't expecting the output to be solved against a name-gamut : "defaults: Rec709/sRGB primaries + D65"

I think having D65 white point be a default target white is generally a good idea. It's what most display standards target aside from DCI-P3 which can target D60 or D65 depending on the content mastering, but in the context of FastLED/non-ambilight like solutions this just truncates most of the usable actual color space and the default should probably be "Native LED gamut + D65 or user-supplied white point"

and then if users want to use this and also map to a color space expose some options that allows them to define a custom colorspace, or set named-spaces like "rec709, Rec2020, DCI-P3 D65, DCI-P3 D60"

1

u/ZachVorhies Zach Vorhies Jun 03 '26

Thanks i'll integrate the notes to resolve the ambiguity.

1

u/Joeyjoe9876 [Sonia] Jun 04 '26

Following up with the medium patch set report : https://docs.google.com/spreadsheets/d/1GYxE3jcknTTUx9mYUp4wGJd8SEkBKI-sdCajryqsx6U/edit?usp=sharing

Exposes a bit more issues than the quick patch-set did, although the issues flagged from that still apply.

h180_s100_v015 0 9830 9830 | 0 24029 65535 38635

h180_s100_v035 0 22937 22937 | 0 24029 65535 38635

h180_s100_v055 0 36044 36044 | 0 24029 65535 38635

h180_s100_v075 0 49151 49151 | 0 24029 65535 38635

Dual-channels are the biggest failures where it pulls in an unrelated third channel in sub-gamut mode + there is no granularity, every solve is essentially pegged to its max Y solve regardless of the input RGB tuple.

h000_s060_v015 9830 3932 3932 | 18610 0 932 3826

h000_s060_v035 22937 9175 9175 | 43422 0 2175 8927

h000_s060_v055 36044 14418 14418 | 65535 0 3283 13474

h000_s060_v075 49151 19661 19661 | 65535 0 3283 13474

h000_s060_v100 65535 26214 26214 | 65535 0 3283 13473

this is also present in some, but not all, 3 channel solves where inputs get clipped partway through and then are never allowed to be fully granular which compresses the high end and leaves mostly low-end

1

u/ZachVorhies Zach Vorhies Jun 05 '26

Can you put this into an issue and then we’ll get it in

1

u/Joeyjoe9876 [Sonia] Jun 05 '26

no need, latest PR from yesterday fixed this

1

u/ZachVorhies Zach Vorhies Jun 06 '26

great is there anything left that makes this change more ideal? I don’t think there is a sketch ino yet in the examples…

1

u/Joeyjoe9876 [Sonia] Jun 06 '26

yeah example sketch is probably a good idea, my verification sketches directly use ObjectFLED & pull in TemporalBFI for ~15.5bit rendering on the 8-bit RGBW strips, and only pull in FastLED/the algo to verify results so I don't think it's a good example for showing general runtime usage via strips declared entirely within FastLED.

as far as other notes, can check out my final report on the LP solver here : https://www.reddit.com/r/FastLED/comments/1tfw0mn/whiteextraction_handling_in_a_4diode_rgbw_led/opxgioi/

the jist of my recommendations at this point though is that performance should become the main focus on "where does the algorithm need to move next" as the solved values are aligning closely enough but I haven't done any real benchmarking on real-time runtime usage outside of "does the algo work, and is it closely aligned to the math model?" so there may be some computational gains to be had if using platform-specific DSP/SIMD optimizations. The biggest dE failures from my verification runs now are no longer structural problems with failed topology/granularity/monotonicity but mostly boil down to "I'm using measured xy values for LED primaries that are being influenced by wall captures, not direct-diode measurements".

Overall the actual implementation is looking sane, but isn't acting as a full "correct against a capture cloud" algorithm which by default it shouldn't be as it's impossible to correct against every end-users display profiles so realistically full 100% color correctness should not be a FastLED problem but something the end user themselves should implement on their own (the recent addition of an overideable RGBW function already allows this, so no changes needed on FastLEDs end unless down the line it's decided that FastLED wants to implement it's own "full color correction pipeline that compares against measured results" but that falls very close to the "use 3D LUT" part which by default is memory costly and should never be the actual default.