# Color Helpers
# (c) 2016-2017 Simon Leiner
# licensed under the GNU Public License, version 2
#
# This module provides helper functions and classes for the lightshows:
# - grayscale_correction(lightness, max_in, max_out)
# - linear_dim(undimmed, factor)
# - is_rgb_color_tuple(to_check)
# - add_tuples(tuple1, tuple2)
# - blend_whole_strip_to_color(strip, color, fadetime_sec)
# - wheel(wheel_pos)
#
# - SmoothBlend
import types
import logging
import time
from drivers import LEDStrip
from helpers import verify, exceptions
[docs]def grayscale_correction(lightness: float, max_in: float = 255.0, max_out: int = 255):
"""\
Corrects the non-linear human perception of the led brightness according to the CIE 1931 standard.
This is commonly mistaken for gamma correction. [#gamma-vs-lightness]_
.. admonition:: CIE 1931 Lightness correction [#cie1931-source]_
The human perception of brightness is not linear to the duty cycle of an LED.
The relation between the (perceived) lightness :math:`Y`
and the (technical) lightness :math:`L^*` was described by the CIE:
.. math::
:nowrap:
\\begin{align}
Y & = Y_{max} \cdot g( ( L^* + 16) / 116 ) \\quad ,& \\quad 0 \\le L^* \\le 100 \\\\
\\text{with} \\quad g(t) & =
\\begin{cases}
3 \cdot \\delta^2 \cdot ( t - \\frac{4}{29}) & t \\le \\delta \\\\
t^3 & t > \\delta
\\end{cases}
\\quad ,& \\quad \\delta = \\frac{6}{29}
\\end{align}
For more efficient computation, these two formulas can be simplified to:
.. math::
Y =
\\begin{cases}
L^* / 902.33 & L^* \le 8 \\\\
((L^* + 16) / 116)^3 & L^* > 8
\\end{cases} \\\\
\\\\
0 \\le Y \\le 1 \\qquad 0 \\le L^* \\le 100
.. [#gamma-vs-lightness] For more information, read here: https://goo.gl/9Ji129
.. [#cie1931-source] formula from
`Wikipedia <https://en.wikipedia.org/wiki/Lab_color_space#Reverse_transformation>`_
:param lightness: linear brightness value between 0 and max_in
:param max_in: maximum value for lightness
:param max_out: maximum output integer value (255 for 8-bit LED drivers)
:return: the correct PWM duty cycle for humans to see the desired lightness as integer
"""
# safeguard and shortcut
if lightness <= 0:
return 0
elif lightness >= max_in:
return max_out
# apply the formula from aboce
l_star = lightness / max_in * 100 # map from 0..max_in to 0..100
if l_star <= 8:
duty_cycle = l_star / 902.33
else:
duty_cycle = ((l_star + 16) / 116) ** 3
return round(duty_cycle * max_out) # this will be an integer!
[docs]def wheel(wheel_pos: float):
"""\
Get a color from a color wheel: Green -> Red -> Blue -> Green
:param wheel_pos: numeric from 0 to 254
:return: RGB color tuple
"""
if wheel_pos > 254:
wheel_pos = 254 # Safeguard
if wheel_pos < 85: # Green -> Red
color = (wheel_pos * 3, 255 - wheel_pos * 3, 0)
elif wheel_pos < 170: # Red -> Blue
wheel_pos -= 85
color = (255 - wheel_pos * 3, 0, wheel_pos * 3)
else: # Blue -> Green
wheel_pos -= 170
color = (0, wheel_pos * 3, 255 - wheel_pos * 3)
return color
[docs]def linear_dim(undimmed: tuple, factor: float) -> tuple:
"""\
Multiply all components of undimmed with factor
:param undimmed: the vector
:param factor: the factor to multiply the components of the vector byy
:return: resulting RGB color vector
"""
dimmed = ()
for i in undimmed:
i *= factor
dimmed += i, # merge tuples
return dimmed
[docs]def add_tuples(tuple1: tuple, tuple2: tuple):
"""\
Add two tuples component-wise
:param tuple1: summand
:param tuple2: summand
:return: sum
"""
if len(tuple1) is not len(tuple2):
return None # this type of addition is not defined for tuples with different lengths
# calculate sum
sum_of_two = []
for i in range(len(tuple1)):
sum_of_two.append(tuple1[i] + tuple2[i])
return tuple(sum_of_two)
[docs]class SmoothBlend:
"""\
This class lets the user define a specific state of the strip (:py:attr:`target_colors`)
and then smoothly blends the current state over to the set state.
"""
logger = logging.getLogger('102shows.server.helpers.color.SmoothBlend')
def __init__(self, strip: LEDStrip):
self.strip = strip
self.target_colors = [(0.0, 0.0, 0.0)] * self.strip.num_leds #: an array of float tuples
[docs] def set_pixel(self, led_num: int, red: float, green: float, blue: float):
""" set the desired state of a given pixel after the blending is finished """
# check if the given color values are valid
try:
verify.rgb_color_tuple((red, green, blue))
except exceptions.InvalidParameters as error_message:
self.logger.error(error_message)
# store in buffer
self.target_colors[led_num] = (red, green, blue)
[docs] def set_color_for_whole_strip(self, red: float, green: float, blue: float):
""" set the same color for all LEDs in the strip """
for led_num in range(self.strip.num_leds):
self.set_pixel(led_num, red, green, blue)
[docs] class BlendFunctions:
"""\
.. todo:: Include blend pictures directly in documentation
An internal class which provides functions to blend between two colors by a parameter fade_progress
for ``fade_progress == 0`` the function should return the start_color
for ``fade_progress == 1`` the function should return the end_color
"""
def __init__(self):
pass
@classmethod
[docs] def linear_blend(cls, start_color: tuple, end_color: tuple, fade_progress: float) -> tuple:
""" linear blend => see https://goo.gl/lG8RIW """
return cls.power_blend(1, start_color, end_color, fade_progress)
@classmethod
[docs] def parabolic_blend(cls, start_color: tuple, end_color: tuple, fade_progress: float) -> tuple:
""" quadratic blend => see https://goo.gl/hzeFb6 """
return cls.power_blend(2, start_color, end_color, fade_progress)
@classmethod
[docs] def cubic_blend(cls, start_color: tuple, end_color: tuple, fade_progress: float) -> tuple:
""" cubic blend => see https://goo.gl/wZWm07 """
return cls.power_blend(3, start_color, end_color, fade_progress)
@classmethod
[docs] def power_blend(cls, power: float, start_color: tuple, end_color: tuple, fade_progress: float) -> tuple:
""" blend two colors using a power function, the exponent is set via param power """
start_component = linear_dim(start_color, fade_progress ** power)
target_component = linear_dim(end_color, (1 - fade_progress) ** power)
return add_tuples(start_component, target_component)
[docs] def blend(self, time_sec: float = 2, blend_function: types.FunctionType = BlendFunctions.linear_blend):
""" blend the current LED state to the desired state """
# buffer current status
initial_colors = []
for led_num in range(self.strip.num_leds):
initial_colors.append(self.strip.get_pixel(led_num))
# do the actual fadeout
now = time.perf_counter()
end_time = time.perf_counter() + time_sec
while now < end_time:
fade_progress = (end_time - now) / time_sec
for led_num in range(self.strip.num_leds):
color = blend_function(initial_colors[led_num], self.target_colors[led_num], fade_progress)
self.strip.set_pixel(led_num, *color)
self.strip.show()
now = time.perf_counter()
# set to final target state
for led_num in range(self.strip.num_leds):
self.strip.set_pixel(led_num, *(self.target_colors[led_num]))
self.strip.show()
[docs]def blend_whole_strip_to_color(strip: LEDStrip, color: tuple, fadetime_sec: float = 2) -> None:
"""
this name is pretty self-explanatory ;-)
:param strip: LEDStrip object
:param color: the color to blend two
:param fadetime_sec: the time in seconds to blend in
"""
transition = SmoothBlend(strip)
transition.set_color_for_whole_strip(*color)
transition.blend(time_sec=fadetime_sec)