adafruit / adafruit/Adafruit_CircuitPython_FancyLED
Implentation of hsv2rgb_rainbow() from FastLED
- Dominant language
- Python
- Stars
- 36
- Forks
- 16
- PR merge metrics
- No merged PRs in 30d
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](https://fastled.io/docs/group___h_s_v2_r_g_b.html).
In order:
- hsv2rgb_rainbow()
- hsv2rgb_spectrum() from this library (maximum brightness)
- my implementation of FastLED's hsv2rgb_spectrum() (uniform brightness) ([following this graph](https://fastled.io/docs/group___h_s_v2_r_g_b.html#ga704160ddb9173c3420a3dbfb44674440))

```py
# 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)
```
```py
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", )
```
Contributor guide
No contributing guide indexed for this repository
Assessment
This issue has not been assessed yet.