stouputils.image.auto_crop module#

auto_crop(
image: Image.Image,
mask: NDArray[np.bool_] | None = None,
threshold: int | float | Callable[[NDArray[T]], int | float] | None = None,
return_type: Literal['same'] = 'same',
contiguous: bool = True,
padding: int | tuple[int, ...] = 0,
*,
return_offsets: Literal[False] = False,
) Image.Image[source]#
auto_crop(
image: NDArray[T],
mask: NDArray[np.bool_] | None = None,
threshold: int | float | Callable[[NDArray[T]], int | float] | None = None,
return_type: Literal['same'] = 'same',
contiguous: bool = True,
padding: int | tuple[int, ...] = 0,
*,
return_offsets: Literal[False] = False,
) NDArray[T]
auto_crop(
image: Image.Image | NDArray[T],
mask: NDArray[np.bool_] | None = None,
threshold: int | float | Callable[[NDArray[T]], int | float] | None = None,
*,
return_type: type[Image.Image],
contiguous: bool = True,
padding: int | tuple[int, ...] = 0,
return_offsets: Literal[False] = False,
) Image.Image
auto_crop(
image: Image.Image | NDArray[T],
mask: NDArray[np.bool_] | None = None,
threshold: int | float | Callable[[NDArray[T]], int | float] | None = None,
*,
return_type: type[np.ndarray],
contiguous: bool = True,
padding: int | tuple[int, ...] = 0,
return_offsets: Literal[False] = False,
) NDArray[T]
auto_crop(
image: Image.Image,
mask: NDArray[np.bool_] | None = None,
threshold: int | float | Callable[[NDArray[T]], int | float] | None = None,
return_type: Literal['same'] = 'same',
contiguous: bool = True,
padding: int | tuple[int, ...] = 0,
*,
return_offsets: Literal[True],
) tuple[Image.Image, tuple[list[int], list[int]]]
auto_crop(
image: NDArray[T],
mask: NDArray[np.bool_] | None = None,
threshold: int | float | Callable[[NDArray[T]], int | float] | None = None,
return_type: Literal['same'] = 'same',
contiguous: bool = True,
padding: int | tuple[int, ...] = 0,
*,
return_offsets: Literal[True],
) tuple[NDArray[T], tuple[list[int], list[int]]]
auto_crop(
image: Image.Image | NDArray[T],
mask: NDArray[np.bool_] | None = None,
threshold: int | float | Callable[[NDArray[T]], int | float] | None = None,
*,
return_type: type[Image.Image],
contiguous: bool = True,
padding: int | tuple[int, ...] = 0,
return_offsets: Literal[True],
) tuple[Image.Image, tuple[list[int], list[int]]]
auto_crop(
image: Image.Image | NDArray[T],
mask: NDArray[np.bool_] | None = None,
threshold: int | float | Callable[[NDArray[T]], int | float] | None = None,
*,
return_type: type[np.ndarray],
contiguous: bool = True,
padding: int | tuple[int, ...] = 0,
return_offsets: Literal[True],
) tuple[NDArray[T], tuple[list[int], list[int]]]

Automatically crop an image to remove zero or uniform regions.

This function crops the image to keep only the region where pixels are non-zero (or above a threshold). It can work with a mask or directly analyze the image.

Parameters:
  • image (Image.Image | NDArray) – The image to crop.

  • mask (NDArray[bool] | None) – Optional binary mask indicating regions to keep.

  • threshold (int | float | Callable) – Threshold value or function (default: np.min).

  • return_type (type | str) – Type of the return value (Image.Image, NDArray[np.number], or “same” to match input type).

  • contiguous (bool) – If True (default), crop to bounding box. If False, remove entire rows/columns with no content.

  • padding (int | tuple[int, ...]) – Extra pixels/slices to keep around detected content. Use one int for all axes or one value per axis.

  • return_offsets (bool) – If True, return a tuple of (cropped_image, (lower_offsets, upper_offsets)) where lower_offsets and upper_offsets are lists of ints (one per axis) describing how many pixels were removed from each side. For non-contiguous crops, offsets reflect the first and last retained index on each axis.

Returns:

The cropped image when return_offsets=False (default). tuple[Image.Image | NDArray[np.number], tuple[list[int], list[int]]]:

A (cropped_image, (lower_offsets, upper_offsets)) tuple when return_offsets=True. lower_offsets[i] is the number of leading elements removed on axis i. upper_offsets[i] is the number of trailing elements removed on axis i.

Return type:

Image.Image | NDArray[np.number]

Examples

