Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Implentation of hsv2rgb_rainbow() from FastLED #32

Open
@Neradoc

Description

@Neradoc

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", )

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions