Spaces:
Running
on
Zero
Running
on
Zero
File size: 14,365 Bytes
a249588 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 |
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 |