# coding: utf-8
from __future__ import annotations
import colorsys
import copy
import gzip
import json
import os
import random
import re
from typing import List
def _validate_color(color):
"""
Checks input color for compliance with the required format
:param: color: color (RGB tuple of integers)
"""
if not isinstance(color, (list, tuple)):
raise ValueError("Color has to be list, or tuple")
if len(color) != 3:
raise ValueError("Color have to contain exactly 3 values: [R, G, B]")
for channel in color:
validate_channel_value(channel)
[docs]def random_rgb(fix_satlight=True) -> List[int, int, int]:
"""
Generate RGB color with fixed saturation and lightness.
:return: RGB integer values
:rtype: :class:`List[int, int, int]`
:Usage example:
.. code-block:: python
import supervisely as sly
color = sly.color.random_rgb()
print(color)
# Output: [138, 15, 123]
"""
saturation = 0.3
lightness = 0.8
if fix_satlight is False:
saturation = random.uniform(0.2, 0.8)
lightness = random.uniform(0.4, 1.0)
hsl_color = (random.random(), saturation, lightness)
rgb_color = colorsys.hls_to_rgb(*hsl_color)
return [round(c * 255) for c in rgb_color]
def _normalize_color(color):
"""
Divide all RGB values by 255.
:param color: color (RGB tuple of integers)
"""
return [c / 255.0 for c in color]
def _color_distance(first_color: list, second_color: list) -> float:
"""
Calculate distance in HLS color space between Hue components of 2 colors
:param first_color: first color (RGB tuple of integers)
:param second_color: second color (RGB tuple of integers)
:return: Euclidean distance between 'first_color' and 'second_color'
"""
first_color_hls = colorsys.rgb_to_hls(*_normalize_color(first_color))
second_color_hls = colorsys.rgb_to_hls(*_normalize_color(second_color))
hue_distance = min(
abs(first_color_hls[0] - second_color_hls[0]),
1 - abs(first_color_hls[0] - second_color_hls[0]),
)
return hue_distance
[docs]def generate_rgb(exist_colors: List[List[int, int, int]]) -> List[int, int, int]:
"""
Generate new color which oppositely by exist colors.
:param exist_colors: List of existing colors in RGB format.
:type exist_colors: list
:return: RGB integer values
:rtype: :class:`List[int, int, int]`
:Usage example:
.. code-block:: python
import supervisely as sly
exist_colors = [[0, 0, 0], [128, 64, 255]]
color = sly.color.generate_rgb(exist_colors)
print(color)
# Output: [15, 138, 39]
"""
largest_min_distance = 0
best_color = random_rgb()
if len(exist_colors) > 0:
for _ in range(100):
color = random_rgb()
current_min_distance = min(_color_distance(color, c) for c in exist_colors)
if current_min_distance > largest_min_distance:
largest_min_distance = current_min_distance
best_color = color
_validate_color(best_color)
return best_color
[docs]def rgb2hex(color: List[int, int, int]) -> str:
"""
Convert integer color format to HEX string.
:param color: List of existing colors in RGB format.
:type color: List[int, int, int]
:return: HEX RGB string
:rtype: :class:`str`
:Usage example:
.. code-block:: python
import supervisely as sly
hex_color = sly.color.rgb2hex([128, 64, 255])
print(hex_color)
# Output: #8040FF
"""
_validate_color(color)
return "#" + "".join("{:02X}".format(component) for component in color)
def _hex2color(hex_value: str) -> list:
"""
Convert HEX RGB string to integer RGB format
:param hex_value: HEX RGBA string. Example: "#FF02A4
:return: RGB integer values. Example: [80, 255, 0]
"""
assert hex_value.startswith("#")
return [int(hex_value[i : (i + 2)], 16) for i in range(1, len(hex_value), 2)]
[docs]def hex2rgb(hex_value: str) -> List[int, int, int]:
"""
Convert HEX RGB string to integer RGB format.
:param hex_value: HEX RGB string.
:type hex_value: str
:return: RGB integer values
:rtype: :class:`List[int, int, int]`
:Usage example:
.. code-block:: python
import supervisely as sly
hex_color = '#8040FF'
color = sly.color.hex2rgb(hex_color)
print(color)
# Output: [128, 64, 255]
"""
if not _validate_hex_color(hex_value):
raise ValueError("Supported only HEX RGB string format!")
color = _hex2color(hex_value)
_validate_color(color)
return color
def _hex2rgba(hex_value: str) -> list:
"""
Convert HEX RGBA string to integer RGBA format
:param hex_value: HEX RGBA string. Example: "#FF02A4CC
:return: RGBA integer values. Example: [80, 255, 0, 128]
"""
assert len(hex_value) == 9, "Supported only HEX RGBA string format!"
return _hex2color(hex_value)
[docs]def validate_channel_value(value: int) -> None:
"""
Generates ValueError if value not between 0 and 255.
:param value: Input channel value.
:type value: int
:raises: :class:`ValueError` if value not between 0 and 255.
:return: None
:rtype: :class:`NoneType`
"""
if 0 <= value <= 255:
pass
else:
raise ValueError("Color channel has to be in range [0; 255]")
# generate colors
# see tests/generate_distinct_colors.py
# import distinctipy
# data = {}
# for n in range(100):
# print(n)
# colors = distinctipy.get_colors(n)
# rgb_colors = [distinctipy.get_rgb256(color) for color in colors]
# data[n] = rgb_colors
# sly.json.dump_json_file(data, "colors.json")
# import gzip
# data = sly.json.load_json_file("colors.json")
# with gzip.open("colors.json.gz", "wt", encoding="UTF-8") as zipfile:
# json.dump(data, zipfile)
# with gzip.open("colors.json.gz", "r") as fin:
# data = json.loads(fin.read().decode("utf-8"))
def get_predefined_colors(n: int):
try:
file = os.path.join(os.path.dirname(os.path.abspath(__file__)), "colors.json.gz")
with gzip.open(file, "r") as fin:
data = json.loads(fin.read().decode("utf-8"))
if str(n) in data:
colors = copy.deepcopy(data[str(n)])
random.Random(7).shuffle(colors)
return colors
# generate random colors
except Exception as e:
print(repr(e))
rand_colors = []
for i in range(n):
rand_colors.append(random_rgb(fix_satlight=False))
return rand_colors
def _validate_hex_color(hex_value: str) -> bool:
"""
Checks if the HEX value is valid for the color
:param hex_value: HEX color value
:type hex_value: str
:return: If the value matches the pattern - True, otherwise - False
:rtype: bool
"""
pattern = r"^#([A-Fa-f0-9]{6})$"
return re.match(pattern, hex_value) is not None