Spaces:
Running
on
Zero
Running
on
Zero
import math | |
import random | |
import numpy as np | |
from . import colorblind | |
# pre-define interesting colours/points at corners, edges, faces and interior of | |
# r,g,b cube | |
WHITE = (1.0, 1.0, 1.0) | |
BLACK = (0.0, 0.0, 0.0) | |
RED = (1.0, 0.0, 0.0) | |
GREEN = (0.0, 1.0, 0.0) | |
BLUE = (0.0, 0.0, 1.0) | |
CYAN = (0.0, 1.0, 1.0) | |
YELLOW = (1.0, 1.0, 0.0) | |
MAGENTA = (1.0, 0.0, 1.0) | |
CORNERS = [WHITE, BLACK, RED, GREEN, BLUE, CYAN, YELLOW, MAGENTA] | |
MID_FACE = [ | |
(0.0, 0.5, 0.0), | |
(0.0, 0.0, 0.5), | |
(0.0, 1.0, 0.5), | |
(0.0, 0.5, 1.0), | |
(0.0, 0.5, 0.5), | |
(0.5, 0.0, 0.0), | |
(0.5, 0.5, 0.0), | |
(0.5, 1.0, 0.0), | |
(0.5, 0.0, 0.5), | |
(0.5, 0.0, 1.0), | |
(0.5, 1.0, 0.5), | |
(0.5, 1.0, 1.0), | |
(0.5, 0.5, 1.0), | |
(1.0, 0.5, 0.0), | |
(1.0, 0.0, 0.5), | |
(1.0, 0.5, 0.5), | |
(1.0, 1.0, 0.5), | |
(1.0, 0.5, 1.0), | |
] | |
INTERIOR = [ | |
(0.5, 0.5, 0.5), | |
(0.75, 0.5, 0.5), | |
(0.25, 0.5, 0.5), | |
(0.5, 0.75, 0.5), | |
(0.5, 0.25, 0.5), | |
(0.5, 0.5, 0.75), | |
(0.5, 0.5, 0.25), | |
] | |
POINTS_OF_INTEREST = CORNERS + MID_FACE + INTERIOR | |
_SEED_MAX = int(2**32 - 1) | |
def _ensure_rng(rng): | |
""" | |
Returns a random.Random state based on the input | |
""" | |
if rng is None: | |
rng = random._inst | |
elif isinstance(rng, int): | |
rng = random.Random(int(rng) % _SEED_MAX) | |
elif isinstance(rng, float): | |
rng = float(rng) | |
# Coerce the float into an integer | |
a, b = rng.as_integer_ratio() | |
if b == 1: | |
seed = a_color | |
else: | |
s = max(a.bit_length(), b.bit_length()) | |
seed = (b << s) | a | |
rng = random.Random(seed % _SEED_MAX) | |
elif isinstance(rng, random.Random): | |
rng = rng | |
else: | |
raise TypeError(type(rng)) | |
return rng | |
def get_random_color(pastel_factor=0.0, rng=None): | |
""" | |
Generate a random rgb colour. | |
:param pastel_factor: float between 0 and 1. If pastel_factor>0 paler colours will | |
be generated. | |
:param rng: A random integer seed or random.Random state. | |
If unspecified the global random is used. | |
:return: color: a (r,g,b) tuple. r, g and b are values between 0 and 1. | |
""" | |
rng = _ensure_rng(rng) | |
color = [(rng.random() + pastel_factor) / (1.0 + pastel_factor) for _ in range(3)] | |
return tuple(color) | |
def color_distance(c1, c2): | |
""" | |
Metric to define the visual distinction between two (r,g,b) colours. | |
Inspired by: https://www.compuphase.com/cmetric.htm | |
:param c1: (r,g,b) colour tuples. r,g and b are values between 0 and 1. | |
:param c2: (r,g,b) colour tuples. r,g and b are values between 0 and 1. | |
:return: distance: float representing visual distinction between c1 and c2. | |
Larger values = more distinct. | |
""" | |
r1, g1, b1 = c1 | |
r2, g2, b2 = c2 | |
mean_r = (r1 + r2) / 2 | |
delta_r = (r1 - r2) ** 2 | |
delta_g = (g1 - g2) ** 2 | |
delta_b = (b1 - b2) ** 2 | |
distance = (2 + mean_r) * delta_r + 4 * delta_g + (3 - mean_r) * delta_b | |
return distance | |
def distinct_color( | |
exclude_colors, pastel_factor=0.0, n_attempts=1000, colorblind_type=None, rng=None | |
): | |
""" | |
Generate a colour as distinct as possible from the colours defined in exclude_colors | |
Inspired by: https://gist.github.com/adewes/5884820 | |
:param exclude_colors: a list of (r,g,b) tuples. r,g,b are values between 0 and 1. | |
:param pastel_factor: float between 0 and 1. If pastel_factor>0 paler colours will | |
be generated. | |
:param n_attempts: number of random colours to generate to find most distinct colour | |
:param colorblind_type: Type of colourblindness to simulate, can be: | |
* 'Normal': Normal vision | |
* 'Protanopia': Red-green colorblindness (1% males) | |
* 'Protanomaly': Red-green colorblindness (1% males, 0.01% females) | |
* 'Deuteranopia': Red-green colorblindness (1% males) | |
* 'Deuteranomaly': Red-green colorblindness (most common type: 6% males, | |
0.4% females) | |
* 'Tritanopia': Blue-yellow colourblindness (<1% males and females) | |
* 'Tritanomaly' Blue-yellow colourblindness (0.01% males and females) | |
* 'Achromatopsia': Total colourblindness | |
* 'Achromatomaly': Total colourblindness | |
:param rng: A random integer seed or random.Random state. | |
If unspecified the global random is used. | |
:return: (r,g,b) color tuple of the generated colour with the largest minimum | |
color_distance to the colours in exclude_colors. | |
""" | |
rng = _ensure_rng(rng) | |
if not exclude_colors: | |
return get_random_color(pastel_factor=pastel_factor, rng=rng) | |
if colorblind_type: | |
exclude_colors = [ | |
colorblind.colorblind_filter(color, colorblind_type) | |
for color in exclude_colors | |
] | |
max_distance = None | |
best_color = None | |
# try pre-defined corners, edges, interior points first | |
if pastel_factor == 0: | |
for color in POINTS_OF_INTEREST: | |
if color not in exclude_colors: | |
if colorblind_type: | |
compare_color = colorblind.colorblind_filter(color, colorblind_type) | |
else: | |
compare_color = color | |
distance_to_nearest = min( | |
[color_distance(compare_color, c) for c in exclude_colors] | |
) | |
if (not max_distance) or (distance_to_nearest > max_distance): | |
max_distance = distance_to_nearest | |
best_color = color | |
# try n_attempts randomly generated colours | |
for _ in range(n_attempts): | |
color = get_random_color(pastel_factor=pastel_factor, rng=rng) | |
if not exclude_colors: | |
return color | |
else: | |
if colorblind_type: | |
compare_color = colorblind.colorblind_filter(color, colorblind_type) | |
else: | |
compare_color = color | |
distance_to_nearest = min( | |
[color_distance(compare_color, c) for c in exclude_colors] | |
) | |
if (not max_distance) or (distance_to_nearest > max_distance): | |
max_distance = distance_to_nearest | |
best_color = color | |
return tuple(best_color) | |
def get_text_color(background_color, threshold=0.6): | |
""" | |
Choose whether black or white text will work better on top of background_color. | |
Inspired by: https://stackoverflow.com/a/3943023 | |
:param background_color: The colour the text will be displayed on | |
:param threshold: float between 0 and 1. With threshold close to 1 white text will | |
be chosen more often. | |
:return: (0,0,0) if black text should be used or (1,1,1) if white text should be | |
used. | |
""" | |
r, g, b = background_color[0], background_color[1], background_color[2] | |
if (r * 0.299 + g * 0.587 + b * 0.114) > threshold: | |
return BLACK | |
else: | |
return WHITE | |
def get_colors( | |
n_colors, | |
exclude_colors=None, | |
return_excluded=False, | |
pastel_factor=0.0, | |
n_attempts=1000, | |
colorblind_type=None, | |
rng=None, | |
): | |
""" | |
Generate a list of n visually distinct colours. | |
:param n_colors: How many colours to generate | |
:param exclude_colors: A list of (r,g,b) colours that new colours should be distinct | |
from. If exclude_colours=None then exclude_colours will be set to avoid white | |
and black (exclude_colours=[(0,0,0), (1,1,1)]). (r,g,b) values should be floats | |
between 0 and 1. | |
:param return_excluded: If return_excluded=True then exclude_colors will be included | |
in the returned color list. Otherwise only the newly generated colors are | |
returned (default). | |
:param pastel_factor: float between 0 and 1. If pastel_factor>0 paler colours will | |
be generated. | |
:param n_attempts: number of random colours to generated to find most distinct | |
colour. | |
:param colorblind_type: Generate colours that are distinct with given type of | |
colourblindness. Can be: | |
* 'Normal': Normal vision | |
* 'Protanopia': Red-green colorblindness (1% males) | |
* 'Protanomaly': Red-green colorblindness (1% males, 0.01% females) | |
* 'Deuteranopia': Red-green colorblindness (1% males) | |
* 'Deuteranomaly': Red-green colorblindness (most common type: 6% males, | |
0.4% females) | |
* 'Tritanopia': Blue-yellow colourblindness (<1% males and females) | |
* 'Tritanomaly' Blue-yellow colourblindness (0.01% males and females) | |
* 'Achromatopsia': Total colourblindness | |
* 'Achromatomaly': Total colourblindness | |
:param rng: A random integer seed or random.Random state. | |
If unspecified the global random is used. | |
:return: colors - A list of (r,g,b) colors that are visually distinct to each other | |
and to the colours in exclude_colors. (r,g,b) values are floats between 0 and 1. | |
""" | |
rng = _ensure_rng(rng) | |
if exclude_colors is None: | |
exclude_colors = [WHITE, BLACK] | |
colors = exclude_colors.copy() | |
for i in range(n_colors): | |
colors.append( | |
distinct_color( | |
colors, | |
pastel_factor=pastel_factor, | |
n_attempts=n_attempts, | |
colorblind_type=colorblind_type, | |
rng=rng, | |
) | |
) | |
if return_excluded: | |
return colors | |
else: | |
return colors[len(exclude_colors) :] | |
def invert_colors(colors): | |
""" | |
Generates inverted colours for each colour in the given colour list, using a simple | |
inversion of each colour to the opposite corner on the r,g,b cube. | |
:return: inverted_colors - A list of inverted (r,g,b) (r,g,b) values are floats | |
between 0 and 1. | |
""" | |
inverted_colors = [] | |
for color in colors: | |
r = 0.0 if color[0] > 0.5 else 1.0 | |
g = 0.0 if color[1] > 0.5 else 1.0 | |
b = 0.0 if color[2] > 0.5 else 1.0 | |
inverted_colors.append((r, g, b)) | |
return inverted_colors | |
def color_swatch( | |
colors, | |
edgecolors=None, | |
show_text=False, | |
text_threshold=0.6, | |
ax=None, | |
title=None, | |
one_row=None, | |
fontsize=None, | |
): | |
""" | |
Display the colours defined in a list of colors. | |
:param colors: List of (r,g,b) colour tuples to display. (r,g,b) should be floats | |
between 0 and 1. | |
:param edgecolors: If None displayed colours have no outline. Otherwise a list of | |
(r,g,b) colours to use as outlines for each colour. | |
:param show_text: If True writes the background colour's hex on top of it in black | |
or white, as appropriate. | |
:param text_threshold: float between 0 and 1. With threshold close to 1 white text | |
will be chosen more often. | |
:param ax: Matplotlib axis to plot to. If ax is None plt.show() is run in function | |
call. | |
:param title: Add a title to the colour swatch. | |
:param one_row: If True display colours on one row, if False as a grid. If | |
one_row=None a grid is used when there are more than 8 colours. | |
:param fontsize: Fontsize of text on colour swatch. If None fontsize will attempt to | |
be set to an appropriate size based on the number of colours. | |
:return: | |
""" | |
import matplotlib.colors | |
import matplotlib.patches as patches | |
import matplotlib.pyplot as plt | |
if one_row is None: | |
if len(colors) > 8: | |
one_row = False | |
else: | |
one_row = True | |
if one_row: | |
n_grid = len(colors) | |
else: | |
n_grid = math.ceil(np.sqrt(len(colors))) | |
if fontsize is None: | |
fontsize = 60 / n_grid | |
width = 1 | |
height = 1 | |
x = 0 | |
y = 0 | |
max_x = 0 | |
max_y = 0 | |
if ax is None: | |
show = True | |
fig = plt.figure(figsize=(8, 8)) | |
ax = fig.add_subplot(111, aspect="equal") | |
else: | |
show = False | |
for idx, color in enumerate(colors): | |
if edgecolors is None: | |
ax.add_patch(patches.Rectangle((x, y), width, height, color=color)) | |
else: | |
ax.add_patch( | |
patches.Rectangle( | |
(x, y), | |
width, | |
height, | |
facecolor=color, | |
edgecolor=edgecolors[idx], | |
linewidth=5, | |
) | |
) | |
if show_text: | |
ax.text( | |
x + (width / 2), | |
y + (height / 2), | |
matplotlib.colors.rgb2hex(color), | |
fontsize=fontsize, | |
ha="center", | |
va="center", | |
color=get_text_color(color, threshold=text_threshold), | |
) | |
if (idx + 1) % n_grid == 0: | |
if edgecolors is None: | |
y += height | |
x = 0 | |
else: | |
y += height + (height / 10) | |
x = 0 | |
else: | |
if edgecolors is None: | |
x += width | |
else: | |
x += width + (width / 10) | |
if x > max_x: | |
max_x = x | |
if y > max_y: | |
max_y = y | |
ax.set_ylim([-height / 10, max_y + 1.1 * height]) | |
ax.set_xlim([-width / 10, max_x + 1.1 * width]) | |
ax.invert_yaxis() | |
ax.axis("off") | |
if title is not None: | |
ax.set_title(title) | |
if show: | |
plt.show() | |
def get_hex(color): | |
""" | |
Returns hex of given color | |
:param color: (r,g,b) color tuple. r,g,b are floats between 0 and 1. | |
:return: hex str of color | |
""" | |
import matplotlib.colors | |
return matplotlib.colors.rgb2hex(color) | |
def get_rgb256(color): | |
""" | |
Converts 0.0-1.0 rgb colour into 0-255 integer rgb colour. | |
:param color: (r,g,b) tuple with r,g,b floats between 0.0 and 1.0 | |
:return: (r,g,b) ints between 0 and 255 | |
""" | |
return ( | |
int(round(color[0] * 255)), | |
int(round(color[1] * 255)), | |
int(round(color[2] * 255)), | |
) | |
def get_colormap(list_of_colors, name="distinctipy"): | |
""" | |
Converts a list of colors into a matplotlib colormap. | |
:param list_of_colors: a list of (r,g,b) color tuples. (r,g,b) values should be | |
floats between 0 and 1. | |
:param name: name of the generated colormap | |
:return: cmap: a matplotlib colormap. | |
""" | |
import matplotlib.colors | |
cmap = matplotlib.colors.ListedColormap(list_of_colors, name=name) | |
return cmap |