Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Color class #26

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions distinctipy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

name = "distinctipy"

__version__ = "1.2.2"
__version__ = "1.2.3"

# Expose these module names and their internals in the top-level API
__external__ = ["distinctipy"]
Expand All @@ -27,7 +27,7 @@
"""

# Everything after this point is autogenerate with mkinit
from . import colorblind, colorsets, distinctipy, examples
from . import colorblind, colorsets, distinctipy, examples, color
from .distinctipy import (
BLACK,
BLUE,
Expand Down
167 changes: 167 additions & 0 deletions distinctipy/color.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
from typing import Tuple, List
from enum import IntEnum
from numbers import Number

from .distinctipy import *


class ColorOrder(IntEnum):
RGB = 1 # Default of the library
BGR = 2


class DataType(IntEnum):
FLOAT = 1 # Default of the library, 0..1 float
INT8 = 2 # Used by cv2, 0..255 int
INT16 = 3 # 0..65535 int


class Color:
colors: Tuple[Number, Number, Number]

_color_order: ColorOrder = ColorOrder.RGB
_data_type: DataType = DataType.FLOAT

def __init__(self, colors: Tuple[Number, Number, Number],
color_order: ColorOrder = ColorOrder.RGB,
data_type: DataType = DataType.FLOAT):

self.colors = colors
self._color_order = color_order
self._data_type = data_type

@staticmethod
Erol444 marked this conversation as resolved.
Show resolved Hide resolved
def from_rgb8(rgb8: Tuple[int, int, int]) -> 'Color':
return Color(rgb8, color_order=ColorOrder.RGB, data_type=DataType.INT8)

@staticmethod
def from_bgr8(bgr8: Tuple[int, int, int]) -> 'Color':
return Color(bgr8, color_order=ColorOrder.BGR, data_type=DataType.INT8)

@staticmethod
def from_rgb_float(rgb_float: Tuple[float, float, float]) -> 'Color':
return Color(rgb_float, color_order=ColorOrder.RGB, data_type=DataType.FLOAT)

@staticmethod
def from_bgr_float(bgr_float: Tuple[float, float, float]) -> 'Color':
return Color(bgr_float, color_order=ColorOrder.BGR, data_type=DataType.FLOAT)

@staticmethod
def get_color(pastel_factor: float = 0, rng=None) -> 'Color':
return Color(get_random_color(pastel_factor, rng))

@staticmethod
def get_colors(n_colors,
exclude_colors=None,
return_excluded=False,
pastel_factor=0,
n_attempts=1000,
colorblind_type=None,
rng=None) -> List['Color']:
colors = get_colors(
n_colors,
exclude_colors,
return_excluded,
pastel_factor,
n_attempts,
colorblind_type,
rng
)
return [Color(color) for color in colors]

def _reverse_color_order(self):
"""
BGR -> RGB or RGB -> BGR
"""
self.colors = (self.colors[2], self.colors[1], self.colors[0])

def bgr(self) -> 'Color':
if self._color_order == ColorOrder.BGR:
return self # Already in BGR
self._reverse_color_order()
self._color_order = ColorOrder.BGR
return self

def rgb(self) -> 'Color':
"""
Convert color order to RGB
"""
if self._color_order == ColorOrder.RGB:
return self # Already in RGB
self._reverse_color_order()
self._color_order = ColorOrder.RGB
return self

def float(self) -> 'Color':
"""
Convert color values to float [0.0,1.0]
"""
if self._data_type == DataType.FLOAT:
return self # Already in FLOAT

if self._data_type == DataType.INT8:
self.colors = (
self.colors[0] / 255,
self.colors[1] / 255,
self.colors[2] / 255,
)
elif self._data_type == DataType.INT16:
self.colors = (
self.colors[0] / 65535,
self.colors[1] / 65535,
self.colors[2] / 65535,
)
self._data_type = DataType.FLOAT
return self

def type(self, data_type: DataType) -> 'Color':
if data_type == self._data_type:
return self

self.float() # First convert to FLOAT

if data_type == DataType.INT8:
self.colors = (
round(self.colors[0] * 255),
round(self.colors[1] * 255),
round(self.colors[2] * 255),
)
elif data_type == DataType.INT16:
self.colors = (
round(self.colors[0] * 65535),
round(self.colors[1] * 65535),
round(self.colors[2] * 65535),
)

self._data_type = data_type
return self

def tuple(self) -> Tuple[Number, Number, Number]:
return self.colors

def cv2(self) -> Tuple[int, int, int]:
return self.copy().bgr().type(DataType.INT8).tuple()

def hex(self) -> str:
return get_hex(self.default_tuple())

def text_color(self, threshold=0.6) -> 'Color':
return Color(get_text_color(self.default_tuple(), threshold))

def distance(self, other: 'Color') -> float:
return color_distance(
self.default_tuple(),
other.default_tuple()
)

def invert(self) -> 'Color':
return Color(invert_color(self.default_tuple()))

def copy(self) -> 'Color':
return Color(self.colors, self._color_order, self._data_type)

def default(self) -> 'Color':
return self.copy().rgb().float()

def default_tuple(self) -> Tuple[float, float, float]:
return self.default().tuple()
23 changes: 13 additions & 10 deletions distinctipy/distinctipy.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,18 @@ def get_colors(
else:
return colors[len(exclude_colors) :]

def invert_color(color):
"""
Generates inverted colour a given colour, using a simple
inversion of colour to the opposite corner on the r,g,b cube.

