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/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/docs/utils/image.md b/docs/utils/image.md index 8e39136a..a24d19dd 100644 --- a/docs/utils/image.md +++ b/docs/utils/image.md @@ -29,10 +29,16 @@ comments: true :::supervision.utils.image.letterbox_image
-

overlay_image

+

tint_image

-:::supervision.utils.image.overlay_image +:::supervision.utils.image.tint_image + +
+

grayscale_image

+
+ +:::supervision.utils.image.grayscale_image

ImageSink

diff --git a/mkdocs.yml b/mkdocs.yml index 394d5ddd..daf3d098 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -26,7 +26,7 @@ extra: extra_css: - stylesheets/extra.css - - stylesheets/cookbooks-card.css + - stylesheets/cookbooks_card.css nav: - Home: index.md diff --git a/supervision/__init__.py b/supervision/__init__.py index ab45651a..04d3fb25 100644 --- a/supervision/__init__.py +++ b/supervision/__init__.py @@ -120,12 +120,13 @@ 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, + grayscale_image, letterbox_image, overlay_image, resize_image, scale_image, + tint_image, ) from supervision.utils.notebook import plot_image, plot_images_grid from supervision.utils.video import ( @@ -206,7 +207,6 @@ __all__ = [ "clip_boxes", "contains_holes", "contains_multiple_segments", - "create_tiles", "crop_image", "cv2_to_pillow", "draw_filled_polygon", @@ -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,6 +246,7 @@ __all__ = [ "rle_to_mask", "scale_boxes", "scale_image", + "tint_image", "xcycwh_to_xyxy", "xywh_to_xyxy", "xyxy_to_polygons", 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..900d823b 100644 --- a/supervision/annotators/core.py +++ b/supervision/annotators/core.py @@ -9,7 +9,7 @@ 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.annotators.base import BaseAnnotator from supervision.annotators.utils import ( PENDING_TRACK_ID, ColorLookup, @@ -29,12 +29,13 @@ 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 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, @@ -177,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, @@ -260,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, @@ -349,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, @@ -439,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, @@ -526,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, @@ -622,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, @@ -722,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, @@ -814,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, @@ -903,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, @@ -1002,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, @@ -1128,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, @@ -1438,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, @@ -1665,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: @@ -1754,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, @@ -1843,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, @@ -1965,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. @@ -2047,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, @@ -2144,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, @@ -2256,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, @@ -2396,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, @@ -2577,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, @@ -2726,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. @@ -2824,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/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/draw/utils.py b/supervision/draw/utils.py index ed4a9037..0d9ffe12 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. Adjusts font scale + proportionally to the smallest dimension of the given image resolution for + consistent readability. - Parameters: - resolution_wh (Tuple[int, int]): A tuple representing the width and height - of the image. + 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: + ```python + import supervision as sv + + sv.calculate_optimal_text_scale((1920, 1080)) + # 1.08 + sv.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. Adjusts the line + thickness for readability depending on the smallest dimension of the provided + image resolution. - Parameters: - resolution_wh (Tuple[int, int]): A tuple representing the width and height - of the image. + 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: + ```python + import supervision as sv + + sv.calculate_optimal_line_thickness((1920, 1080)) + # 4 + sv.calculate_optimal_line_thickness((640, 480)) + # 2 + ``` """ if min(resolution_wh) < 1080: return 2 diff --git a/supervision/key_points/annotators.py b/supervision/key_points/annotators.py index ab9f04d1..c3f9e984 100644 --- a/supervision/key_points/annotators.py +++ b/supervision/key_points/annotators.py @@ -6,14 +6,14 @@ from logging import warn import cv2 import numpy as np -from supervision.annotators.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 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 79ec5003..b1c8f16b 100644 --- a/supervision/utils/conversion.py +++ b/supervision/utils/conversion.py @@ -4,10 +4,10 @@ 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): +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 69609867..23981c90 100644 --- a/supervision/utils/image.py +++ b/supervision/utils/image.py @@ -1,12 +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 import numpy as np @@ -14,74 +9,58 @@ 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 +from supervision.utils.internal import deprecated -@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], ) -> 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) @@ -89,31 +68,28 @@ 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. + 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) @@ -122,13 +98,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) @@ -146,35 +120,31 @@ 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], 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) @@ -185,13 +155,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) @@ -219,54 +187,53 @@ 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], 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) ``` @@ -302,37 +269,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 @@ -371,6 +360,98 @@ def overlay_image( return image +@ensure_cv2_image_for_standalone_function +def tint_image( + image: ImageType, + color: Color = Color.BLACK, + opacity: float = 0.5, +) -> ImageType: + """ + Tint image with solid color overlay at specified opacity. + + Args: + 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`. + + Returns: + (`numpy.ndarray` or `PIL.Image.Image`): Tinted image matching input + type. + + Raises: + ValueError: If opacity is outside range [0.0, 1.0]. + + Examples: + ```python + import cv2 + import supervision as sv + + image = cv2.imread("source.png") + tinted_image = sv.tint_image( + image=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.png") + tinted_image = sv.tint_image( + image=image, color=sv.Color.BLACK, opacity=0.5 + ) + tinted_image.save("target.png") + ``` + """ + if not 0.0 <= opacity <= 1.0: + raise ValueError("opacity must be between 0.0 and 1.0") + + 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 + ) + return image + + +@ensure_cv2_image_for_standalone_function +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: + image (`numpy.ndarray` or `PIL.Image.Image`): The image to convert to + grayscale. + + Returns: + (`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.png") + grayscale_image = sv.grayscale_image(image=image) + cv2.imwrite("target.png", grayscale_image) + ``` + + ```python + from PIL import Image + import supervision as sv + + image = Image.open("source.png") + grayscale_image = sv.grayscale_image(image=image) + grayscale_image.save("target.png") + ``` + """ + grayscaled = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) + return cv2.cvtColor(grayscaled, cv2.COLOR_GRAY2BGR) + + class ImageSink: def __init__( self, @@ -379,27 +460,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 @@ -417,14 +535,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) @@ -435,355 +553,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/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 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, 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))