Merge pull request #1943 from roboflow/docs/refactor_0.27.0

docs/refactor 0.27.0
This commit is contained in:
Piotr Skalski 2025-08-07 13:46:36 +02:00 committed by GitHub
commit c7de70ed20
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 333 additions and 684 deletions

View File

@ -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
)
```

View File

@ -29,10 +29,16 @@ comments: true
:::supervision.utils.image.letterbox_image
<div class="md-typeset">
<h2><a href="#supervision.utils.image.overlay_image">overlay_image</a></h2>
<h2><a href="#supervision.utils.image.tint_image">tint_image</a></h2>
</div>
:::supervision.utils.image.overlay_image
:::supervision.utils.image.tint_image
<div class="md-typeset">
<h2><a href="#supervision.utils.image.grayscale_image">grayscale_image</a></h2>
</div>
:::supervision.utils.image.grayscale_image
<div class="md-typeset">
<h2><a href="#supervision.utils.image.ImageSink">ImageSink</a></h2>

View File

@ -26,7 +26,7 @@ extra:
extra_css:
- stylesheets/extra.css
- stylesheets/cookbooks-card.css
- stylesheets/cookbooks_card.css
nav:
- Home: index.md

View File

@ -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",

View File

@ -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):

View File

@ -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:

13
supervision/draw/base.py Normal file
View File

@ -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.
"""

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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(<SOURCE_IMAGE_PATH>)
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(<SOURCE_IMAGE_PATH>)
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(<SOURCE_IMAGE_PATH>)
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(<SOURCE_IMAGE_PATH>)
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(<SOURCE_IMAGE_PATH>)
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(<SOURCE_IMAGE_PATH>)
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(<SOURCE_IMAGE_PATH>)
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(<SOURCE_IMAGE_PATH>)
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(<SOURCE_IMAGE_PATH>)
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(<SOURCE_VIDEO_PATH>, stride=2)
frames_generator = sv.get_video_frames_generator(
"source.mp4", stride=2
)
with sv.ImageSink(target_dir_path=<TARGET_CROPS_DIRECTORY>) 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

View File

@ -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

View File

@ -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,

View File

@ -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))