<jupyter_start><jupyter_text>Configuration<jupyter_code># Generic imports and configuration.
%matplotlib inline
from matplotlib.pyplot import imshow, subplots
from numpy import array, zeros, ones
from colorsys import hsv_to_rgb
from random import randint<jupyter_output><empty_output><jupyter_text>Detectable colors<jupyter_code>class Color:
    """Light or surface color."""

    def __init__(self, h, s=100, v=100, name=None):
        self.h = None if h is None else h % 360
        self.s = max(0, min(s, 100))
        self.v = max(0, min(v, 100))
        self.name = name

    def __repr__(self):
        name_str = '' if self.name is None else ", '{}'".format(self.name)

        return "Color({}, {}, {}{})".format(self.h, self.s, self.v, name_str)

    def __eq__(self, other):
        return (isinstance(other, Color) and
                self.h == other.h and self.s == other.s and self.v == other.v)

    def __mul__(self, scale):
        v = max(0, min(self.v * scale, 100))
        return Color(self.h, self.s, int(v), self.name)

    def __rmul__(self, scale):
        return self.__mul__(scale)


# Color.BLACK = Color(None, 0, 15, 'BLACK')
# Color.GRAY = Color(None, 0, 50, 'GRAY')
# Color.WHITE = Color(None, 0, 100, 'WHITE')
# Color.RED = Color(0, 100, 100, 'RED')
# Color.ORANGE = Color(30, 100, 100, 'ORANGE')
# Color.BROWN = Color(30, 100, 30, 'BROWN')
# Color.YELLOW = Color(60, 100, 100, 'YELLOW')
# Color.GREEN = Color(120, 100, 100, 'GREEN')
# Color.CYAN = Color(180, 100, 100, 'CYAN')
# Color.BLUE = Color(240, 100, 100, 'BLUE')
# Color.VIOLET = Color(270, 100, 100, 'VIOLET')
# Color.MAGENTA = Color(300, 100, 100, 'MAGENTA')

# MAKE BRICK COLORS AVAILABLE?


# When measuring also do none in hue/sat?

# Color.GREEN = Color(130, 90, 40, 'GREEN')
Color.RED = Color(358, 95, 82, 'RED')
# Color.ORANGE = Color(1, 97, 93, 'ORANGE')
# Color.BROWN = Color(0, 66, 32, 'BROWN')
# Color.CYAN = Color(99, 93, 60, 'CYAN')
# Color.BLUE = Color(226, 63, 60, 'BLUE')

detectable_colors = []

for key, value in Color.__dict__.items():
    if type(value) == Color:
        detectable_colors.append(value)
detectable_colors.append(None)<jupyter_output><empty_output><jupyter_text>Color map implementation<jupyter_code># Integer implementation of Python's builtin hsv_to_rgb.
def pybricks_hsv_to_rgb(h, s, v):
    if h is None:
        h = 0
    return [int(c*255) for c in hsv_to_rgb(h/360, s/100, v/100)]

# Highest possible error cost between measured and discrete color.
INT32_MAX = 2147483547

# The currently implemented HSV cost function. This is a simplistic implementation
# to keep the new code working like the old color detection algorithm. The purpose
# of this Jupyter Notebook is to improve this particular function and review the
# result below.

# IDEAS:
   # LET HUE be none below certain value / saturation, leading to none error

def pybricks_hsv_cost(x, c):

    
    if c.h is None or x.h is None:
        hue_error = 0
    else:
        hue_error = c.h - x.h if c.h > x.h else x.h - c.h
        if hue_error > 180:
            hue_error = 360 - hue_error   
        
    saturation_error = abs(c.s - x.s)
    
    value_error = abs(c.v - x.v)
    
    cost = hue_error**2 + 5*saturation_error**2 + 2*value_error**2

    return cost 

# Given a measured color, loop through all colors in color_map to find the best match.
def pybricks_discretize_color(measured):

    # Initially there is no match and the errors are at a maximum.
    match = None
    cost_now = INT32_MAX
    cost_min = INT32_MAX

    # Iterate through candidate colors.
    for compare in detectable_colors:
        
        # For None, give values for comparison with measurement.
        if compare is None:
            compare = Color(None, 0, 0, 'None')

        # Evaluate the cost function for this candidate
        cost_now = pybricks_hsv_cost(measured, compare)

        # Update the minimum detected so far, and the corresponding matching color.
        if cost_now < cost_min:
            cost_min = cost_now
            match = compare

    # If None was the best match, return corresponding object.
    if match is not None and match.name == 'None':
        match = None
        
    return match

pybricks_hsv_cost(Color(120, 100, 100), Color.RED)<jupyter_output><empty_output><jupyter_text>Visualize<jupyter_code># We make several HUE-VALUE plots for several steps of saturation.
VALUE_INCREMENT = 11

# Define hue, saturation and value ranges.
value_range = range(99, -VALUE_INCREMENT, -VALUE_INCREMENT)
hue_range = range(360)
saturation_range = range(0, 101)

# Create the figure and its axes.
figure, axes = subplots(nrows=len(value_range), ncols=2, figsize=(12, 6*len(value_range)), facecolor='white')

# Create empty matrices to hold measured and discretized colors.
image_measured = zeros((101, 360, 3), dtype=int)
image_discrete = zeros((101, 360, 4), dtype=int)

# Create a graph with a few grey lines so that we can see 0-alpha of the overlay.
image_alphadot = ones((101, 360, 3), dtype=int)*255
for i in hue_range:
    if (i//10) % 2:
        for j in saturation_range:
            image_alphadot[j, i] = (180, 180, 180)

# Loop through all discrete value steps that we will visualize.
for value_index, value in enumerate(value_range):

    # Loop through saturation 0--100.
    for saturation in saturation_range:
        # Loop through hues 0--359.
        for hue in hue_range:
            
            # Convert h, s, v for this iteration to RGB for visalization.
            image_measured[saturation][hue][:] = pybricks_hsv_to_rgb(hue, saturation, value)
            # Get the discrete color for this h, s, v.
            discrete = pybricks_discretize_color(Color(hue, saturation, value))
            
            # Display the discrete color.
            if discrete is None:
                # If it's None, don't show anything.
                image_discrete[saturation][hue][:] = 0, 0, 0, 0
            else:
                # Convert discrete h, s, v, to RGB or visualization.
                r, g, b = pybricks_hsv_to_rgb(discrete.h, discrete.s, discrete.v)
                image_discrete[saturation][hue][:] = r, g, b, 255

    # Plot the measured and the discrete colors for the current saturation step.
    axes[value_index][0].imshow(image_measured, aspect=3.6, origin='lower')
    axes[value_index][1].imshow(image_alphadot, aspect=3.6, origin='lower')
    axes[value_index][1].imshow(image_discrete, aspect=3.6, origin='lower')
    for axis in axes[value_index]:
        axis.set_title('Value = {0}'.format(value))
        axis.set_xlabel('Hue')
        axis.set_ylabel('Saturation')<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.1149511645379414, 'fraction_numerical': 0.04087152516904583, 'mean_word_length': 3.080931943592888, 'pattern_counts': {'":': 0, '<': 18, '<?xml version=': 0, '>': 19, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '4', 'dir': 'jupyter-structured-clean-dedup', 'id': '484567', 'license': 'MIT', 'n_tokens_mistral': 2499, 'n_tokens_neox': 2186, 'n_words': 686, 'path': 'color.ipynb', 'repo_name': 'Nelvarion/pybricksdev-demo', 'size': '6655'}