Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 120 additions & 13 deletions supervision/draw/color.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,29 +57,33 @@ def _validate_color_hex(color_hex: str):
color_hex = color_hex.lstrip("#")
if not all(c in "0123456789abcdefABCDEF" for c in color_hex):
raise ValueError("Invalid characters in color hash")
if len(color_hex) not in (3, 6):
if len(color_hex) not in (3, 4, 6, 8):
raise ValueError("Invalid length of color hash")


@dataclass
class Color:
"""
Represents a color in RGB format.
Represents a color in RGBA format.

This class provides methods to work with colors, including creating colors from hex
codes, converting colors to hex strings, RGB tuples, and BGR tuples.
codes, converting colors to hex strings, RGB tuples, BGR tuples, and RGBA tuples.

Attributes:
r (int): Red channel value (0-255).
g (int): Green channel value (0-255).
b (int): Blue channel value (0-255).
a (int): Alpha channel value (0-255). Default is 255 (fully opaque).

Example:
```python
import supervision as sv

sv.Color.WHITE
# Color(r=255, g=255, b=255)
# Color(r=255, g=255, b=255, a=255)

sv.Color(r=255, g=0, b=255, a=128)
# Color(r=255, g=0, b=255, a=128)
```

| Constant | Hex Code | RGB |
Expand All @@ -97,6 +101,7 @@ class Color:
r: int
g: int
b: int
a: int = 255

@classmethod
def from_hex(cls, color_hex: str) -> Color:
Expand All @@ -105,9 +110,11 @@ def from_hex(cls, color_hex: str) -> Color:

Args:
color_hex (str): The hex string representing the color. This string can
start with '#' followed by either 3 or 6 hexadecimal characters. In
case of 3 characters, each character is repeated to form the full
6-character hex code.
start with '#' followed by 3, 4, 6, or 8 hexadecimal characters.
- 3 characters: RGB (e.g., '#f0f' -> #ff00ff)
- 4 characters: RGBA (e.g., '#f0f8' -> #ff00ff88)
- 6 characters: RRGGBB (e.g., '#ff00ff')
- 8 characters: RRGGBBAA (e.g., '#ff00ff80')

Returns:
Color: An instance representing the color.
Expand All @@ -117,18 +124,31 @@ def from_hex(cls, color_hex: str) -> Color:
import supervision as sv

sv.Color.from_hex('#ff00ff')
# Color(r=255, g=0, b=255)
# Color(r=255, g=0, b=255, a=255)

sv.Color.from_hex('#f0f')
# Color(r=255, g=0, b=255)
# Color(r=255, g=0, b=255, a=255)

sv.Color.from_hex('#ff00ff80')
# Color(r=255, g=0, b=255, a=128)

sv.Color.from_hex('#f0f8')
# Color(r=255, g=0, b=255, a=136)
```
"""
_validate_color_hex(color_hex)
color_hex = color_hex.lstrip("#")
if len(color_hex) == 3:
color_hex = "".join(c * 2 for c in color_hex)
r, g, b = (int(color_hex[i : i + 2], 16) for i in range(0, 6, 2))
return cls(r, g, b)
elif len(color_hex) == 4:
color_hex = "".join(c * 2 for c in color_hex)

if len(color_hex) == 6:
r, g, b = (int(color_hex[i : i + 2], 16) for i in range(0, 6, 2))
return cls(r, g, b)
else: # len(color_hex) == 8
r, g, b, a = (int(color_hex[i : i + 2], 16) for i in range(0, 8, 2))
return cls(r, g, b, a)

@classmethod
def from_rgb_tuple(cls, color_tuple: tuple[int, int, int]) -> Color:
Expand Down Expand Up @@ -176,21 +196,73 @@ def from_bgr_tuple(cls, color_tuple: tuple[int, int, int]) -> Color:
b, g, r = color_tuple
return cls(r=r, g=g, b=b)

@classmethod
def from_rgba_tuple(cls, color_tuple: tuple[int, int, int, int]) -> Color:
"""
Create a Color instance from an RGBA tuple.

Args:
color_tuple (Tuple[int, int, int, int]): A tuple representing the color
in RGBA format, where each element is an integer in the range 0-255.

Returns:
Color: An instance representing the color.

Example:
```python
import supervision as sv

sv.Color.from_rgba_tuple((255, 255, 0, 128))
# Color(r=255, g=255, b=0, a=128)
```
"""
r, g, b, a = color_tuple
return cls(r=r, g=g, b=b, a=a)

@classmethod
def from_bgra_tuple(cls, color_tuple: tuple[int, int, int, int]) -> Color:
"""
Create a Color instance from a BGRA tuple.

Args:
color_tuple (Tuple[int, int, int, int]): A tuple representing the color
in BGRA format, where each element is an integer in the range 0-255.

Returns:
Color: An instance representing the color.

Example:
```python
import supervision as sv

sv.Color.from_bgra_tuple((0, 255, 255, 128))
# Color(r=255, g=255, b=0, a=128)
```
"""
b, g, r, a = color_tuple
return cls(r=r, g=g, b=b, a=a)

def as_hex(self) -> str:
"""
Converts the Color instance to a hex string.

Returns:
str: The hexadecimal color string.
str: The hexadecimal color string. Returns #RRGGBBAA if alpha is not 255,
otherwise returns #RRGGBB.

Example:
```python
import supervision as sv

sv.Color(r=255, g=255, b=0).as_hex()
# '#ffff00'

sv.Color(r=255, g=0, b=255, a=128).as_hex()
# '#ff00ff80'
```
"""
if self.a != 255:
return f"#{self.r:02x}{self.g:02x}{self.b:02x}{self.a:02x}"
return f"#{self.r:02x}{self.g:02x}{self.b:02x}"

def as_rgb(self) -> tuple[int, int, int]:
Expand Down Expand Up @@ -227,6 +299,40 @@ def as_bgr(self) -> tuple[int, int, int]:
"""
return self.b, self.g, self.r

def as_rgba(self) -> tuple[int, int, int, int]:
"""
Returns the color as an RGBA tuple.

Returns:
Tuple[int, int, int, int]: RGBA tuple.

Example:
```python
import supervision as sv

sv.Color(r=255, g=255, b=0, a=128).as_rgba()
# (255, 255, 0, 128)
```
"""
return self.r, self.g, self.b, self.a

def as_bgra(self) -> tuple[int, int, int, int]:
"""
Returns the color as a BGRA tuple.

Returns:
Tuple[int, int, int, int]: BGRA tuple.

Example:
```python
import supervision as sv

sv.Color(r=255, g=255, b=0, a=128).as_bgra()
# (0, 255, 255, 128)
```
"""
return self.b, self.g, self.r, self.a

@classproperty
def WHITE(cls) -> Color:
return Color.from_hex("#FFFFFF")
Expand Down Expand Up @@ -260,14 +366,15 @@ def ROBOFLOW(cls) -> Color:
return Color.from_hex("#A351FB")

def __hash__(self):
return hash((self.r, self.g, self.b))
return hash((self.r, self.g, self.b, self.a))

def __eq__(self, other):
return (
isinstance(other, Color)
and self.r == other.r
and self.g == other.g
and self.b == other.b
and self.a == other.a
)


Expand Down
93 changes: 92 additions & 1 deletion test/draw/test_color.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,25 @@
("0f0", Color.GREEN, DoesNotRaise()),
("00f", Color.BLUE, DoesNotRaise()),
("#808000", Color(r=128, g=128, b=0), DoesNotRaise()),
# RGBA hex codes (4 digits)
("f0f8", Color(r=255, g=0, b=255, a=136), DoesNotRaise()),
("#f0f8", Color(r=255, g=0, b=255, a=136), DoesNotRaise()),
("ffff", Color(r=255, g=255, b=255, a=255), DoesNotRaise()),
("f008", Color(r=255, g=0, b=0, a=136), DoesNotRaise()),
# RGBA hex codes (8 digits)
("ff00ff80", Color(r=255, g=0, b=255, a=128), DoesNotRaise()),
("#ff00ff80", Color(r=255, g=0, b=255, a=128), DoesNotRaise()),
("ffffff00", Color(r=255, g=255, b=255, a=0), DoesNotRaise()),
("00ff00ff", Color(r=0, g=255, b=0, a=255), DoesNotRaise()),
# Invalid hex codes
("", None, pytest.raises(ValueError)),
("00", None, pytest.raises(ValueError)),
("0000", None, pytest.raises(ValueError)),
("00000", None, pytest.raises(ValueError)),
("0000000", None, pytest.raises(ValueError)),
("000000000", None, pytest.raises(ValueError)),
("ffg", None, pytest.raises(ValueError)),
("fffg", None, pytest.raises(ValueError)),
("ff00ffgg", None, pytest.raises(ValueError)),
],
)
def test_color_from_hex(
Expand All @@ -42,6 +56,11 @@ def test_color_from_hex(
(Color.GREEN, "#00ff00", DoesNotRaise()),
(Color.BLUE, "#0000ff", DoesNotRaise()),
(Color(r=128, g=128, b=0), "#808000", DoesNotRaise()),
# With alpha channel
(Color(r=255, g=0, b=255, a=128), "#ff00ff80", DoesNotRaise()),
(Color(r=255, g=255, b=255, a=255), "#ffffff", DoesNotRaise()),
(Color(r=0, g=255, b=0, a=0), "#00ff0000", DoesNotRaise()),
(Color(r=128, g=128, b=0, a=200), "#808000c8", DoesNotRaise()),
],
)
def test_color_as_hex(
Expand All @@ -50,3 +69,75 @@ def test_color_as_hex(
with exception:
result = color.as_hex()
assert result == expected_result


@pytest.mark.parametrize(
"color_tuple, expected_result, exception",
[
((255, 255, 0, 128), Color(r=255, g=255, b=0, a=128), DoesNotRaise()),
((0, 255, 255, 255), Color(r=0, g=255, b=255, a=255), DoesNotRaise()),
((128, 0, 128, 0), Color(r=128, g=0, b=128, a=0), DoesNotRaise()),
],
)
def test_color_from_rgba_tuple(
color_tuple: tuple[int, int, int, int],
expected_result: Color | None,
exception: Exception,
) -> None:
with exception:
result = Color.from_rgba_tuple(color_tuple=color_tuple)
assert result == expected_result


@pytest.mark.parametrize(
"color_tuple, expected_result, exception",
[
((0, 255, 255, 128), Color(r=255, g=255, b=0, a=128), DoesNotRaise()),
((255, 255, 0, 255), Color(r=0, g=255, b=255, a=255), DoesNotRaise()),
((128, 0, 128, 0), Color(r=128, g=0, b=128, a=0), DoesNotRaise()),
],
)
def test_color_from_bgra_tuple(
color_tuple: tuple[int, int, int, int],
expected_result: Color | None,
exception: Exception,
) -> None:
with exception:
result = Color.from_bgra_tuple(color_tuple=color_tuple)
assert result == expected_result


@pytest.mark.parametrize(
"color, expected_result, exception",
[
(Color(r=255, g=255, b=0, a=128), (255, 255, 0, 128), DoesNotRaise()),
(Color(r=0, g=255, b=255, a=255), (0, 255, 255, 255), DoesNotRaise()),
(Color(r=128, g=0, b=128, a=0), (128, 0, 128, 0), DoesNotRaise()),
],
)
def test_color_as_rgba(
color: Color,
expected_result: tuple[int, int, int, int] | None,
exception: Exception,
) -> None:
with exception:
result = color.as_rgba()
assert result == expected_result


@pytest.mark.parametrize(
"color, expected_result, exception",
[
(Color(r=255, g=255, b=0, a=128), (0, 255, 255, 128), DoesNotRaise()),
(Color(r=0, g=255, b=255, a=255), (255, 255, 0, 255), DoesNotRaise()),
(Color(r=128, g=0, b=128, a=0), (128, 0, 128, 0), DoesNotRaise()),
],
)
def test_color_as_bgra(
color: Color,
expected_result: tuple[int, int, int, int] | None,
exception: Exception,
) -> None:
with exception:
result = color.as_bgra()
assert result == expected_result
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.