>>> # Test with numpy array with zeros on edges
>>> import numpy as np
>>> array = np.zeros((100, 100, 3), dtype=np.uint8)
>>> array[20:80, 30:70] = 255  # White rectangle in center
>>> cropped = auto_crop(array, return_type=np.ndarray)
>>> cropped.shape
(60, 40, 3)
>>> # Test with custom mask
>>> mask = np.zeros((100, 100), dtype=bool)
>>> mask[10:90, 10:90] = True
>>> cropped_with_mask = auto_crop(array, mask=mask, return_type=np.ndarray)
>>> cropped_with_mask.shape
(80, 80, 3)
>>> # Test with PIL Image
>>> from PIL import Image
>>> pil_image = Image.new('RGB', (100, 100), (0, 0, 0))
>>> from PIL import ImageDraw
>>> draw = ImageDraw.Draw(pil_image)
>>> draw.rectangle([25, 25, 75, 75], fill=(255, 255, 255))
>>> cropped_pil = auto_crop(pil_image)
>>> cropped_pil.size
(51, 51)
>>> # Test with threshold
>>> array_gray = np.ones((100, 100), dtype=np.uint8) * 10
>>> array_gray[20:80, 30:70] = 255
>>> cropped_threshold = auto_crop(array_gray, threshold=50, return_type=np.ndarray)
>>> cropped_threshold.shape
(60, 40)
>>> # Test with callable threshold (using lambda to avoid min value)
>>> array_gray2 = np.ones((100, 100), dtype=np.uint8) * 10
>>> array_gray2[20:80, 30:70] = 255
>>> cropped_max = auto_crop(array_gray2, threshold=lambda x: 50, return_type=np.ndarray)
>>> cropped_max.shape
(60, 40)
>>> # Test with non-contiguous crop
>>> array_sparse = np.zeros((100, 100, 3), dtype=np.uint8)
>>> array_sparse[10, 10] = 255
>>> array_sparse[50, 50] = 255
>>> array_sparse[90, 90] = 255
>>> cropped_contiguous = auto_crop(array_sparse, contiguous=True, return_type=np.ndarray)
>>> cropped_contiguous.shape  # Bounding box from (10,10) to (90,90)
(81, 81, 3)
>>> cropped_non_contiguous = auto_crop(array_sparse, contiguous=False, return_type=np.ndarray)
>>> cropped_non_contiguous.shape  # Only rows/cols 10, 50, 90
(3, 3, 3)
>>> # Test with 3D crop on depth dimension
>>> array_3d = np.zeros((50, 50, 10), dtype=np.uint8)
>>> array_3d[10:40, 10:40, 2:8] = 255  # Content only in depth slices 2-7
>>> cropped_3d = auto_crop(array_3d, contiguous=True, return_type=np.ndarray)
>>> cropped_3d.shape  # Should crop all 3 dimensions
(30, 30, 6)
>>> # Test with padding around detected content
>>> array_padded = np.zeros((20, 20), dtype=np.uint8)
>>> array_padded[8:12, 8:12] = 255
>>> cropped_padded = auto_crop(array_padded, padding=2, return_type=np.ndarray)
>>> cropped_padded.shape
(8, 8)
>>> # Test return_offsets
>>> array_off = np.zeros((20, 20), dtype=np.uint8)
>>> array_off[5:15, 4:16] = 255
>>> cropped_off, (lo, hi) = auto_crop(array_off, return_type=np.ndarray, return_offsets=True)
>>> lo  # pixels removed from the top and left
[5, 4]
>>> hi  # pixels removed from the bottom and right
[5, 4]
>>> # Test return_offsets with padding
>>> array_off_pad = np.zeros((20, 20), dtype=np.uint8)
>>> array_off_pad[5:15, 5:15] = 255
>>> cropped_off_pad, (lo_pad, hi_pad) = auto_crop(array_off_pad, padding=2, return_offsets=True)
>>> lo_pad  # pixels removed from the top and left (3 instead of 5 due to padding)
[3, 3]
>>> hi_pad  # pixels removed from the bottom and right (3 instead of 5 due to padding)
[3, 3]
>>> # Test return_offsets with non-contiguous crop
>>> array_off_nc = np.zeros((20, 20), dtype=np.uint8)
>>> array_off_nc[5, 5] = 255
>>> array_off_nc[10, 10] = 255
>>> cropped_off_nc, (lo_nc, hi_nc) = auto_crop(array_off_nc, contiguous=False, return_type=np.ndarray, return_offsets=True)
>>> lo_nc  # first retained index on each axis (5 for both)
[5, 5]
>>> hi_nc  # pixels removed from the end (20 - 1 - last retained index)
[9, 9]