From 83d490dadcdda50d29248f7e82d28dc4683efaa0 Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Wed, 6 Aug 2025 23:19:21 +0200 Subject: [PATCH 01/11] initial support for `pycon` docs examples --- docs/stylesheets/code_select.css | 3 ++ ...{cookbooks-card.css => cookbooks_card.css} | 0 mkdocs.yml | 6 ++- supervision/draw/utils.py | 42 ++++++++++++++----- 4 files changed, 39 insertions(+), 12 deletions(-) create mode 100644 docs/stylesheets/code_select.css rename docs/stylesheets/{cookbooks-card.css => cookbooks_card.css} (100%) diff --git a/docs/stylesheets/code_select.css b/docs/stylesheets/code_select.css new file mode 100644 index 00000000..dce599a2 --- /dev/null +++ b/docs/stylesheets/code_select.css @@ -0,0 +1,3 @@ +.language-pycon .gp, .language-pycon .go { + user-select: none; +} \ No newline at end of file diff --git a/docs/stylesheets/cookbooks-card.css b/docs/stylesheets/cookbooks_card.css similarity index 100% rename from docs/stylesheets/cookbooks-card.css rename to docs/stylesheets/cookbooks_card.css diff --git a/mkdocs.yml b/mkdocs.yml index 394d5ddd..612f8901 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -26,7 +26,8 @@ extra: extra_css: - stylesheets/extra.css - - stylesheets/cookbooks-card.css + - stylesheets/cookbooks_card.css + - stylesheets/code_select.css nav: - Home: index.md @@ -169,6 +170,7 @@ markdown_extensions: - pymdownx.snippets: check_paths: true - pymdownx.highlight: + use_pygments: true anchor_linenums: true line_spans: __span pygments_lang_class: true @@ -190,4 +192,4 @@ validation: nav: absolute_links: ignore links: - absolute_links: ignore + absolute_links: ignore \ No newline at end of file diff --git a/supervision/draw/utils.py b/supervision/draw/utils.py index ed4a9037..536e493f 100644 --- a/supervision/draw/utils.py +++ b/supervision/draw/utils.py @@ -346,28 +346,50 @@ def draw_image( def calculate_optimal_text_scale(resolution_wh: tuple[int, int]) -> float: """ - Calculate font scale based on the resolution of an image. + Calculate optimal font scale based on image resolution. - Parameters: - resolution_wh (Tuple[int, int]): A tuple representing the width and height - of the image. + Adjusts font scale proportionally to the smallest dimension of the given image + resolution for consistent readability. + + Args: + resolution_wh (tuple[int, int]): (width, height) of the image in pixels Returns: - float: The calculated font scale factor. + float: recommended font scale factor + + Examples: + ```pycon + >>> from supervision import calculate_optimal_text_scale + >>> calculate_optimal_text_scale((1920, 1080)) + 1.08 + >>> calculate_optimal_text_scale((640, 480)) + 0.48 + ``` """ return min(resolution_wh) * 1e-3 def calculate_optimal_line_thickness(resolution_wh: tuple[int, int]) -> int: """ - Calculate line thickness based on the resolution of an image. + Calculate optimal line thickness based on image resolution. - Parameters: - resolution_wh (Tuple[int, int]): A tuple representing the width and height - of the image. + Adjusts the line thickness for readability depending on the smallest dimension + of the provided image resolution. + + Args: + resolution_wh (tuple[int, int]): (width, height) of the image in pixels Returns: - int: The calculated line thickness in pixels. + int: recommended line thickness in pixels + + Examples: + ```pycon + >>> from supervision import calculate_optimal_line_thickness + >>> calculate_optimal_line_thickness((1920, 1080)) + 4 + >>> calculate_optimal_line_thickness((640, 480)) + 2 + ``` """ if min(resolution_wh) < 1080: return 2 From 5c6745291b401983d846596fbed84829ef0c6b94 Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Thu, 7 Aug 2025 07:41:52 +0200 Subject: [PATCH 02/11] after few tests drop support for `pycon` in docs examples --- docs/stylesheets/code_select.css | 3 --- mkdocs.yml | 2 -- supervision/draw/utils.py | 24 ++++++++++++------------ 3 files changed, 12 insertions(+), 17 deletions(-) delete mode 100644 docs/stylesheets/code_select.css diff --git a/docs/stylesheets/code_select.css b/docs/stylesheets/code_select.css deleted file mode 100644 index dce599a2..00000000 --- a/docs/stylesheets/code_select.css +++ /dev/null @@ -1,3 +0,0 @@ -.language-pycon .gp, .language-pycon .go { - user-select: none; -} \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 612f8901..131b7aad 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -27,7 +27,6 @@ extra: extra_css: - stylesheets/extra.css - stylesheets/cookbooks_card.css - - stylesheets/code_select.css nav: - Home: index.md @@ -170,7 +169,6 @@ markdown_extensions: - pymdownx.snippets: check_paths: true - pymdownx.highlight: - use_pygments: true anchor_linenums: true line_spans: __span pygments_lang_class: true diff --git a/supervision/draw/utils.py b/supervision/draw/utils.py index 536e493f..d72ac2c4 100644 --- a/supervision/draw/utils.py +++ b/supervision/draw/utils.py @@ -358,12 +358,12 @@ def calculate_optimal_text_scale(resolution_wh: tuple[int, int]) -> float: float: recommended font scale factor Examples: - ```pycon - >>> from supervision import calculate_optimal_text_scale - >>> calculate_optimal_text_scale((1920, 1080)) - 1.08 - >>> calculate_optimal_text_scale((640, 480)) - 0.48 + ```python + from supervision import calculate_optimal_text_scale + calculate_optimal_text_scale((1920, 1080)) + # 1.08 + calculate_optimal_text_scale((640, 480)) + # 0.48 ``` """ return min(resolution_wh) * 1e-3 @@ -383,12 +383,12 @@ def calculate_optimal_line_thickness(resolution_wh: tuple[int, int]) -> int: int: recommended line thickness in pixels Examples: - ```pycon - >>> from supervision import calculate_optimal_line_thickness - >>> calculate_optimal_line_thickness((1920, 1080)) - 4 - >>> calculate_optimal_line_thickness((640, 480)) - 2 + ```python + from supervision import calculate_optimal_line_thickness + calculate_optimal_line_thickness((1920, 1080)) + # 4 + calculate_optimal_line_thickness((640, 480)) + # 2 ``` """ if min(resolution_wh) < 1080: From 4b7b347aeabf45ea1508e0ecfa6fbf1b9a9eb50e Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Thu, 7 Aug 2025 07:45:55 +0200 Subject: [PATCH 03/11] drop create tiles from public API --- docs/how_to/process_datasets.md | 5 +- supervision/__init__.py | 2 - supervision/utils/image.py | 352 -------------------------------- test/utils/test_image.py | 148 +------------- 4 files changed, 2 insertions(+), 505 deletions(-) diff --git a/docs/how_to/process_datasets.md b/docs/how_to/process_datasets.md index 36c122df..acfd941c 100644 --- a/docs/how_to/process_datasets.md +++ b/docs/how_to/process_datasets.md @@ -331,12 +331,9 @@ for i in range(16): annotated_image = label_annotator.annotate(annotated_image, annotations, labels) annotated_images.append(annotated_image) -grid = sv.create_tiles( +sv.plot_images_grid( annotated_images, grid_size=(4, 4), - single_tile_size=(400, 400), - tile_padding_color=sv.Color.WHITE, - tile_margin_color=sv.Color.WHITE ) ``` diff --git a/supervision/__init__.py b/supervision/__init__.py index ab45651a..0b3df6c0 100644 --- a/supervision/__init__.py +++ b/supervision/__init__.py @@ -120,7 +120,6 @@ from supervision.utils.conversion import cv2_to_pillow, pillow_to_cv2 from supervision.utils.file import list_files_with_extensions from supervision.utils.image import ( ImageSink, - create_tiles, crop_image, letterbox_image, overlay_image, @@ -206,7 +205,6 @@ __all__ = [ "clip_boxes", "contains_holes", "contains_multiple_segments", - "create_tiles", "crop_image", "cv2_to_pillow", "draw_filled_polygon", diff --git a/supervision/utils/image.py b/supervision/utils/image.py index 69609867..5b51b628 100644 --- a/supervision/utils/image.py +++ b/supervision/utils/image.py @@ -435,355 +435,3 @@ class ImageSink: def __exit__(self, exc_type, exc_value, exc_traceback): pass - - -def create_tiles( - images: list[ImageType], - grid_size: tuple[int | None, int | None] | None = None, - single_tile_size: tuple[int, int] | None = None, - tile_scaling: Literal["min", "max", "avg"] = "avg", - tile_padding_color: tuple[int, int, int] | Color = Color.from_hex("#D9D9D9"), - tile_margin: int = 10, - tile_margin_color: tuple[int, int, int] | Color = Color.from_hex("#BFBEBD"), - return_type: Literal["auto", "cv2", "pillow"] = "auto", - titles: list[str | None] | None = None, - titles_anchors: Point | list[Point | None] | None = None, - titles_color: tuple[int, int, int] | Color = Color.from_hex("#262523"), - titles_scale: float | None = None, - titles_thickness: int = 1, - titles_padding: int = 10, - titles_text_font: int = cv2.FONT_HERSHEY_SIMPLEX, - titles_background_color: tuple[int, int, int] | Color = Color.from_hex("#D9D9D9"), - default_title_placement: RelativePosition = "top", -) -> ImageType: - """ - Creates tiles mosaic from input images, automating grid placement and - converting images to common resolution maintaining aspect ratio. It is - also possible to render text titles on tiles, using optional set of - parameters specifying text drawing (see parameters description). - - Automated grid placement will try to maintain square shape of grid - (with size being the nearest integer square root of #images), up to two exceptions: - * if there are up to 3 images - images will be displayed in single row - * if square-grid placement causes last row to be empty - number of rows is trimmed - until last row has at least one image - - Args: - images (List[ImageType]): Images to create tiles. Elements can be either - np.ndarray or PIL.Image, common representation will be agreed by the - function. - grid_size (Optional[Tuple[Optional[int], Optional[int]]]): Expected grid - size in format (n_rows, n_cols). If not given - automated grid placement - will be applied. One may also provide only one out of two elements of the - tuple - then grid will be created with either n_rows or n_cols fixed, - leaving the other dimension to be adjusted by the number of images - single_tile_size (Optional[Tuple[int, int]]): sizeof a single tile element - provided in (width, height) format. If not given - size of tile will be - automatically calculated based on `tile_scaling` parameter. - tile_scaling (Literal["min", "max", "avg"]): If `single_tile_size` is not - given - parameter will be used to calculate tile size - using - min / max / avg size of image provided in `images` list. - tile_padding_color (Union[Tuple[int, int, int], sv.Color]): Color to be used in - images letterbox procedure (while standardising tiles sizes) as a padding. - If tuple provided - should be BGR. - tile_margin (int): size of margin between tiles (in pixels) - tile_margin_color (Union[Tuple[int, int, int], sv.Color]): Color of tile margin. - If tuple provided - should be BGR. - return_type (Literal["auto", "cv2", "pillow"]): Parameter dictates the format of - return image. One may choose specific type ("cv2" or "pillow") to enforce - conversion. "auto" mode takes a majority vote between types of elements in - `images` list - resolving draws in favour of OpenCV format. "auto" can be - safely used when all input images are of the same type. - titles (Optional[List[Optional[str]]]): Optional titles to be added to tiles. - Elements of that list may be empty - then specific tile (in order presented - in `images` parameter) will not be filled with title. It is possible to - provide list of titles shorter than `images` - then remaining titles will - be assumed empty. - titles_anchors (Optional[Union[Point, List[Optional[Point]]]]): Parameter to - specify anchor points for titles. It is possible to specify anchor either - globally or for specific tiles (following order of `images`). - If not given (either globally, or for specific element of the list), - it will be calculated automatically based on `default_title_placement`. - titles_color (Union[Tuple[int, int, int], Color]): Color of titles text. - If tuple provided - should be BGR. - titles_scale (Optional[float]): Scale of titles. If not provided - value will - be calculated using `calculate_optimal_text_scale(...)`. - titles_thickness (int): Thickness of titles text. - titles_padding (int): Size of titles padding. - titles_text_font (int): Font to be used to render titles. Must be integer - constant representing OpenCV font. - (See docs: https://docs.opencv.org/4.x/d6/d6e/group__imgproc__draw.html) - titles_background_color (Union[Tuple[int, int, int], Color]): Color of title - text padding. - default_title_placement (Literal["top", "bottom"]): Parameter specifies title - anchor placement in case if explicit anchor is not provided. - - Returns: - ImageType: Image with all input images located in tails grid. The output type is - determined by `return_type` parameter. - - Raises: - ValueError: In case when input images list is empty, provided `grid_size` is too - small to fit all images, `tile_scaling` mode is invalid. - """ - if len(images) == 0: - raise ValueError("Could not create image tiles from empty list of images.") - if return_type == "auto": - return_type = _negotiate_tiles_format(images=images) - tile_padding_color = unify_to_bgr(color=tile_padding_color) - tile_margin_color = unify_to_bgr(color=tile_margin_color) - images = images_to_cv2(images=images) - if single_tile_size is None: - single_tile_size = _aggregate_images_shape(images=images, mode=tile_scaling) - resized_images = [ - letterbox_image( - image=i, resolution_wh=single_tile_size, color=tile_padding_color - ) - for i in images - ] - grid_size = _establish_grid_size(images=images, grid_size=grid_size) - if len(images) > grid_size[0] * grid_size[1]: - raise ValueError( - f"Could not place {len(images)} in grid with size: {grid_size}." - ) - if titles is not None: - titles = fill(sequence=titles, desired_size=len(images), content=None) - titles_anchors = ( - [titles_anchors] - if not issubclass(type(titles_anchors), list) - else titles_anchors - ) - titles_anchors = fill( - sequence=titles_anchors, desired_size=len(images), content=None - ) - titles_color = unify_to_bgr(color=titles_color) - titles_background_color = unify_to_bgr(color=titles_background_color) - tiles = _generate_tiles( - images=resized_images, - grid_size=grid_size, - single_tile_size=single_tile_size, - tile_padding_color=tile_padding_color, - tile_margin=tile_margin, - tile_margin_color=tile_margin_color, - titles=titles, - titles_anchors=titles_anchors, - titles_color=titles_color, - titles_scale=titles_scale, - titles_thickness=titles_thickness, - titles_padding=titles_padding, - titles_text_font=titles_text_font, - titles_background_color=titles_background_color, - default_title_placement=default_title_placement, - ) - if return_type == "pillow": - tiles = cv2_to_pillow(image=tiles) - return tiles - - -def _negotiate_tiles_format(images: list[ImageType]) -> Literal["cv2", "pillow"]: - number_of_np_arrays = sum(issubclass(type(i), np.ndarray) for i in images) - if number_of_np_arrays >= (len(images) // 2): - return "cv2" - return "pillow" - - -def _calculate_aggregated_images_shape( - images: list[np.ndarray], aggregator: Callable[[list[int]], float] -) -> tuple[int, int]: - height = round(aggregator([i.shape[0] for i in images])) - width = round(aggregator([i.shape[1] for i in images])) - return width, height - - -SHAPE_AGGREGATION_FUN = { - "min": partial(_calculate_aggregated_images_shape, aggregator=np.min), - "max": partial(_calculate_aggregated_images_shape, aggregator=np.max), - "avg": partial(_calculate_aggregated_images_shape, aggregator=np.average), -} - - -def _aggregate_images_shape( - images: list[np.ndarray], mode: Literal["min", "max", "avg"] -) -> tuple[int, int]: - if mode not in SHAPE_AGGREGATION_FUN: - raise ValueError( - f"Could not aggregate images shape - provided unknown mode: {mode}. " - f"Supported modes: {list(SHAPE_AGGREGATION_FUN.keys())}." - ) - return SHAPE_AGGREGATION_FUN[mode](images) - - -def _establish_grid_size( - images: list[np.ndarray], grid_size: tuple[int | None, int | None] | None -) -> tuple[int, int]: - if grid_size is None or all(e is None for e in grid_size): - return _negotiate_grid_size(images=images) - if grid_size[0] is None: - return math.ceil(len(images) / grid_size[1]), grid_size[1] - if grid_size[1] is None: - return grid_size[0], math.ceil(len(images) / grid_size[0]) - return grid_size - - -def _negotiate_grid_size(images: list[np.ndarray]) -> tuple[int, int]: - if len(images) <= MAX_COLUMNS_FOR_SINGLE_ROW_GRID: - return 1, len(images) - nearest_sqrt = math.ceil(np.sqrt(len(images))) - proposed_columns = nearest_sqrt - proposed_rows = nearest_sqrt - while proposed_columns * (proposed_rows - 1) >= len(images): - proposed_rows -= 1 - return proposed_rows, proposed_columns - - -def _generate_tiles( - images: list[np.ndarray], - grid_size: tuple[int, int], - single_tile_size: tuple[int, int], - tile_padding_color: tuple[int, int, int], - tile_margin: int, - tile_margin_color: tuple[int, int, int], - titles: list[str | None] | None, - titles_anchors: list[Point | None], - titles_color: tuple[int, int, int], - titles_scale: float | None, - titles_thickness: int, - titles_padding: int, - titles_text_font: int, - titles_background_color: tuple[int, int, int], - default_title_placement: RelativePosition, -) -> np.ndarray: - images = _draw_texts( - images=images, - titles=titles, - titles_anchors=titles_anchors, - titles_color=titles_color, - titles_scale=titles_scale, - titles_thickness=titles_thickness, - titles_padding=titles_padding, - titles_text_font=titles_text_font, - titles_background_color=titles_background_color, - default_title_placement=default_title_placement, - ) - rows, columns = grid_size - tiles_elements = list(create_batches(sequence=images, batch_size=columns)) - while len(tiles_elements[-1]) < columns: - tiles_elements[-1].append( - _generate_color_image(shape=single_tile_size, color=tile_padding_color) - ) - while len(tiles_elements) < rows: - tiles_elements.append( - [_generate_color_image(shape=single_tile_size, color=tile_padding_color)] - * columns - ) - return _merge_tiles_elements( - tiles_elements=tiles_elements, - grid_size=grid_size, - single_tile_size=single_tile_size, - tile_margin=tile_margin, - tile_margin_color=tile_margin_color, - ) - - -def _draw_texts( - images: list[np.ndarray], - titles: list[str | None] | None, - titles_anchors: list[Point | None], - titles_color: tuple[int, int, int], - titles_scale: float | None, - titles_thickness: int, - titles_padding: int, - titles_text_font: int, - titles_background_color: tuple[int, int, int], - default_title_placement: RelativePosition, -) -> list[np.ndarray]: - if titles is None: - return images - titles_anchors = _prepare_default_titles_anchors( - images=images, - titles_anchors=titles_anchors, - default_title_placement=default_title_placement, - ) - if titles_scale is None: - image_height, image_width = images[0].shape[:2] - titles_scale = calculate_optimal_text_scale( - resolution_wh=(image_width, image_height) - ) - result = [] - for image, text, anchor in zip(images, titles, titles_anchors): - if text is None: - result.append(image) - continue - processed_image = draw_text( - scene=image, - text=text, - text_anchor=anchor, - text_color=Color.from_bgr_tuple(titles_color), - text_scale=titles_scale, - text_thickness=titles_thickness, - text_padding=titles_padding, - text_font=titles_text_font, - background_color=Color.from_bgr_tuple(titles_background_color), - ) - result.append(processed_image) - return result - - -def _prepare_default_titles_anchors( - images: list[np.ndarray], - titles_anchors: list[Point | None], - default_title_placement: RelativePosition, -) -> list[Point]: - result = [] - for image, anchor in zip(images, titles_anchors): - if anchor is not None: - result.append(anchor) - continue - image_height, image_width = image.shape[:2] - if default_title_placement == "top": - default_anchor = Point(x=image_width / 2, y=image_height * 0.1) - else: - default_anchor = Point(x=image_width / 2, y=image_height * 0.9) - result.append(default_anchor) - return result - - -def _merge_tiles_elements( - tiles_elements: list[list[np.ndarray]], - grid_size: tuple[int, int], - single_tile_size: tuple[int, int], - tile_margin: int, - tile_margin_color: tuple[int, int, int], -) -> np.ndarray: - vertical_padding = ( - np.ones((single_tile_size[1], tile_margin, 3)) * tile_margin_color - ) - merged_rows = [ - np.concatenate( - list( - itertools.chain.from_iterable( - zip(row, [vertical_padding] * grid_size[1]) - ) - )[:-1], - axis=1, - ) - for row in tiles_elements - ] - row_width = merged_rows[0].shape[1] - horizontal_padding = ( - np.ones((tile_margin, row_width, 3), dtype=np.uint8) * tile_margin_color - ) - rows_with_paddings = [] - for row in merged_rows: - rows_with_paddings.append(row) - rows_with_paddings.append(horizontal_padding) - return np.concatenate( - rows_with_paddings[:-1], - axis=0, - ).astype(np.uint8) - - -def _generate_color_image( - shape: tuple[int, int], color: tuple[int, int, int] -) -> np.ndarray: - return np.ones((*shape[::-1], 3), dtype=np.uint8) * color diff --git a/test/utils/test_image.py b/test/utils/test_image.py index 39640330..6ae9567b 100644 --- a/test/utils/test_image.py +++ b/test/utils/test_image.py @@ -1,9 +1,7 @@ import numpy as np -import pytest from PIL import Image, ImageChops -from supervision import Color, Point -from supervision.utils.image import create_tiles, letterbox_image, resize_image +from supervision.utils.image import letterbox_image, resize_image def test_resize_image_for_opencv_image() -> None: @@ -96,147 +94,3 @@ def test_letterbox_image_for_pillow_image() -> None: assert difference.getbbox() is None, ( "Expected padding to be added top and bottom with padding added top and bottom" ) - - -def test_create_tiles_with_one_image( - one_image: np.ndarray, single_image_tile: np.ndarray -) -> None: - # when - result = create_tiles(images=[one_image], single_tile_size=(240, 240)) - - # # then - assert np.allclose(result, single_image_tile, atol=5.0) - - -def test_create_tiles_with_one_image_and_enforced_grid( - one_image: np.ndarray, single_image_tile_enforced_grid: np.ndarray -) -> None: - # when - result = create_tiles( - images=[one_image], - grid_size=(None, 3), - single_tile_size=(240, 240), - ) - - # then - assert np.allclose(result, single_image_tile_enforced_grid, atol=5.0) - - -def test_create_tiles_with_two_images( - two_images: list[np.ndarray], two_images_tile: np.ndarray -) -> None: - # when - result = create_tiles(images=two_images, single_tile_size=(240, 240)) - - # then - assert np.allclose(result, two_images_tile, atol=5.0) - - -def test_create_tiles_with_three_images( - three_images: list[np.ndarray], three_images_tile: np.ndarray -) -> None: - # when - result = create_tiles(images=three_images, single_tile_size=(240, 240)) - - # then - assert np.allclose(result, three_images_tile, atol=5.0) - - -def test_create_tiles_with_four_images( - four_images: list[np.ndarray], - four_images_tile: np.ndarray, -) -> None: - # when - result = create_tiles(images=four_images, single_tile_size=(240, 240)) - - # then - assert np.allclose(result, four_images_tile, atol=5.0) - - -def test_create_tiles_with_all_images( - all_images: list[np.ndarray], - all_images_tile: np.ndarray, -) -> None: - # when - result = create_tiles(images=all_images, single_tile_size=(240, 240)) - - # then - assert np.allclose(result, all_images_tile, atol=5.0) - - -def test_create_tiles_with_all_images_and_custom_grid( - all_images: list[np.ndarray], all_images_tile_and_custom_grid: np.ndarray -) -> None: - # when - result = create_tiles( - images=all_images, - grid_size=(3, 3), - single_tile_size=(240, 240), - ) - - # then - assert np.allclose(result, all_images_tile_and_custom_grid, atol=5.0) - - -def test_create_tiles_with_all_images_and_custom_colors( - all_images: list[np.ndarray], all_images_tile_and_custom_colors: np.ndarray -) -> None: - # when - result = create_tiles( - images=all_images, - tile_margin_color=(127, 127, 127), - tile_padding_color=(224, 224, 224), - single_tile_size=(240, 240), - ) - - # then - assert np.allclose(result, all_images_tile_and_custom_colors, atol=5.0) - - -def test_create_tiles_with_all_images_and_titles( - all_images: list[np.ndarray], - all_images_tile_and_custom_colors_and_titles: np.ndarray, -) -> None: - # when - result = create_tiles( - images=all_images, - titles=["Image 1", None, "Image 3", "Image 4"], - single_tile_size=(240, 240), - ) - - # then - assert np.allclose(result, all_images_tile_and_custom_colors_and_titles, atol=5.0) - - -def test_create_tiles_with_all_images_and_titles_with_custom_configs( - all_images: list[np.ndarray], - all_images_tile_and_titles_with_custom_configs: np.ndarray, -) -> None: - # when - result = create_tiles( - images=all_images, - titles=["Image 1", None, "Image 3", "Image 4"], - single_tile_size=(240, 240), - titles_anchors=[ - Point(x=200, y=300), - Point(x=300, y=400), - None, - Point(x=300, y=400), - ], - titles_color=Color.RED, - titles_scale=1.5, - titles_thickness=3, - titles_padding=20, - titles_background_color=Color.BLACK, - default_title_placement="bottom", - ) - - # then - assert np.allclose(result, all_images_tile_and_titles_with_custom_configs, atol=5.0) - - -def test_create_tiles_with_all_images_and_custom_grid_to_small_to_fit_images( - all_images: list[np.ndarray], -) -> None: - with pytest.raises(ValueError): - _ = create_tiles(images=all_images, grid_size=(2, 2)) From 8fc22f0281e855d9a0611609918c17f250d6974a Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Thu, 7 Aug 2025 07:54:50 +0200 Subject: [PATCH 04/11] move `ImageType` to a more suitable location --- supervision/annotators/base.py | 14 +------------- supervision/annotators/core.py | 3 ++- supervision/draw/base.py | 13 +++++++++++++ supervision/key_points/annotators.py | 2 +- supervision/utils/conversion.py | 2 +- supervision/utils/notebook.py | 2 +- 6 files changed, 19 insertions(+), 17 deletions(-) create mode 100644 supervision/draw/base.py diff --git a/supervision/annotators/base.py b/supervision/annotators/base.py index 159ad556..9b4bbcbe 100644 --- a/supervision/annotators/base.py +++ b/supervision/annotators/base.py @@ -1,19 +1,7 @@ from abc import ABC, abstractmethod -from typing import TypeVar - -import numpy as np -from PIL import Image from supervision.detection.core import Detections - -ImageType = TypeVar("ImageType", np.ndarray, Image.Image) -""" -An image of type `np.ndarray` or `PIL.Image.Image`. - -Unlike a `Union`, ensures the type remains consistent. If a function -takes an `ImageType` argument and returns an `ImageType`, when you -pass an `np.ndarray`, you will get an `np.ndarray` back. -""" +from supervision.draw.base import ImageType class BaseAnnotator(ABC): diff --git a/supervision/annotators/core.py b/supervision/annotators/core.py index f951e681..780d1754 100644 --- a/supervision/annotators/core.py +++ b/supervision/annotators/core.py @@ -9,7 +9,8 @@ import numpy.typing as npt from PIL import Image, ImageDraw, ImageFont from scipy.interpolate import splev, splprep -from supervision.annotators.base import BaseAnnotator, ImageType +from supervision.draw.base import ImageType +from supervision.annotators.base import BaseAnnotator from supervision.annotators.utils import ( PENDING_TRACK_ID, ColorLookup, diff --git a/supervision/draw/base.py b/supervision/draw/base.py new file mode 100644 index 00000000..e27c1d3c --- /dev/null +++ b/supervision/draw/base.py @@ -0,0 +1,13 @@ +from typing import TypeVar + +import numpy as np +from PIL import Image + +ImageType = TypeVar("ImageType", np.ndarray, Image.Image) +""" +An image of type `np.ndarray` or `PIL.Image.Image`. + +Unlike a `Union`, ensures the type remains consistent. If a function +takes an `ImageType` argument and returns an `ImageType`, when you +pass an `np.ndarray`, you will get an `np.ndarray` back. +""" diff --git a/supervision/key_points/annotators.py b/supervision/key_points/annotators.py index ab9f04d1..7cc752eb 100644 --- a/supervision/key_points/annotators.py +++ b/supervision/key_points/annotators.py @@ -6,7 +6,7 @@ from logging import warn import cv2 import numpy as np -from supervision.annotators.base import ImageType +from supervision.draw.base import ImageType from supervision.detection.utils.boxes import pad_boxes, spread_out_boxes from supervision.draw.color import Color from supervision.draw.utils import draw_rounded_rectangle diff --git a/supervision/utils/conversion.py b/supervision/utils/conversion.py index 79ec5003..30a9465e 100644 --- a/supervision/utils/conversion.py +++ b/supervision/utils/conversion.py @@ -4,7 +4,7 @@ import cv2 import numpy as np from PIL import Image -from supervision.annotators.base import ImageType +from supervision.draw.base import ImageType def ensure_cv2_image_for_annotation(annotate_func): diff --git a/supervision/utils/notebook.py b/supervision/utils/notebook.py index 9262f12b..3af09ebb 100644 --- a/supervision/utils/notebook.py +++ b/supervision/utils/notebook.py @@ -4,7 +4,7 @@ import cv2 import matplotlib.pyplot as plt from PIL import Image -from supervision.annotators.base import ImageType +from supervision.draw.base import ImageType from supervision.utils.conversion import pillow_to_cv2 From cbb58a8c6fff7ca4ea8f88d9a5e8425397656071 Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Thu, 7 Aug 2025 10:01:50 +0200 Subject: [PATCH 05/11] `tint_image` and `grayscale_image` functions added --- docs/utils/image.md | 12 +++ supervision/__init__.py | 4 + supervision/annotators/core.py | 50 ++++++------ supervision/draw/utils.py | 28 +++---- supervision/key_points/annotators.py | 6 +- supervision/utils/conversion.py | 6 +- supervision/utils/image.py | 110 +++++++++++++++++++++++---- test/utils/test_conversion.py | 6 +- 8 files changed, 160 insertions(+), 62 deletions(-) diff --git a/docs/utils/image.md b/docs/utils/image.md index 8e39136a..b1daa9c2 100644 --- a/docs/utils/image.md +++ b/docs/utils/image.md @@ -34,6 +34,18 @@ comments: true :::supervision.utils.image.overlay_image + + +:::supervision.utils.image.tint_image + + + +:::supervision.utils.image.grayscale_image + diff --git a/supervision/__init__.py b/supervision/__init__.py index 0b3df6c0..9f16f0bb 100644 --- a/supervision/__init__.py +++ b/supervision/__init__.py @@ -125,6 +125,8 @@ from supervision.utils.image import ( overlay_image, resize_image, scale_image, + tint_image, + grayscale_image ) from supervision.utils.notebook import plot_image, plot_images_grid from supervision.utils.video import ( @@ -248,4 +250,6 @@ __all__ = [ "xyxy_to_polygons", "xyxy_to_xcycarh", "xyxy_to_xywh", + "tint_image", + "grayscale_image" ] diff --git a/supervision/annotators/core.py b/supervision/annotators/core.py index 780d1754..55536307 100644 --- a/supervision/annotators/core.py +++ b/supervision/annotators/core.py @@ -34,8 +34,8 @@ from supervision.draw.color import Color, ColorPalette from supervision.draw.utils import draw_polygon, draw_rounded_rectangle, draw_text from supervision.geometry.core import Point, Position, Rect from supervision.utils.conversion import ( - ensure_cv2_image_for_annotation, - ensure_pil_image_for_annotation, + ensure_cv2_image_for_class_method, + ensure_pil_image_for_class_method, ) from supervision.utils.image import ( crop_image, @@ -178,7 +178,7 @@ class BoxAnnotator(BaseAnnotator): self.thickness: int = thickness self.color_lookup: ColorLookup = color_lookup - @ensure_cv2_image_for_annotation + @ensure_cv2_image_for_class_method def annotate( self, scene: ImageType, @@ -261,7 +261,7 @@ class OrientedBoxAnnotator(BaseAnnotator): self.thickness: int = thickness self.color_lookup: ColorLookup = color_lookup - @ensure_cv2_image_for_annotation + @ensure_cv2_image_for_class_method def annotate( self, scene: ImageType, @@ -350,7 +350,7 @@ class MaskAnnotator(BaseAnnotator): self.opacity = opacity self.color_lookup: ColorLookup = color_lookup - @ensure_cv2_image_for_annotation + @ensure_cv2_image_for_class_method def annotate( self, scene: ImageType, @@ -440,7 +440,7 @@ class PolygonAnnotator(BaseAnnotator): self.thickness: int = thickness self.color_lookup: ColorLookup = color_lookup - @ensure_cv2_image_for_annotation + @ensure_cv2_image_for_class_method def annotate( self, scene: ImageType, @@ -527,7 +527,7 @@ class ColorAnnotator(BaseAnnotator): self.color_lookup: ColorLookup = color_lookup self.opacity = opacity - @ensure_cv2_image_for_annotation + @ensure_cv2_image_for_class_method def annotate( self, scene: ImageType, @@ -623,7 +623,7 @@ class HaloAnnotator(BaseAnnotator): self.color_lookup: ColorLookup = color_lookup self.kernel_size: int = kernel_size - @ensure_cv2_image_for_annotation + @ensure_cv2_image_for_class_method def annotate( self, scene: ImageType, @@ -723,7 +723,7 @@ class EllipseAnnotator(BaseAnnotator): self.end_angle: int = end_angle self.color_lookup: ColorLookup = color_lookup - @ensure_cv2_image_for_annotation + @ensure_cv2_image_for_class_method def annotate( self, scene: ImageType, @@ -815,7 +815,7 @@ class BoxCornerAnnotator(BaseAnnotator): self.corner_length: int = corner_length self.color_lookup: ColorLookup = color_lookup - @ensure_cv2_image_for_annotation + @ensure_cv2_image_for_class_method def annotate( self, scene: ImageType, @@ -904,7 +904,7 @@ class CircleAnnotator(BaseAnnotator): self.thickness: int = thickness self.color_lookup: ColorLookup = color_lookup - @ensure_cv2_image_for_annotation + @ensure_cv2_image_for_class_method def annotate( self, scene: ImageType, @@ -1003,7 +1003,7 @@ class DotAnnotator(BaseAnnotator): self.outline_thickness = outline_thickness self.outline_color: Color | ColorPalette = outline_color - @ensure_cv2_image_for_annotation + @ensure_cv2_image_for_class_method def annotate( self, scene: ImageType, @@ -1129,7 +1129,7 @@ class LabelAnnotator(_BaseLabelAnnotator): max_line_length=max_line_length, ) - @ensure_cv2_image_for_annotation + @ensure_cv2_image_for_class_method def annotate( self, scene: ImageType, @@ -1439,7 +1439,7 @@ class RichLabelAnnotator(_BaseLabelAnnotator): max_line_length=max_line_length, ) - @ensure_pil_image_for_annotation + @ensure_pil_image_for_class_method def annotate( self, scene: ImageType, @@ -1666,7 +1666,7 @@ class IconAnnotator(BaseAnnotator): self.position = icon_position self.offset_xy = offset_xy - @ensure_cv2_image_for_annotation + @ensure_cv2_image_for_class_method def annotate( self, scene: ImageType, detections: Detections, icon_path: str | list[str] ) -> ImageType: @@ -1755,7 +1755,7 @@ class BlurAnnotator(BaseAnnotator): """ self.kernel_size: int = kernel_size - @ensure_cv2_image_for_annotation + @ensure_cv2_image_for_class_method def annotate( self, scene: ImageType, @@ -1844,7 +1844,7 @@ class TraceAnnotator(BaseAnnotator): self.smooth = smooth self.color_lookup: ColorLookup = color_lookup - @ensure_cv2_image_for_annotation + @ensure_cv2_image_for_class_method def annotate( self, scene: ImageType, @@ -1966,7 +1966,7 @@ class HeatMapAnnotator(BaseAnnotator): self.low_hue = low_hue self.heat_mask: npt.NDArray[np.float32] | None = None - @ensure_cv2_image_for_annotation + @ensure_cv2_image_for_class_method def annotate(self, scene: ImageType, detections: Detections) -> ImageType: """ Annotates the scene with a heatmap based on the provided detections. @@ -2048,7 +2048,7 @@ class PixelateAnnotator(BaseAnnotator): """ self.pixel_size: int = pixel_size - @ensure_cv2_image_for_annotation + @ensure_cv2_image_for_class_method def annotate( self, scene: ImageType, @@ -2145,7 +2145,7 @@ class TriangleAnnotator(BaseAnnotator): self.outline_thickness: int = outline_thickness self.outline_color: Color | ColorPalette = outline_color - @ensure_cv2_image_for_annotation + @ensure_cv2_image_for_class_method def annotate( self, scene: ImageType, @@ -2257,7 +2257,7 @@ class RoundBoxAnnotator(BaseAnnotator): raise ValueError("roundness attribute must be float between (0, 1.0]") self.roundness: float = roundness - @ensure_cv2_image_for_annotation + @ensure_cv2_image_for_class_method def annotate( self, scene: ImageType, @@ -2397,7 +2397,7 @@ class PercentageBarAnnotator(BaseAnnotator): else int(0.15 * self.height) ) - @ensure_cv2_image_for_annotation + @ensure_cv2_image_for_class_method def annotate( self, scene: ImageType, @@ -2578,7 +2578,7 @@ class CropAnnotator(BaseAnnotator): self.border_thickness: int = border_thickness self.border_color_lookup: ColorLookup = border_color_lookup - @ensure_cv2_image_for_annotation + @ensure_cv2_image_for_class_method def annotate( self, scene: ImageType, @@ -2727,7 +2727,7 @@ class BackgroundOverlayAnnotator(BaseAnnotator): self.opacity = opacity self.force_box = force_box - @ensure_cv2_image_for_annotation + @ensure_cv2_image_for_class_method def annotate(self, scene: ImageType, detections: Detections) -> ImageType: """ Applies a colored overlay to the scene outside of the detected regions. @@ -2825,7 +2825,7 @@ class ComparisonAnnotator: self.label_scale = label_scale self.text_thickness = int(self.label_scale + 1.2) - @ensure_cv2_image_for_annotation + @ensure_cv2_image_for_class_method def annotate( self, scene: ImageType, detections_1: Detections, detections_2: Detections ) -> ImageType: diff --git a/supervision/draw/utils.py b/supervision/draw/utils.py index d72ac2c4..0d9ffe12 100644 --- a/supervision/draw/utils.py +++ b/supervision/draw/utils.py @@ -346,10 +346,9 @@ def draw_image( def calculate_optimal_text_scale(resolution_wh: tuple[int, int]) -> float: """ - Calculate optimal font scale based on image resolution. - - Adjusts font scale proportionally to the smallest dimension of the given image - resolution for consistent readability. + Calculate optimal font scale based on image resolution. Adjusts font scale + proportionally to the smallest dimension of the given image resolution for + consistent readability. Args: resolution_wh (tuple[int, int]): (width, height) of the image in pixels @@ -359,10 +358,11 @@ def calculate_optimal_text_scale(resolution_wh: tuple[int, int]) -> float: Examples: ```python - from supervision import calculate_optimal_text_scale - calculate_optimal_text_scale((1920, 1080)) + import supervision as sv + + sv.calculate_optimal_text_scale((1920, 1080)) # 1.08 - calculate_optimal_text_scale((640, 480)) + sv.calculate_optimal_text_scale((640, 480)) # 0.48 ``` """ @@ -371,10 +371,9 @@ def calculate_optimal_text_scale(resolution_wh: tuple[int, int]) -> float: def calculate_optimal_line_thickness(resolution_wh: tuple[int, int]) -> int: """ - Calculate optimal line thickness based on image resolution. - - Adjusts the line thickness for readability depending on the smallest dimension - of the provided image resolution. + Calculate optimal line thickness based on image resolution. Adjusts the line + thickness for readability depending on the smallest dimension of the provided + image resolution. Args: resolution_wh (tuple[int, int]): (width, height) of the image in pixels @@ -384,10 +383,11 @@ def calculate_optimal_line_thickness(resolution_wh: tuple[int, int]) -> int: Examples: ```python - from supervision import calculate_optimal_line_thickness - calculate_optimal_line_thickness((1920, 1080)) + import supervision as sv + + sv.calculate_optimal_line_thickness((1920, 1080)) # 4 - calculate_optimal_line_thickness((640, 480)) + sv.calculate_optimal_line_thickness((640, 480)) # 2 ``` """ diff --git a/supervision/key_points/annotators.py b/supervision/key_points/annotators.py index 7cc752eb..5b649ab4 100644 --- a/supervision/key_points/annotators.py +++ b/supervision/key_points/annotators.py @@ -13,7 +13,7 @@ from supervision.draw.utils import draw_rounded_rectangle from supervision.geometry.core import Rect from supervision.key_points.core import KeyPoints from supervision.key_points.skeletons import SKELETONS_BY_VERTEX_COUNT -from supervision.utils.conversion import ensure_cv2_image_for_annotation +from supervision.utils.conversion import ensure_cv2_image_for_class_method class BaseKeyPointAnnotator(ABC): @@ -43,7 +43,7 @@ class VertexAnnotator(BaseKeyPointAnnotator): self.color = color self.radius = radius - @ensure_cv2_image_for_annotation + @ensure_cv2_image_for_class_method def annotate(self, scene: ImageType, key_points: KeyPoints) -> ImageType: """ Annotates the given scene with skeleton vertices based on the provided key @@ -120,7 +120,7 @@ class EdgeAnnotator(BaseKeyPointAnnotator): self.thickness = thickness self.edges = edges - @ensure_cv2_image_for_annotation + @ensure_cv2_image_for_class_method def annotate(self, scene: ImageType, key_points: KeyPoints) -> ImageType: """ Annotates the given scene by drawing lines between specified key points to form diff --git a/supervision/utils/conversion.py b/supervision/utils/conversion.py index 30a9465e..b1c8f16b 100644 --- a/supervision/utils/conversion.py +++ b/supervision/utils/conversion.py @@ -7,7 +7,7 @@ from PIL import Image from supervision.draw.base import ImageType -def ensure_cv2_image_for_annotation(annotate_func): +def ensure_cv2_image_for_class_method(annotate_func): """ Decorates `BaseAnnotator.annotate` implementations, converts scene to an image type used internally by the annotators, converts back when annotation @@ -32,7 +32,7 @@ def ensure_cv2_image_for_annotation(annotate_func): return wrapper -def ensure_cv2_image_for_processing(image_processing_fun): +def ensure_cv2_image_for_standalone_function(image_processing_fun): """ Decorates image processing functions that accept np.ndarray, converting `image` to np.ndarray, converts back when processing is complete. @@ -55,7 +55,7 @@ def ensure_cv2_image_for_processing(image_processing_fun): return wrapper -def ensure_pil_image_for_annotation(annotate_func): +def ensure_pil_image_for_class_method(annotate_func): """ Decorates image processing functions that accept np.ndarray, converting `image` to PIL image, converts back when processing is complete. diff --git a/supervision/utils/image.py b/supervision/utils/image.py index 5b51b628..f8de9818 100644 --- a/supervision/utils/image.py +++ b/supervision/utils/image.py @@ -1,11 +1,7 @@ from __future__ import annotations -import itertools -import math import os import shutil -from collections.abc import Callable -from functools import partial from typing import Literal import cv2 @@ -14,21 +10,16 @@ import numpy.typing as npt from supervision.annotators.base import ImageType from supervision.draw.color import Color, unify_to_bgr -from supervision.draw.utils import calculate_optimal_text_scale, draw_text -from supervision.geometry.core import Point from supervision.utils.conversion import ( - cv2_to_pillow, - ensure_cv2_image_for_processing, - images_to_cv2, + ensure_cv2_image_for_standalone_function, ) -from supervision.utils.iterables import create_batches, fill RelativePosition = Literal["top", "bottom"] MAX_COLUMNS_FOR_SINGLE_ROW_GRID = 3 -@ensure_cv2_image_for_processing +@ensure_cv2_image_for_standalone_function def crop_image( image: ImageType, xyxy: npt.NDArray[int] | list[int] | tuple[int, int, int, int], @@ -89,7 +80,7 @@ def crop_image( return image[y_min:y_max, x_min:x_max] -@ensure_cv2_image_for_processing +@ensure_cv2_image_for_standalone_function def scale_image(image: ImageType, scale_factor: float) -> ImageType: """ Scales the given image based on the given scale factor. @@ -146,7 +137,7 @@ def scale_image(image: ImageType, scale_factor: float) -> ImageType: return cv2.resize(image, (width_new, height_new), interpolation=cv2.INTER_LINEAR) -@ensure_cv2_image_for_processing +@ensure_cv2_image_for_standalone_function def resize_image( image: ImageType, resolution_wh: tuple[int, int], @@ -219,7 +210,7 @@ def resize_image( return cv2.resize(image, (width_new, height_new), interpolation=cv2.INTER_LINEAR) -@ensure_cv2_image_for_processing +@ensure_cv2_image_for_standalone_function def letterbox_image( image: ImageType, resolution_wh: tuple[int, int], @@ -371,6 +362,97 @@ def overlay_image( return image +@ensure_cv2_image_for_standalone_function +def tint_image( + scene: ImageType, + color: Color = Color.BLACK, + opacity: float = 0.5, +) -> ImageType: + """ + Blend a solid-color overlay onto an image. Create a tinted effect by blending a + uniform color overlay with the input image at a specified opacity. + + Args: + scene (ImageType): input image to be tinted (`numpy.ndarray` or `PIL.Image.Image`) + color (Color): overlay tint color + opacity (float): blend ratio between overlay and image (0.0–1.0, inclusive) + + Returns: + ImageType: tinted image in the same format as the input + + Raises: + ValueError: if opacity is outside the range [0.0, 1.0] + + Examples: + ```python + import cv2 + import supervision as sv + + image = cv2.imread("source.jpg") + tinted = sv.tint_image(scene=image, color=sv.Color.BLACK, opacity=0.5) + cv2.imwrite("result.jpg", tinted) + ``` + + ```python + from PIL import Image + import supervision as sv + + image = Image.open("source.jpg") + tinted = sv.tint_image(scene=image, color=Color.BLACK, opacity=0.5) + tinted.save("result.jpg") + ``` + """ # noqa: E501 // docs + if not 0.0 <= opacity <= 1.0: + raise ValueError("opacity must be between 0.0 and 1.0") + + overlay = np.full_like(scene, fill_value=color.as_bgr(), dtype=scene.dtype) + cv2.addWeighted( + src1=overlay, + alpha=opacity, + src2=scene, + beta=1 - opacity, + gamma=0, + dst=scene + ) + return scene + + +@ensure_cv2_image_for_standalone_function +def grayscale_image(scene: ImageType) -> ImageType: + """ + Convert an RGB or BGR image to 3-channel grayscale. The luminance channel is + broadcast to all three channels, ensuring compatibility with color-based drawing + helpers that expect 3-channel input. + + Args: + scene (ImageType): input image to be converted (`numpy.ndarray` or `PIL.Image.Image`) + + Returns: + ImageType: 3-channel grayscale version in the same format as input + + Examples: + ```python + import cv2 + import supervision as sv + + image = cv2.imread("source.jpg") + grayscaled = sv.grayscale_image(scene=image) + cv2.imwrite("result.jpg", grayscaled) + ``` + + ```python + from PIL import Image + import supervision as sv + + image = Image.open("source.jpg") + grayscaled = sv.grayscale_image(scene=image) + grayscaled.save("result.jpg") + ``` + """ # noqa: E501 // docs + grayscaled = cv2.cvtColor(scene, cv2.COLOR_BGR2GRAY) + return cv2.cvtColor(grayscaled, cv2.COLOR_GRAY2BGR) + + class ImageSink: def __init__( self, diff --git a/test/utils/test_conversion.py b/test/utils/test_conversion.py index 65cbd8a1..e9fabb0d 100644 --- a/test/utils/test_conversion.py +++ b/test/utils/test_conversion.py @@ -3,7 +3,7 @@ from PIL import Image, ImageChops from supervision.utils.conversion import ( cv2_to_pillow, - ensure_cv2_image_for_processing, + ensure_cv2_image_for_standalone_function, images_to_cv2, pillow_to_cv2, ) @@ -16,7 +16,7 @@ def test_ensure_cv2_image_for_processing_when_pillow_image_submitted( param_a_value = 3 param_b_value = "some" - @ensure_cv2_image_for_processing + @ensure_cv2_image_for_standalone_function def my_custom_processing_function( image: np.ndarray, param_a: int, @@ -55,7 +55,7 @@ def test_ensure_cv2_image_for_processing_when_cv2_image_submitted( param_a_value = 3 param_b_value = "some" - @ensure_cv2_image_for_processing + @ensure_cv2_image_for_standalone_function def my_custom_processing_function( image: np.ndarray, param_a: int, From a933b93234e0bbd6d9571bb6f0e1ff3146c7b6b7 Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Thu, 7 Aug 2025 11:51:59 +0200 Subject: [PATCH 06/11] update image utils docs; deprecate `overlay_image` image --- docs/utils/image.md | 6 - supervision/utils/image.py | 297 +++++++++++++++++++++---------------- 2 files changed, 170 insertions(+), 133 deletions(-) diff --git a/docs/utils/image.md b/docs/utils/image.md index b1daa9c2..a24d19dd 100644 --- a/docs/utils/image.md +++ b/docs/utils/image.md @@ -28,12 +28,6 @@ comments: true :::supervision.utils.image.letterbox_image - - -:::supervision.utils.image.overlay_image - diff --git a/supervision/utils/image.py b/supervision/utils/image.py index f8de9818..96a13f20 100644 --- a/supervision/utils/image.py +++ b/supervision/utils/image.py @@ -2,7 +2,6 @@ from __future__ import annotations import os import shutil -from typing import Literal import cv2 import numpy as np @@ -13,10 +12,7 @@ from supervision.draw.color import Color, unify_to_bgr from supervision.utils.conversion import ( ensure_cv2_image_for_standalone_function, ) - -RelativePosition = Literal["top", "bottom"] - -MAX_COLUMNS_FOR_SINGLE_ROW_GRID = 3 +from supervision.utils.internal import deprecated @ensure_cv2_image_for_standalone_function @@ -25,54 +21,48 @@ def crop_image( xyxy: npt.NDArray[int] | list[int] | tuple[int, int, int, int], ) -> ImageType: """ - Crops the given image based on the given bounding box. + Crop image based on bounding box coordinates. Args: - image (ImageType): The image to be cropped. `ImageType` is a flexible type, - accepting either `numpy.ndarray` or `PIL.Image.Image`. - xyxy (Union[np.ndarray, List[int], Tuple[int, int, int, int]]): A bounding box - coordinates in the format `(x_min, y_min, x_max, y_max)`, accepted as either - a `numpy.ndarray`, a `list`, or a `tuple`. + image (`numpy.ndarray` or `PIL.Image.Image`): The image to crop. + xyxy (`numpy.array`, `list[int]`, or `tuple[int, int, int, int]`): + Bounding box coordinates in `(x_min, y_min, x_max, y_max)` format. Returns: - (ImageType): The cropped image. The type is determined by the input type and - may be either a `numpy.ndarray` or `PIL.Image.Image`. - - === "OpenCV" + (`numpy.ndarray` or `PIL.Image.Image`): Cropped image matching input + type. + Examples: ```python import cv2 import supervision as sv - image = cv2.imread() + image = cv2.imread("source.png") image.shape # (1080, 1920, 3) - xyxy = [200, 400, 600, 800] + xyxy = (200, 400, 600, 800) cropped_image = sv.crop_image(image=image, xyxy=xyxy) cropped_image.shape # (400, 400, 3) ``` - === "Pillow" - ```python from PIL import Image import supervision as sv - image = Image.open() + image = Image.open("source.png") image.size # (1920, 1080) - xyxy = [200, 400, 600, 800] + xyxy = (200, 400, 600, 800) cropped_image = sv.crop_image(image=image, xyxy=xyxy) cropped_image.size # (400, 400) ``` - + ![crop_image](https://media.roboflow.com/supervision-docs/crop-image.png){ align=center width="800" } """ # noqa E501 // docs - if isinstance(xyxy, (list, tuple)): xyxy = np.array(xyxy) xyxy = np.round(xyxy).astype(int) @@ -83,28 +73,25 @@ def crop_image( @ensure_cv2_image_for_standalone_function def scale_image(image: ImageType, scale_factor: float) -> ImageType: """ - Scales the given image based on the given scale factor. + Scale image by given factor. Scale factor > 1.0 zooms in, < 1.0 zooms out. Args: - image (ImageType): The image to be scaled. `ImageType` is a flexible type, - accepting either `numpy.ndarray` or `PIL.Image.Image`. - scale_factor (float): The factor by which the image will be scaled. Scale - factor > `1.0` zooms in, < `1.0` zooms out. + image (`numpy.ndarray` or `PIL.Image.Image`): The image to scale. + scale_factor (`float`): Factor by which to scale the image. Returns: - (ImageType): The scaled image. The type is determined by the input type and - may be either a `numpy.ndarray` or `PIL.Image.Image`. + (`numpy.ndarray` or `PIL.Image.Image`): Scaled image matching input + type. Raises: - ValueError: If the scale factor is non-positive. - - === "OpenCV" + ValueError: If scale factor is non-positive. + Examples: ```python import cv2 import supervision as sv - image = cv2.imread() + image = cv2.imread("source.png") image.shape # (1080, 1920, 3) @@ -113,13 +100,11 @@ def scale_image(image: ImageType, scale_factor: float) -> ImageType: # (540, 960, 3) ``` - === "Pillow" - ```python from PIL import Image import supervision as sv - image = Image.open() + image = Image.open("source.png") image.size # (1920, 1080) @@ -144,28 +129,24 @@ def resize_image( keep_aspect_ratio: bool = False, ) -> ImageType: """ - Resizes the given image to a specified resolution. Can maintain the original aspect - ratio or resize directly to the desired dimensions. + Resize image to specified resolution. Can optionally maintain aspect ratio. Args: - image (ImageType): The image to be resized. `ImageType` is a flexible type, - accepting either `numpy.ndarray` or `PIL.Image.Image`. - resolution_wh (Tuple[int, int]): The target resolution as - `(width, height)`. - keep_aspect_ratio (bool): Flag to maintain the image's original - aspect ratio. Defaults to `False`. + image (`numpy.ndarray` or `PIL.Image.Image`): The image to resize. + resolution_wh (`tuple[int, int]`): Target resolution as `(width, height)`. + keep_aspect_ratio (`bool`): Flag to maintain original aspect ratio. + Defaults to `False`. Returns: - (ImageType): The resized image. The type is determined by the input type and - may be either a `numpy.ndarray` or `PIL.Image.Image`. - - === "OpenCV" + (`numpy.ndarray` or `PIL.Image.Image`): Resized image matching input + type. + Examples: ```python import cv2 import supervision as sv - image = cv2.imread() + image = cv2.imread("source.png") image.shape # (1080, 1920, 3) @@ -176,13 +157,11 @@ def resize_image( # (562, 1000, 3) ``` - === "Pillow" - ```python from PIL import Image import supervision as sv - image = Image.open() + image = Image.open("source.png") image.size # (1920, 1080) @@ -192,7 +171,7 @@ def resize_image( resized_image.size # (1000, 562) ``` - + ![resize_image](https://media.roboflow.com/supervision-docs/resize-image.png){ align=center width="800" } """ # noqa E501 // docs if keep_aspect_ratio: @@ -217,51 +196,50 @@ def letterbox_image( color: tuple[int, int, int] | Color = Color.BLACK, ) -> ImageType: """ - Resizes and pads an image to a specified resolution with a given color, maintaining - the original aspect ratio. + Resize image and pad with color to achieve desired resolution while + maintaining aspect ratio. Args: - image (ImageType): The image to be resized. `ImageType` is a flexible type, - accepting either `numpy.ndarray` or `PIL.Image.Image`. - resolution_wh (Tuple[int, int]): The target resolution as - `(width, height)`. - color (Union[Tuple[int, int, int], Color]): The color to pad with. If tuple - provided it should be in BGR format. + image (`numpy.ndarray` or `PIL.Image.Image`): The image to resize and pad. + resolution_wh (`tuple[int, int]`): Target resolution as `(width, height)`. + color (`tuple[int, int, int]` or `Color`): Padding color. If tuple, should + be in BGR format. Defaults to `Color.BLACK`. Returns: - (ImageType): The resized image. The type is determined by the input type and - may be either a `numpy.ndarray` or `PIL.Image.Image`. - - === "OpenCV" + (`numpy.ndarray` or `PIL.Image.Image`): Letterboxed image matching input + type. + Examples: ```python import cv2 import supervision as sv - image = cv2.imread() + image = cv2.imread("source.png") image.shape # (1080, 1920, 3) - letterboxed_image = sv.letterbox_image(image=image, resolution_wh=(1000, 1000)) + letterboxed_image = sv.letterbox_image( + image=image, resolution_wh=(1000, 1000) + ) letterboxed_image.shape # (1000, 1000, 3) ``` - === "Pillow" - ```python from PIL import Image import supervision as sv - image = Image.open() + image = Image.open("source.png") image.size # (1920, 1080) - letterboxed_image = sv.letterbox_image(image=image, resolution_wh=(1000, 1000)) + letterboxed_image = sv.letterbox_image( + image=image, resolution_wh=(1000, 1000) + ) letterboxed_image.size # (1000, 1000) ``` - + ![letterbox_image](https://media.roboflow.com/supervision-docs/letterbox-image.png){ align=center width="800" } """ # noqa E501 // docs assert isinstance(image, np.ndarray) @@ -293,37 +271,59 @@ def letterbox_image( return image_with_borders +@deprecated( + "`overlay_image` function is deprecated and will be removed in " + "`supervision-0.32.0`. Use `draw_image` instead." +) def overlay_image( image: npt.NDArray[np.uint8], overlay: npt.NDArray[np.uint8], anchor: tuple[int, int], ) -> npt.NDArray[np.uint8]: """ - Places an image onto a scene at a given anchor point, handling cases where - the image's position is partially or completely outside the scene's bounds. + Overlay image onto scene at specified anchor point. Handles cases where + overlay position is partially or completely outside scene bounds. Args: - image (np.ndarray): The background scene onto which the image is placed. - overlay (np.ndarray): The image to be placed onto the scene. - anchor (Tuple[int, int]): The `(x, y)` coordinates in the scene where the - top-left corner of the image will be placed. + image (`numpy.array`): Background scene with shape `(height, width, 3)`. + overlay (`numpy.array`): Image to overlay with shape + `(height, width, 3)` or `(height, width, 4)`. + anchor (`tuple[int, int]`): Coordinates `(x, y)` where top-left corner + of overlay will be placed. Returns: - (np.ndarray): The result image with overlay. + (`numpy.array`): Scene with overlay applied, shape `(height, width, 3)`. Examples: - ```python + ``` import cv2 import numpy as np import supervision as sv - image = cv2.imread() + image = cv2.imread("source.png") overlay = np.zeros((400, 400, 3), dtype=np.uint8) - result_image = sv.overlay_image(image=image, overlay=overlay, anchor=(200, 400)) + overlay[:] = (0, 255, 0) # Green overlay + + result_image = sv.overlay_image( + image=image, overlay=overlay, anchor=(200, 400) + ) + cv2.imwrite("target.png", result_image) ``` - ![overlay_image](https://media.roboflow.com/supervision-docs/overlay-image.png){ align=center width="800" } - """ # noqa E501 // docs + ``` + import cv2 + import numpy as np + import supervision as sv + + image = cv2.imread("source.png") + overlay = cv2.imread("overlay.png", cv2.IMREAD_UNCHANGED) + + result_image = sv.overlay_image( + image=image, overlay=overlay, anchor=(100, 100) + ) + cv2.imwrite("target.png", result_image) + ``` + """ scene_height, scene_width = image.shape[:2] image_height, image_width = overlay.shape[:2] anchor_x, anchor_y = anchor @@ -369,39 +369,44 @@ def tint_image( opacity: float = 0.5, ) -> ImageType: """ - Blend a solid-color overlay onto an image. Create a tinted effect by blending a - uniform color overlay with the input image at a specified opacity. + Tint image with solid color overlay at specified opacity. Args: - scene (ImageType): input image to be tinted (`numpy.ndarray` or `PIL.Image.Image`) - color (Color): overlay tint color - opacity (float): blend ratio between overlay and image (0.0–1.0, inclusive) + scene (`numpy.ndarray` or `PIL.Image.Image`): The image to tint. + color (`Color`): Overlay tint color. Defaults to `Color.BLACK`. + opacity (`float`): Blend ratio between overlay and image (0.0-1.0). + Defaults to `0.5`. Returns: - ImageType: tinted image in the same format as the input + (`numpy.ndarray` or `PIL.Image.Image`): Tinted image matching input + type. Raises: - ValueError: if opacity is outside the range [0.0, 1.0] + ValueError: If opacity is outside range [0.0, 1.0]. Examples: ```python import cv2 import supervision as sv - image = cv2.imread("source.jpg") - tinted = sv.tint_image(scene=image, color=sv.Color.BLACK, opacity=0.5) - cv2.imwrite("result.jpg", tinted) + image = cv2.imread("source.png") + tinted_image = sv.tint_image( + scene=image, color=sv.Color.BLACK, opacity=0.5 + ) + cv2.imwrite("target.png", tinted_image) ``` ```python from PIL import Image import supervision as sv - image = Image.open("source.jpg") - tinted = sv.tint_image(scene=image, color=Color.BLACK, opacity=0.5) - tinted.save("result.jpg") + image = Image.open("source.png") + tinted_image = sv.tint_image( + scene=image, color=sv.Color.BLACK, opacity=0.5 + ) + tinted_image.save("target.png") ``` - """ # noqa: E501 // docs + """ if not 0.0 <= opacity <= 1.0: raise ValueError("opacity must be between 0.0 and 1.0") @@ -420,35 +425,36 @@ def tint_image( @ensure_cv2_image_for_standalone_function def grayscale_image(scene: ImageType) -> ImageType: """ - Convert an RGB or BGR image to 3-channel grayscale. The luminance channel is - broadcast to all three channels, ensuring compatibility with color-based drawing - helpers that expect 3-channel input. + Convert image to 3-channel grayscale. Luminance channel is broadcast to + all three channels for compatibility with color-based drawing helpers. Args: - scene (ImageType): input image to be converted (`numpy.ndarray` or `PIL.Image.Image`) + scene (`numpy.ndarray` or `PIL.Image.Image`): The image to convert to + grayscale. Returns: - ImageType: 3-channel grayscale version in the same format as input + (`numpy.ndarray` or `PIL.Image.Image`): 3-channel grayscale image + matching input type. Examples: ```python import cv2 import supervision as sv - image = cv2.imread("source.jpg") - grayscaled = sv.grayscale_image(scene=image) - cv2.imwrite("result.jpg", grayscaled) + image = cv2.imread("source.png") + grayscale_image = sv.grayscale_image(scene=image) + cv2.imwrite("target.png", grayscale_image) ``` ```python from PIL import Image import supervision as sv - image = Image.open("source.jpg") - grayscaled = sv.grayscale_image(scene=image) - grayscaled.save("result.jpg") + image = Image.open("source.png") + grayscale_image = sv.grayscale_image(scene=image) + grayscale_image.save("target.png") ``` - """ # noqa: E501 // docs + """ grayscaled = cv2.cvtColor(scene, cv2.COLOR_BGR2GRAY) return cv2.cvtColor(grayscaled, cv2.COLOR_GRAY2BGR) @@ -461,27 +467,64 @@ class ImageSink: image_name_pattern: str = "image_{:05d}.png", ): """ - Initialize a context manager for saving images. + Initialize context manager for saving images to directory. Args: - target_dir_path (str): The target directory where images will be saved. - overwrite (bool): Whether to overwrite the existing directory. - Defaults to False. - image_name_pattern (str): The image file name pattern. - Defaults to "image_{:05d}.png". + target_dir_path (`str`): Target directory path where images will be + saved. + overwrite (`bool`): Whether to overwrite existing directory. + Defaults to `False`. + image_name_pattern (`str`): File name pattern for saved images. + Defaults to `"image_{:05d}.png"`. Examples: ```python import supervision as sv - frames_generator = sv.get_video_frames_generator(, stride=2) + frames_generator = sv.get_video_frames_generator( + "source.mp4", stride=2 + ) - with sv.ImageSink(target_dir_path=) as sink: + with sv.ImageSink(target_dir_path="output_frames") as sink: for image in frames_generator: sink.save_image(image=image) - ``` - """ # noqa E501 // docs + # Directory structure: + # output_frames/ + # ├── image_00000.png + # ├── image_00001.png + # ├── image_00002.png + # └── image_00003.png + ``` + + ```python + import cv2 + import supervision as sv + + image = cv2.imread("source.png") + crop_boxes = [ + ( 0, 0, 400, 400), + (400, 0, 800, 400), + ( 0, 400, 400, 800), + (400, 400, 800, 800) + ] + + with sv.ImageSink( + target_dir_path="image_crops", + overwrite=True + ) as sink: + for i, xyxy in enumerate(crop_boxes): + crop = sv.crop_image(image=image, xyxy=xyxy) + sink.save_image(image=crop, image_name=f"crop_{i}.png") + + # Directory structure: + # image_crops/ + # ├── crop_0.png + # ├── crop_1.png + # ├── crop_2.png + # └── crop_3.png + ``` + """ self.target_dir_path = target_dir_path self.overwrite = overwrite self.image_name_pattern = image_name_pattern @@ -499,14 +542,14 @@ class ImageSink: def save_image(self, image: np.ndarray, image_name: str | None = None): """ - Save a given image in the target directory. + Save image to target directory with optional custom filename. Args: - image (np.ndarray): The image to be saved. The image must be in BGR color - format. - image_name (Optional[str]): The name to use for the saved image. - If not provided, a name will be - generated using the `image_name_pattern`. + image (`numpy.array`): Image to save with shape `(height, width, 3)` + in BGR format. + image_name (`str` or `None`): Custom filename for saved image. If + `None`, generates name using `image_name_pattern`. Defaults to + `None`. """ if image_name is None: image_name = self.image_name_pattern.format(self.image_count) From 4ad36db3bb0f0ae3884b339e65ca7bd85fc4ece2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 7 Aug 2025 09:53:16 +0000 Subject: [PATCH 07/11] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20auto?= =?UTF-8?q?=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mkdocs.yml | 2 +- supervision/__init__.py | 6 +++--- supervision/annotators/core.py | 2 +- supervision/key_points/annotators.py | 2 +- supervision/utils/image.py | 13 ++++--------- 5 files changed, 10 insertions(+), 15 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index 131b7aad..daf3d098 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -190,4 +190,4 @@ validation: nav: absolute_links: ignore links: - absolute_links: ignore \ No newline at end of file + absolute_links: ignore diff --git a/supervision/__init__.py b/supervision/__init__.py index 9f16f0bb..04d3fb25 100644 --- a/supervision/__init__.py +++ b/supervision/__init__.py @@ -121,12 +121,12 @@ from supervision.utils.file import list_files_with_extensions from supervision.utils.image import ( ImageSink, crop_image, + grayscale_image, letterbox_image, overlay_image, resize_image, scale_image, tint_image, - grayscale_image ) from supervision.utils.notebook import plot_image, plot_images_grid from supervision.utils.video import ( @@ -222,6 +222,7 @@ __all__ = [ "get_coco_class_index_mapping", "get_polygon_center", "get_video_frames_generator", + "grayscale_image", "letterbox_image", "list_files_with_extensions", "mask_iou_batch", @@ -245,11 +246,10 @@ __all__ = [ "rle_to_mask", "scale_boxes", "scale_image", + "tint_image", "xcycwh_to_xyxy", "xywh_to_xyxy", "xyxy_to_polygons", "xyxy_to_xcycarh", "xyxy_to_xywh", - "tint_image", - "grayscale_image" ] diff --git a/supervision/annotators/core.py b/supervision/annotators/core.py index 55536307..900d823b 100644 --- a/supervision/annotators/core.py +++ b/supervision/annotators/core.py @@ -9,7 +9,6 @@ import numpy.typing as npt from PIL import Image, ImageDraw, ImageFont from scipy.interpolate import splev, splprep -from supervision.draw.base import ImageType from supervision.annotators.base import BaseAnnotator from supervision.annotators.utils import ( PENDING_TRACK_ID, @@ -30,6 +29,7 @@ from supervision.detection.utils.converters import ( polygon_to_mask, xyxy_to_polygons, ) +from supervision.draw.base import ImageType from supervision.draw.color import Color, ColorPalette from supervision.draw.utils import draw_polygon, draw_rounded_rectangle, draw_text from supervision.geometry.core import Point, Position, Rect diff --git a/supervision/key_points/annotators.py b/supervision/key_points/annotators.py index 5b649ab4..c3f9e984 100644 --- a/supervision/key_points/annotators.py +++ b/supervision/key_points/annotators.py @@ -6,8 +6,8 @@ from logging import warn import cv2 import numpy as np -from supervision.draw.base import ImageType from supervision.detection.utils.boxes import pad_boxes, spread_out_boxes +from supervision.draw.base import ImageType from supervision.draw.color import Color from supervision.draw.utils import draw_rounded_rectangle from supervision.geometry.core import Rect diff --git a/supervision/utils/image.py b/supervision/utils/image.py index 96a13f20..70b3d0e4 100644 --- a/supervision/utils/image.py +++ b/supervision/utils/image.py @@ -60,7 +60,7 @@ def crop_image( cropped_image.size # (400, 400) ``` - + ![crop_image](https://media.roboflow.com/supervision-docs/crop-image.png){ align=center width="800" } """ # noqa E501 // docs if isinstance(xyxy, (list, tuple)): @@ -171,7 +171,7 @@ def resize_image( resized_image.size # (1000, 562) ``` - + ![resize_image](https://media.roboflow.com/supervision-docs/resize-image.png){ align=center width="800" } """ # noqa E501 // docs if keep_aspect_ratio: @@ -239,7 +239,7 @@ def letterbox_image( letterboxed_image.size # (1000, 1000) ``` - + ![letterbox_image](https://media.roboflow.com/supervision-docs/letterbox-image.png){ align=center width="800" } """ # noqa E501 // docs assert isinstance(image, np.ndarray) @@ -412,12 +412,7 @@ def tint_image( overlay = np.full_like(scene, fill_value=color.as_bgr(), dtype=scene.dtype) cv2.addWeighted( - src1=overlay, - alpha=opacity, - src2=scene, - beta=1 - opacity, - gamma=0, - dst=scene + src1=overlay, alpha=opacity, src2=scene, beta=1 - opacity, gamma=0, dst=scene ) return scene From 8c49465d4909393af15c4ad96a44e37204521f40 Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Thu, 7 Aug 2025 12:29:05 +0200 Subject: [PATCH 08/11] align new utils with naming conventions --- supervision/utils/image.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/supervision/utils/image.py b/supervision/utils/image.py index 96a13f20..03511153 100644 --- a/supervision/utils/image.py +++ b/supervision/utils/image.py @@ -364,7 +364,7 @@ def overlay_image( @ensure_cv2_image_for_standalone_function def tint_image( - scene: ImageType, + image: ImageType, color: Color = Color.BLACK, opacity: float = 0.5, ) -> ImageType: @@ -372,7 +372,7 @@ def tint_image( Tint image with solid color overlay at specified opacity. Args: - scene (`numpy.ndarray` or `PIL.Image.Image`): The image to tint. + image (`numpy.ndarray` or `PIL.Image.Image`): The image to tint. color (`Color`): Overlay tint color. Defaults to `Color.BLACK`. opacity (`float`): Blend ratio between overlay and image (0.0-1.0). Defaults to `0.5`. @@ -391,7 +391,7 @@ def tint_image( image = cv2.imread("source.png") tinted_image = sv.tint_image( - scene=image, color=sv.Color.BLACK, opacity=0.5 + image=image, color=sv.Color.BLACK, opacity=0.5 ) cv2.imwrite("target.png", tinted_image) ``` @@ -402,7 +402,7 @@ def tint_image( image = Image.open("source.png") tinted_image = sv.tint_image( - scene=image, color=sv.Color.BLACK, opacity=0.5 + image=image, color=sv.Color.BLACK, opacity=0.5 ) tinted_image.save("target.png") ``` @@ -410,26 +410,26 @@ def tint_image( if not 0.0 <= opacity <= 1.0: raise ValueError("opacity must be between 0.0 and 1.0") - overlay = np.full_like(scene, fill_value=color.as_bgr(), dtype=scene.dtype) + overlay = np.full_like(image, fill_value=color.as_bgr(), dtype=image.dtype) cv2.addWeighted( src1=overlay, alpha=opacity, - src2=scene, + src2=image, beta=1 - opacity, gamma=0, - dst=scene + dst=image ) - return scene + return image @ensure_cv2_image_for_standalone_function -def grayscale_image(scene: ImageType) -> ImageType: +def grayscale_image(image: ImageType) -> ImageType: """ Convert image to 3-channel grayscale. Luminance channel is broadcast to all three channels for compatibility with color-based drawing helpers. Args: - scene (`numpy.ndarray` or `PIL.Image.Image`): The image to convert to + image (`numpy.ndarray` or `PIL.Image.Image`): The image to convert to grayscale. Returns: @@ -442,7 +442,7 @@ def grayscale_image(scene: ImageType) -> ImageType: import supervision as sv image = cv2.imread("source.png") - grayscale_image = sv.grayscale_image(scene=image) + grayscale_image = sv.grayscale_image(image=image) cv2.imwrite("target.png", grayscale_image) ``` @@ -455,7 +455,7 @@ def grayscale_image(scene: ImageType) -> ImageType: grayscale_image.save("target.png") ``` """ - grayscaled = cv2.cvtColor(scene, cv2.COLOR_BGR2GRAY) + grayscaled = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) return cv2.cvtColor(grayscaled, cv2.COLOR_GRAY2BGR) From 72621dce32392eb09f3ffddb1403f2f3dc1ce553 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 7 Aug 2025 10:30:16 +0000 Subject: [PATCH 09/11] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20auto?= =?UTF-8?q?=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/utils/image.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/supervision/utils/image.py b/supervision/utils/image.py index 03511153..9050a6ea 100644 --- a/supervision/utils/image.py +++ b/supervision/utils/image.py @@ -60,7 +60,7 @@ def crop_image( cropped_image.size # (400, 400) ``` - + ![crop_image](https://media.roboflow.com/supervision-docs/crop-image.png){ align=center width="800" } """ # noqa E501 // docs if isinstance(xyxy, (list, tuple)): @@ -171,7 +171,7 @@ def resize_image( resized_image.size # (1000, 562) ``` - + ![resize_image](https://media.roboflow.com/supervision-docs/resize-image.png){ align=center width="800" } """ # noqa E501 // docs if keep_aspect_ratio: @@ -239,7 +239,7 @@ def letterbox_image( letterboxed_image.size # (1000, 1000) ``` - + ![letterbox_image](https://media.roboflow.com/supervision-docs/letterbox-image.png){ align=center width="800" } """ # noqa E501 // docs assert isinstance(image, np.ndarray) @@ -412,12 +412,7 @@ def tint_image( overlay = np.full_like(image, fill_value=color.as_bgr(), dtype=image.dtype) cv2.addWeighted( - src1=overlay, - alpha=opacity, - src2=image, - beta=1 - opacity, - gamma=0, - dst=image + src1=overlay, alpha=opacity, src2=image, beta=1 - opacity, gamma=0, dst=image ) return image From c8b966be1aa37709bf8319e91d5a9904abd210e8 Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Thu, 7 Aug 2025 12:35:09 +0200 Subject: [PATCH 10/11] align new utils with naming conventions --- supervision/utils/image.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/supervision/utils/image.py b/supervision/utils/image.py index 03511153..c5c84388 100644 --- a/supervision/utils/image.py +++ b/supervision/utils/image.py @@ -60,9 +60,7 @@ def crop_image( cropped_image.size # (400, 400) ``` - - ![crop_image](https://media.roboflow.com/supervision-docs/crop-image.png){ align=center width="800" } - """ # noqa E501 // docs + """ if isinstance(xyxy, (list, tuple)): xyxy = np.array(xyxy) xyxy = np.round(xyxy).astype(int) @@ -451,7 +449,7 @@ def grayscale_image(image: ImageType) -> ImageType: import supervision as sv image = Image.open("source.png") - grayscale_image = sv.grayscale_image(scene=image) + grayscale_image = sv.grayscale_image(image=image) grayscale_image.save("target.png") ``` """ From dce96d462ef5c601ef4606f103f2686f82aaabbb Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 7 Aug 2025 10:35:57 +0000 Subject: [PATCH 11/11] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20auto?= =?UTF-8?q?=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/utils/image.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/supervision/utils/image.py b/supervision/utils/image.py index c5c84388..23981c90 100644 --- a/supervision/utils/image.py +++ b/supervision/utils/image.py @@ -169,7 +169,7 @@ def resize_image( resized_image.size # (1000, 562) ``` - + ![resize_image](https://media.roboflow.com/supervision-docs/resize-image.png){ align=center width="800" } """ # noqa E501 // docs if keep_aspect_ratio: @@ -237,7 +237,7 @@ def letterbox_image( letterboxed_image.size # (1000, 1000) ``` - + ![letterbox_image](https://media.roboflow.com/supervision-docs/letterbox-image.png){ align=center width="800" } """ # noqa E501 // docs assert isinstance(image, np.ndarray) @@ -410,12 +410,7 @@ def tint_image( overlay = np.full_like(image, fill_value=color.as_bgr(), dtype=image.dtype) cv2.addWeighted( - src1=overlay, - alpha=opacity, - src2=image, - beta=1 - opacity, - gamma=0, - dst=image + src1=overlay, alpha=opacity, src2=image, beta=1 - opacity, gamma=0, dst=image ) return image