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