Skip to content

Implentation of hsv2rgb_rainbow() from FastLED #32

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
Neradoc opened this issue Apr 1, 2025 · 1 comment
Open

Implentation of hsv2rgb_rainbow() from FastLED #32

Neradoc opened this issue Apr 1, 2025 · 1 comment

Comments

@Neradoc
Copy link

Neradoc commented Apr 1, 2025

I made an implementation of the rainbow hsv2rgb conversion function from FastLED.

Convert an HSV value to RGB using a visually balanced rainbow.
This "rainbow" yields better yellow and orange than a straight mathematical "spectrum".

I'm not sure how to better incorporate it into the library.
A function in adafruit_fancyled.py and a helper in fastled_helpers.py, or all in fastled_helpers.py ?

Included is test code generating the picture below for comparison with the hue chart from FastLED.
Reference: https://fastled.io/docs/group___pixel_types.html#gab316cfeb8bd5f37d8faaf761ad3c834b
Adapted from: https://github.com/FastLED/FastLED/blob/master/src/hsv2rgb.cpp (MIT license)

Note the comment about "maximum brightness at any given hue" style, vs. the "uniform brightness for all hues" style at the top of the "HSV to RGB Conversion Functions" page.

In order:

  • hsv2rgb_rainbow()
  • hsv2rgb_spectrum() from this library (maximum brightness)
  • my implementation of FastLED's hsv2rgb_spectrum() (uniform brightness) (following this graph)

Image

# SPDX-FileCopyrightText: Copyright FastLed https://github.com/FastLED
# SPDX-FileCopyrightText: Copyright 2025 Neradoc, https://neradoc.me
# SPDX-License-Identifier: MIT
"""
Ported to python from the FastLed library.
https://github.com/FastLED/FastLED/blob/master/src/hsv2rgb.cpp
"""

# Yellow has a higher inherent brightness than
# any other color; 'pure' yellow is perceived to
# be 93% as bright as white.  In order to make
# yellow appear the correct relative brightness,
# it has to be rendered brighter than all other
# colors.
# Level Y1 is a moderate boost, the default.
# Level Y2 is a strong boost.
Y1 = 1
Y2 = 0

# G2: Whether to divide all greens by two.
# Depends GREATLY on your particular LEDs
G2 = 0

# Gscale: what to scale green down by.
# Depends GREATLY on your particular LEDs
Gscale = 0


def scale8(i, scale):
    return (i * (1 + scale)) >> 8


def scale8_video(i, scale):
    return (1 if i and scale else 0) + ((i * scale) >> 8)


