reimplement `crop_image` to skip type casting when Pillow Image given

`get_image_resolution_wh` added
This commit is contained in:
SkalskiP 2025-11-14 17:55:07 +01:00
parent 8d464aa1b5
commit c8f5c741fd
4 changed files with 143 additions and 3 deletions

View File

@ -41,6 +41,12 @@ status: new
:::supervision.utils.image.grayscale_image
<div class="md-typeset">
<h2><a href="#supervision.utils.image.get_image_resolution_wh">get_image_resolution_wh</a></h2>
</div>
:::supervision.utils.image.get_image_resolution_wh
<div class="md-typeset">
<h2><a href="#supervision.utils.image.ImageSink">ImageSink</a></h2>
</div>

View File

@ -129,6 +129,7 @@ from supervision.utils.image import (
resize_image,
scale_image,
tint_image,
get_image_resolution_wh,
)
from supervision.utils.notebook import plot_image, plot_images_grid
from supervision.utils.video import (
@ -163,6 +164,7 @@ __all__ = [
"DetectionsSmoother",
"DotAnnotator",
"EdgeAnnotator",
"get_image_resolution_wh",
"EllipseAnnotator",
"FPSMonitor",
"HaloAnnotator",

View File

@ -6,6 +6,7 @@ import shutil
import cv2
import numpy as np
import numpy.typing as npt
from PIL import Image
from supervision.annotators.base import ImageType
from supervision.draw.color import Color, unify_to_bgr
@ -15,7 +16,6 @@ from supervision.utils.conversion import (
from supervision.utils.internal import deprecated
@ensure_cv2_image_for_standalone_function
def crop_image(
image: ImageType,
xyxy: npt.NDArray[int] | list[int] | tuple[int, int, int, int],
@ -65,9 +65,20 @@ def crop_image(
""" # noqa E501 // docs
if isinstance(xyxy, (list, tuple)):
xyxy = np.array(xyxy)
xyxy = np.round(xyxy).astype(int)
x_min, y_min, x_max, y_max = xyxy.flatten()
return image[y_min:y_max, x_min:x_max]
if isinstance(image, np.ndarray):
return image[y_min:y_max, x_min:x_max]
if isinstance(image, Image.Image):
return image.crop((x_min, y_min, x_max, y_max))
raise TypeError(
"`image` must be a numpy.ndarray or PIL.Image.Image. "
f"Received {type(image)}"
)
@ensure_cv2_image_for_standalone_function
@ -460,6 +471,61 @@ def grayscale_image(image: ImageType) -> ImageType:
return cv2.cvtColor(grayscaled, cv2.COLOR_GRAY2BGR)
def get_image_resolution_wh(image: ImageType) -> tuple[int, int]:
"""
Get image width and height as a tuple `(width, height)` for various image formats.
Supports both `numpy.ndarray` images (with shape `(H, W, ...)`) and
`PIL.Image.Image` inputs.
Args:
image (`numpy.ndarray` or `PIL.Image.Image`): Input image.
Returns:
(`tuple[int, int]`): Image resolution as `(width, height)`.
Raises:
ValueError: If a `numpy.ndarray` image has fewer than 2 dimensions.
TypeError: If `image` is not a supported type (`numpy.ndarray` or
`PIL.Image.Image`).
Examples:
```python
import cv2
import supervision as sv
image = cv2.imread("example.png")
sv.get_image_resolution_wh(image)
# (1920, 1080)
```
```python
from PIL import Image
import supervision as sv
image = Image.open("example.png")
sv.get_image_resolution_wh(image)
# (1920, 1080)
```
"""
if isinstance(image, np.ndarray):
if image.ndim < 2:
raise ValueError(
"NumPy image must have at least 2 dimensions (H, W, ...). "
f"Received shape: {image.shape}"
)
height, width = image.shape[:2]
return int(width), int(height)
if isinstance(image, Image.Image):
width, height = image.size
return int(width), int(height)
raise TypeError(
"`image` must be a numpy.ndarray or PIL.Image.Image. "
f"Received type: {type(image)}"
)
class ImageSink:
def __init__(
self,

View File

@ -1,7 +1,9 @@
import numpy as np
import pytest
from PIL import Image, ImageChops
from supervision.utils.image import letterbox_image, resize_image
from supervision.utils.image import letterbox_image, resize_image, crop_image, \
get_image_resolution_wh
def test_resize_image_for_opencv_image() -> None:
@ -94,3 +96,67 @@ 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"
)
@pytest.mark.parametrize(
"image, xyxy, expected_size",
[
# NumPy RGB
(
np.zeros((4, 6, 3), dtype=np.uint8),
(2, 1, 5, 3),
(3, 2), # width = 5-2, height = 3-1
),
# NumPy grayscale
(
np.zeros((5, 5), dtype=np.uint8),
(1, 1, 4, 4),
(3, 3),
),
# Pillow RGB
(
Image.new("RGB", (6, 4), color=0),
(2, 1, 5, 3),
(3, 2),
),
# Pillow grayscale
(
Image.new("L", (5, 5), color=0),
(1, 1, 4, 4),
(3, 3),
),
],
)
def test_crop_image(image, xyxy, expected_size):
cropped = crop_image(image=image, xyxy=xyxy)
if isinstance(image, np.ndarray):
assert isinstance(cropped, np.ndarray)
assert cropped.shape[1] == expected_size[0] # width
assert cropped.shape[0] == expected_size[1] # height
else:
assert isinstance(cropped, Image.Image)
assert cropped.size == expected_size
@pytest.mark.parametrize(
"image, expected",
[
# NumPy RGB
(np.zeros((4, 6, 3), dtype=np.uint8), (6, 4)),
# NumPy grayscale
(np.zeros((10, 20), dtype=np.uint8), (20, 10)),
# Pillow RGB
(Image.new("RGB", (6, 4), color=0), (6, 4)),
# Pillow grayscale
(Image.new("L", (20, 10), color=0), (20, 10)),
],
)
def test_get_image_resolution_wh(image, expected):
resolution = get_image_resolution_wh(image)
assert resolution == expected