:return: inverted_color - inverted colour (r,g,b) values are floats between 0 and 1.
"""
return (
0.0 if color[0] > 0.5 else 1.0,
0.0 if color[1] > 0.5 else 1.0,
0.0 if color[2] > 0.5 else 1.0
)

def invert_colors(colors):
"""
Expand All @@ -316,16 +328,7 @@ def invert_colors(colors):
: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
inverted_colors = [invert_color(color) for color in colors]


def color_swatch(
Expand Down
71 changes: 71 additions & 0 deletions distinctipy/tests/test_color.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import unittest

from distinctipy.color import Color


def check_equal(tuple1, tuple2) -> bool:
return all(x == y for x, y in zip(tuple1, tuple2))


class TestBlobManager(unittest.TestCase):

def test_color1(self):
color = Color.get_color(rng=123123).tuple()
expected = (0.9409962223692785, 0.9108596801202742, 0.996284123178982)
self.assertTrue(check_equal(color, expected))

def test_color2(self):
color = Color.get_color(rng=54321).tuple()
expected = (0.48866987066258627, 0.9882259597584993, 0.24456828827706578)
self.assertTrue(check_equal(color, expected))

def test_get_colors1(self):
colors = Color.get_colors(3, rng=123123)
expected = [
(0.0, 1.0, 0.0),
(1.0, 0.0, 1.0),
(0.0, 0.5, 1.0)
]
for i, color in enumerate(colors):
self.assertTrue(check_equal(color.tuple(), expected[i]))

def test_get_colors2(self):
colors = Color.get_colors(3, rng=123123, pastel_factor=0.3)
expected = [
(0.3550144248699979, 0.25428377375481004, 0.9985706133743355),
(0.24250062735618416, 0.9839177165862908, 0.2535440877869681),
(0.9288861441826036, 0.3166081385307568, 0.26839081842829504)
]
for i, color in enumerate(colors):
self.assertTrue(check_equal(color.tuple(), expected[i]))

def test_invert(self):
color = Color.get_color(rng=54321).invert().tuple()
expected = (1.0, 0.0, 1.0)
self.assertTrue(check_equal(color, expected))

def test_cv2_1(self):
color = Color.get_color(rng=54321, pastel_factor=0.3).cv2()
expected = (107, 253, 155)
self.assertTrue(check_equal(color, expected))

def test_cv2_2(self):
color = Color.get_color(rng=1234, pastel_factor=0.1).cv2()
expected = (25, 125, 247)
self.assertTrue(check_equal(color, expected))

def test_distance(self):
color1 = Color.get_color(rng=1234, pastel_factor=0.1)
color2 = Color.get_color(rng=4321, pastel_factor=0.3)
dist = color1.distance(color2)
self.assertEqual(0.8831609432987555, dist)

def test_text_color(self):
color = Color.get_color(rng=999, pastel_factor=0.1)
text_color = color.text_color().tuple()
self.assertTrue(check_equal(text_color, (1.0, 1.0, 1.0)))

def test_from_rgb8(self):
color = Color.from_rgb8((255, 0, 127)).default_tuple()
expected = (1.0, 0.0, 0.4980392156862745)
self.assertTrue(check_equal(color, expected))