def hsv2rgb_rainbow(hsv):
    """
    Convert an HSV value to RGB using a visually balanced rainbow.
    This "rainbow" yields better yellow and orange than a straight mathematical "spectrum".
    
    :param Tuple(int, int, int) hsv: Color tuple (hue, saturation, value) as ints 0-255.
    :return Tuple(int, int, int): (red, green, blue) color tuple as ints 0-255.
    """
    hue, sat, val = hsv

    offset = hue & 0x1F  # 0..31

    offset8 = (offset * 8) % 256

    third = scale8(offset8, (256 // 3))  # max = 85

    r, g, b = 0, 0, 0

    if not (hue & 0x80):
        # 0XX
        if not (hue & 0x40):
            # 00X
            # section 0-1
            if not (hue & 0x20):
                # 000
                # case 0: # R -> O
                r = 255 - third
                g = third
                b = 0
            else:
                # 001
                # case 1: # O -> Y
                if Y1:
                    r = 171
                    g = 85 + third
                    b = 0
                if Y2:
                    r = 170 + third
                    twothirds = scale8(offset8, ((256 * 2) // 3))  # max=170
                    g = 85 + twothirds
                    b = 0
        else:
            # 01X
            # section 2-3
            if not (hue & 0x20):
                # 010
                # case 2: # Y -> G
                if Y1:
                    # uint8_t twothirds = (third << 1)
                    twothirds = scale8(offset8, ((256 * 2) // 3))  # max=170
                    r = 171 - twothirds
                    g = 170 + third
                    b = 0
                if Y2:
                    r = 255 - offset8
                    g = 255
                    b = 0
            else:
                # 011
                # case 3: # G -> A
                r = 0
                g = 255 - third
                b = third
    else:
        # section 4-7
        # 1XX
        if not (hue & 0x40):
            # 10X
            if not (hue & 0x20):
                # 100
                # case 4: # A -> B
                r = 0
                # uint8_t twothirds = (third << 1)
                twothirds = scale8(offset8, ((256 * 2) // 3))  # max=170
                g = 171 - twothirds  # 170?
                b = 85 + twothirds

            else:
                # 101
                # case 5: # B -> P
                r = third
                g = 0
                b = 255 - third
        else:
            if not (hue & 0x20):
                # 110
                # case 6: # P -- K
                r = 85 + third
                g = 0
                b = 171 - third

            else:
                # 111
                # case 7: # K -> R
                r = 170 + third
                g = 0
                b = 85 - third

    # This is one of the good places to scale the green down,
    # although the client can scale green down as well.
    if G2:
        g = g >> 1
    if Gscale:
        g = scale8_video(g, Gscale)

    # Scale down colors if we're desaturated at all
    # and add the brightness_floor to r, g, and b.
    if sat != 255:
        if sat == 0:
            r = 255
            b = 255
            g = 255
        else:
            desat = 255 - sat
            desat = scale8_video(desat, desat)
            satscale = 255 - desat
            # satscale = sat # uncomment to revert to pre-2021 saturation behavior

            # nscale8x3_video( r, g, b, sat)
            # brightness_floor = desat
            r = scale8(r, satscale) + desat
            g = scale8(g, satscale) + desat
            b = scale8(b, satscale) + desat

    # Now scale everything down if we're at value < 255.
    if val != 255:

        val = scale8_video(val, val)
        if val == 0:
            r = 0
            g = 0
            b = 0
        else:
            # nscale8x3_video( r, g, b, val)
            r = scale8(r, val)
            g = scale8(g, val)
            b = scale8(b, val)

    return (r, g, b)
import time
from hsvtorgb_rainbow import hsv2rgb_rainbow
from PIL import Image, ImageDraw, ImageColor
from adafruit_fancyled.fastled_helpers import hsv2rgb_spectrum

sampling = None # Image.Resampling.BICUBIC
# BICUBIC BILINEAR BOX HAMMING LANCZOS NEAREST
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (100, 100, 255) # brighter for easier viewing
SPC = 4
SPECTRUM = 80
CHART = 256
PIL_TOP = 0
THIS_TOP = SPECTRUM + SPC + CHART + SPC
HEIGHT = THIS_TOP * 3

img = Image.new("RGBA", (512, HEIGHT), color=(0, 0, 0))
draw = ImageDraw.Draw(img)

val = 255
sat = 255

for k in range(512):
    hue = k % 256
    rgb = hsv2rgb_rainbow((hue, sat, val))

    # Version generated by PIL basic RGB interpolation
    pilrgb = ImageColor.getrgb(f"hsv({hue*360/256}, {sat/2.56}%, {val/2.56}%)")

    # Version from FancyLED
    spec_color = hsv2rgb_spectrum(hue, sat, val)
    specrgb = tuple(int(256 * x) for x in (spec_color.red, spec_color.green, spec_color.blue))

    # Uniform brightness version from the graph in the fastLED docs
    if hue <= 85:
        r = 256 - hue * 3
        g = hue * 3
        b = 0
    elif hue <= 170:
        r = 0
        g = 256 - (hue - 85) * 3
        b = (hue - 85) * 3
    else:
        r = (hue - 170) * 3
        g = 0
        b = 256 - (hue - 170) * 3
    r, g, b = (int(x * val / 256) for x in (r, g, b))
    r, g, b = (int(x * sat / 256) + (256 - sat) for x in (r, g, b))
    fastleddoc = (r,g,b)

    # Out of those we display this one
    # refrgb = specrgb
    # for top, rgb_ref in zip( [PIL_TOP, THIS_TOP], [refrgb, rgb] ):

    for top, rgb_ref in zip(
        [PIL_TOP, THIS_TOP, 2*THIS_TOP],
        [rgb, specrgb, fastleddoc]
    ):
        # spectrum
        draw.line((k, top, k, top + SPECTRUM), fill=rgb_ref, width=2)
        # components
        offset = top + SPECTRUM + SPC
        for i, color in zip(rgb_ref, (RED, GREEN, BLUE)):
            height = (256 - i)
            position = (k, offset + height)
            endpoint = (k + 1, offset + height + 1)
            #draw.line((position, endpoint), fill=color, width=2)
            #draw.point(position, fill=color)
            draw.circle(position, radius=0.5, fill=color)

resized = img.resize((img.width, img.height // 2), sampling)
resized.save("_tmp_sample-out.png", )
@ace-johnny
Copy link

ace-johnny commented Apr 2, 2025

This FastLED hsv2rgb_rainbow() algorithm is my favorite and returns much more vibrant intermediary colors than the simple linear crossfades in rainbowio.colorwheel() and adafruit_fancyled.CHSV().

My only feature request, since this is actively being considered for inclusion, is to also accommodate normalized float (0.0-1.0) HSV inputs, as the FastLED 8-bit resolution is its only disadvantage compared to the existing functions listed above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants