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
27 changes: 27 additions & 0 deletions supervision/annotators/color_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from typing import Tuple


def hex_to_rgba(hex_color: str, opacity: float = 1.0) -> tuple[int, int, int, int]:
"""Convert a HEX color string to RGBA tuple."""
hex_color = hex_color.lstrip("#")
if len(hex_color) not in (6, 8):
raise ValueError("Invalid HEX color format")
r, g, b = int(hex_color[0:2], 16), int(hex_color[2:4], 16), int(hex_color[4:6], 16)
a = int(opacity * 255) if len(hex_color) == 6 else int(hex_color[6:8], 16)
return (r, g, b, a)


def rgba_to_hex(rgba: tuple[int, int, int, int]) -> str:
"""Convert an RGBA tuple to HEX color string."""
r, g, b, a = rgba
return f"#{r:02X}{g:02X}{b:02X}{a:02X}"


def validate_color(value: str) -> bool:
"""Check if a given string is a valid HEX color."""
if not value.startswith("#"):
return False
hex_digits = value.lstrip("#")
return len(hex_digits) in (6, 8) and all(
c in "0123456789ABCDEFabcdef" for c in hex_digits
)
24 changes: 24 additions & 0 deletions supervision/annotators/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,28 @@
scale_image,
)


def hex_to_rgba(hex_color: str):
"""
Convert a hexadecimal color string (#RRGGBB or #RRGGBBAA) to an RGBA tuple.

Args:
hex_color (str): The hex string (e.g. '#FF00FF' or '#FF00FF80').

Returns:
Tuple[int, int, int, int]: Corresponding (R, G, B, A) tuple.
"""
hex_color = hex_color.lstrip("#")
if len(hex_color) == 6:
r, g, b = tuple(int(hex_color[i : i + 2], 16) for i in (0, 2, 4))
a = 255
elif len(hex_color) == 8:
r, g, b, a = tuple(int(hex_color[i : i + 2], 16) for i in (0, 2, 4, 6))
else:
raise ValueError(f"Invalid hex color format: {hex_color}")
return (r, g, b, a)


CV2_FONT = cv2.FONT_HERSHEY_SIMPLEX


Expand Down Expand Up @@ -101,6 +123,8 @@ def __init__(
max_line_length (Optional[int], optional): Maximum number of characters per
line before wrapping the text. None means no wrapping.
"""
if isinstance(color, str) and color.startswith("#"):
color = hex_to_rgba(color)
self.color: Color | ColorPalette = color
self.color_lookup: ColorLookup = color_lookup
self.text_color: Color | ColorPalette = text_color
Expand Down
4 changes: 4 additions & 0 deletions supervision/annotators/test_hex_color.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from supervision.annotators.core import hex_to_rgba

print(hex_to_rgba("#FF00FF")) # (255, 0, 255, 255)
print(hex_to_rgba("#FF00FF80")) # (255, 0, 255, 128)
46 changes: 46 additions & 0 deletions supervision/annotators/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -354,3 +354,49 @@ def put(self, detections: Detections) -> None:

def get(self, tracker_id: int) -> np.ndarray:
return self.xy[self.tracker_id == tracker_id]


def hex_to_rgba(hex_color: str) -> tuple[int, int, int, int]:
"""
Converts a hex color string (e.g. "#FF00FF" or "#FF00FF80") to an RGBA tuple.

Args:
hex_color (str): A hex color string.

Returns:
tuple[int, int, int, int]: RGBA values in range 0–255.

Raises:
ValueError: If the format is invalid.
"""
hex_color = hex_color.strip().lstrip("#")
if len(hex_color) == 6:
hex_color += "FF" # default full opacity
if len(hex_color) != 8:
raise ValueError(f"Invalid hex color format: {hex_color}")
try:
r = int(hex_color[0:2], 16)
g = int(hex_color[2:4], 16)
b = int(hex_color[4:6], 16)
a = int(hex_color[6:8], 16)
except ValueError:
raise ValueError(f"Invalid hex digits in {hex_color}")
return (r, g, b, a)


def rgba_to_hex(rgba: tuple[int, int, int, int]) -> str:
"""
Converts an RGBA tuple (0–255 each) to a hex color string.
"""
if len(rgba) != 4 or not all(0 <= c <= 255 for c in rgba):
raise ValueError("RGBA must be a 4-tuple with values between 0–255.")
return "#{:02X}{:02X}{:02X}{:02X}".format(*rgba)


def is_valid_hex(hex_color: str) -> bool:
"""
Checks if a given string is a valid hex color.
"""
import re

return bool(re.fullmatch(r"#?[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?", hex_color.strip()))
32 changes: 32 additions & 0 deletions test/annotators/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,3 +172,35 @@ def test_wrap_text(
with exception:
result = wrap_text(text=text, max_line_length=max_line_length)
assert result == expected_result


def test_hex_to_rgba_valid():
from supervision.annotators.utils import hex_to_rgba

assert hex_to_rgba("#FF00FF") == (255, 0, 255, 255)
assert hex_to_rgba("#FF00FF80") == (255, 0, 255, 128)
assert hex_to_rgba("00FF0080") == (0, 255, 0, 128)


def test_hex_to_rgba_invalid():
import pytest

from supervision.annotators.utils import hex_to_rgba

with pytest.raises(ValueError):
hex_to_rgba("#FF00F") # wrong length

with pytest.raises(ValueError):
hex_to_rgba("#GGHHII") # invalid chars


def test_rgba_to_hex_and_is_valid_hex():
from supervision.annotators.utils import is_valid_hex, rgba_to_hex

assert rgba_to_hex((255, 0, 255, 255)) == "#FF00FFFF"
assert rgba_to_hex((0, 255, 0, 128)) == "#00FF0080"

assert is_valid_hex("#FF00FF")
assert is_valid_hex("00FF0080")
assert not is_valid_hex("#XYZ123")
assert not is_valid_hex("FF00F")