From 4b7b347aeabf45ea1508e0ecfa6fbf1b9a9eb50e Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Thu, 7 Aug 2025 07:45:55 +0200 Subject: [PATCH] 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))