Skip to content

frame

sleap_io.transform.frame

Frame transformation functions using PIL.

This module provides frame-level transformation operations for cropping, scaling, rotating, and padding video frames.

Functions:

Name Description
crop_frame

Crop a frame to the specified region.

flip_h_frame

Flip a frame horizontally (mirror left-right).

flip_v_frame

Flip a frame vertically (mirror top-bottom).

pad_frame

Add padding around a frame.

rotate_frame

Rotate a frame by the given angle.

scale_frame

Scale a frame by the given factors.

transform_frame

Apply a sequence of transformations to a frame.

Attributes:

Name Type Description
QUALITY_TO_RESAMPLE

dict() -> new empty dictionary

__cached__

str(object='') -> str

__doc__

str(object='') -> str

__file__

str(object='') -> str

__name__

str(object='') -> str

__package__

str(object='') -> str

QUALITY_TO_RESAMPLE = {'nearest': <Resampling.NEAREST: 0>, 'bilinear': <Resampling.BILINEAR: 2>, 'bicubic': <Resampling.BICUBIC: 3>} module-attribute

dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)

__cached__ = '/home/runner/work/sleap-io/sleap-io/sleap_io/transform/__pycache__/frame.cpython-313.pyc' module-attribute

str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.

__doc__ = 'Frame transformation functions using PIL.\n\nThis module provides frame-level transformation operations for cropping,\nscaling, rotating, and padding video frames.\n' module-attribute

str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.

__file__ = '/home/runner/work/sleap-io/sleap-io/sleap_io/transform/frame.py' module-attribute

str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.

__name__ = 'sleap_io.transform.frame' module-attribute

str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.

__package__ = 'sleap_io.transform' module-attribute

str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.

crop_frame(frame, crop, fill=0)

Crop a frame to the specified region.

If the crop region extends beyond the frame bounds, the out-of-bounds area is filled with the fill value.

Parameters:

Name Type Description Default
frame ndarray

Input frame as numpy array with shape (H, W) or (H, W, C).

required
crop tuple[int, int, int, int]

Crop region as (x1, y1, x2, y2) pixel coordinates.

required
fill tuple[int, ...] | int

Fill value for out-of-bounds regions.

0

Returns:

Type Description
ndarray

Cropped frame as numpy array.

Source code in sleap_io/transform/frame.py
def crop_frame(
    frame: np.ndarray,
    crop: tuple[int, int, int, int],
    fill: tuple[int, ...] | int = 0,
) -> np.ndarray:
    """Crop a frame to the specified region.

    If the crop region extends beyond the frame bounds, the out-of-bounds area
    is filled with the fill value.

    Args:
        frame: Input frame as numpy array with shape (H, W) or (H, W, C).
        crop: Crop region as (x1, y1, x2, y2) pixel coordinates.
        fill: Fill value for out-of-bounds regions.

    Returns:
        Cropped frame as numpy array.
    """
    x1, y1, x2, y2 = crop
    h, w = frame.shape[:2]
    crop_w, crop_h = x2 - x1, y2 - y1

    # Compute valid source region. Clamp the upper bounds to the lower bounds so a
    # crop that lies wholly beyond the frame on an axis yields an empty (not
    # negative-extent) source slice, which pastes cleanly into an all-fill output
    # instead of raising a broadcast error.
    src_x1 = max(0, x1)
    src_y1 = max(0, y1)
    src_x2 = max(src_x1, min(w, x2))
    src_y2 = max(src_y1, min(h, y2))

    # Extract source region
    cropped = frame[src_y1:src_y2, src_x1:src_x2]

    # Check if padding is needed
    if x1 < 0 or y1 < 0 or x2 > w or y2 > h:
        # Create output array with fill value
        if frame.ndim == 3:
            output_shape = (crop_h, crop_w, frame.shape[2])
        else:
            output_shape = (crop_h, crop_w)

        output = np.full(output_shape, fill, dtype=frame.dtype)

        # Compute paste region
        paste_x1 = src_x1 - x1
        paste_y1 = src_y1 - y1
        paste_x2 = paste_x1 + (src_x2 - src_x1)
        paste_y2 = paste_y1 + (src_y2 - src_y1)

        output[paste_y1:paste_y2, paste_x1:paste_x2] = cropped
        return output

    return cropped

flip_h_frame(frame)

Flip a frame horizontally (mirror left-right).

Parameters:

Name Type Description Default
frame ndarray

Input frame as numpy array with shape (H, W) or (H, W, C).

required

Returns:

Type Description
ndarray

Horizontally flipped frame as numpy array with same dimensions.

Source code in sleap_io/transform/frame.py
def flip_h_frame(frame: np.ndarray) -> np.ndarray:
    """Flip a frame horizontally (mirror left-right).

    Args:
        frame: Input frame as numpy array with shape (H, W) or (H, W, C).

    Returns:
        Horizontally flipped frame as numpy array with same dimensions.
    """
    return np.fliplr(frame).copy()

flip_v_frame(frame)

Flip a frame vertically (mirror top-bottom).

Parameters:

Name Type Description Default
frame ndarray

Input frame as numpy array with shape (H, W) or (H, W, C).

required

Returns:

Type Description
ndarray

Vertically flipped frame as numpy array with same dimensions.

Source code in sleap_io/transform/frame.py
def flip_v_frame(frame: np.ndarray) -> np.ndarray:
    """Flip a frame vertically (mirror top-bottom).

    Args:
        frame: Input frame as numpy array with shape (H, W) or (H, W, C).

    Returns:
        Vertically flipped frame as numpy array with same dimensions.
    """
    return np.flipud(frame).copy()

pad_frame(frame, padding, fill=0)

Add padding around a frame.

Parameters:

Name Type Description Default
frame ndarray

Input frame as numpy array with shape (H, W) or (H, W, C).

required
padding tuple[int, int, int, int]

Padding as (top, right, bottom, left) in pixels.

required
fill tuple[int, ...] | int

Fill value for padded regions.

0

Returns:

Type Description
ndarray

Padded frame as numpy array.

Source code in sleap_io/transform/frame.py
def pad_frame(
    frame: np.ndarray,
    padding: tuple[int, int, int, int],
    fill: tuple[int, ...] | int = 0,
) -> np.ndarray:
    """Add padding around a frame.

    Args:
        frame: Input frame as numpy array with shape (H, W) or (H, W, C).
        padding: Padding as (top, right, bottom, left) in pixels.
        fill: Fill value for padded regions.

    Returns:
        Padded frame as numpy array.
    """
    top, right, bottom, left = padding

    if top == 0 and right == 0 and bottom == 0 and left == 0:
        return frame

    h, w = frame.shape[:2]
    new_h = h + top + bottom
    new_w = w + left + right

    if frame.ndim == 3:
        output_shape = (new_h, new_w, frame.shape[2])
    else:
        output_shape = (new_h, new_w)

    output = np.full(output_shape, fill, dtype=frame.dtype)
    output[top : top + h, left : left + w] = frame

    return output

rotate_frame(frame, angle, quality='bilinear', fill=0, expand=True)

Rotate a frame by the given angle.

The frame is rotated about its center. Positive angles rotate clockwise.

Parameters:

Name Type Description Default
frame ndarray

Input frame as numpy array with shape (H, W) or (H, W, C).

required
angle float

Rotation angle in degrees. Positive is clockwise.

required
quality str

Interpolation quality. One of "nearest", "bilinear", "bicubic".

'bilinear'
fill tuple[int, ...] | int

Fill value for areas outside the rotated image.

0
expand bool

If True (default), expand canvas to fit entire rotated image. If False, keep original dimensions (clips corners).

True

Returns:

Type Description
ndarray

Rotated frame as numpy array. If expand=True, dimensions may change. If expand=False, dimensions match input.

Source code in sleap_io/transform/frame.py
def rotate_frame(
    frame: np.ndarray,
    angle: float,
    quality: str = "bilinear",
    fill: tuple[int, ...] | int = 0,
    expand: bool = True,
) -> np.ndarray:
    """Rotate a frame by the given angle.

    The frame is rotated about its center. Positive angles rotate clockwise.

    Args:
        frame: Input frame as numpy array with shape (H, W) or (H, W, C).
        angle: Rotation angle in degrees. Positive is clockwise.
        quality: Interpolation quality. One of "nearest", "bilinear", "bicubic".
        fill: Fill value for areas outside the rotated image.
        expand: If True (default), expand canvas to fit entire rotated image.
            If False, keep original dimensions (clips corners).

    Returns:
        Rotated frame as numpy array. If expand=True, dimensions may change.
        If expand=False, dimensions match input.
    """
    if angle == 0:
        return frame

    resample = QUALITY_TO_RESAMPLE.get(quality, Image.Resampling.BILINEAR)

    # Convert fill to tuple if needed
    if isinstance(fill, int):
        if frame.ndim == 3:
            fill_color = (fill,) * frame.shape[2]
        else:
            fill_color = fill
    else:
        fill_color = fill

    # Handle grayscale images with shape (H, W, 1)
    pil_frame, was_squeezed = _to_pil_compatible(frame)
    pil_img = Image.fromarray(pil_frame)
    # PIL rotates counter-clockwise, so negate angle for clockwise
    pil_img = pil_img.rotate(
        -angle, resample=resample, expand=expand, fillcolor=fill_color
    )
    result = np.array(pil_img)
    return _from_pil_result(result, was_squeezed)

scale_frame(frame, scale, quality='bilinear')

Scale a frame by the given factors.

Parameters:

Name Type Description Default
frame ndarray

Input frame as numpy array with shape (H, W) or (H, W, C).

required
scale tuple[float, float]

Scale factors as (scale_x, scale_y).

required
quality str

Interpolation quality. One of "nearest", "bilinear", "bicubic".

'bilinear'

Returns:

Type Description
ndarray

Scaled frame as numpy array.

Source code in sleap_io/transform/frame.py
def scale_frame(
    frame: np.ndarray,
    scale: tuple[float, float],
    quality: str = "bilinear",
) -> np.ndarray:
    """Scale a frame by the given factors.

    Args:
        frame: Input frame as numpy array with shape (H, W) or (H, W, C).
        scale: Scale factors as (scale_x, scale_y).
        quality: Interpolation quality. One of "nearest", "bilinear", "bicubic".

    Returns:
        Scaled frame as numpy array.
    """
    scale_x, scale_y = scale

    if scale_x == 1.0 and scale_y == 1.0:
        return frame

    h, w = frame.shape[:2]
    new_w = int(round(w * scale_x))
    new_h = int(round(h * scale_y))

    if new_w <= 0 or new_h <= 0:
        raise ValueError(f"Invalid output dimensions: {new_w}x{new_h}")

    resample = QUALITY_TO_RESAMPLE.get(quality, Image.Resampling.BILINEAR)

    # Handle grayscale images with shape (H, W, 1)
    pil_frame, was_squeezed = _to_pil_compatible(frame)
    pil_img = Image.fromarray(pil_frame)
    pil_img = pil_img.resize((new_w, new_h), resample)
    result = np.array(pil_img)
    return _from_pil_result(result, was_squeezed)

transform_frame(frame, crop=None, scale=None, rotate=None, pad=None, quality='bilinear', fill=0, expand_rotation=True, flip_h=False, flip_v=False)

Apply a sequence of transformations to a frame.

Transforms are applied in order: crop -> scale -> rotate -> pad -> flip.

Parameters:

Name Type Description Default
frame ndarray

Input frame as numpy array with shape (H, W) or (H, W, C).

required
crop tuple[int, int, int, int] | None

Crop region as (x1, y1, x2, y2) pixel coordinates.

None
scale tuple[float, float] | None

Scale factors as (scale_x, scale_y).

None
rotate float | None

Rotation angle in degrees. Positive is clockwise.

None
pad tuple[int, int, int, int] | None

Padding as (top, right, bottom, left) in pixels.

None
quality str

Interpolation quality. One of "nearest", "bilinear", "bicubic".

'bilinear'
fill tuple[int, ...] | int

Fill value for out-of-bounds and padded regions.

0
expand_rotation bool

If True (default), expand canvas to fit rotated image. If False, keep original dimensions (clips corners).

True
flip_h bool

If True, flip horizontally (mirror left-right).

False
flip_v bool

If True, flip vertically (mirror top-bottom).

False

Returns:

Type Description
ndarray

Transformed frame as numpy array.

Source code in sleap_io/transform/frame.py
def transform_frame(
    frame: np.ndarray,
    crop: tuple[int, int, int, int] | None = None,
    scale: tuple[float, float] | None = None,
    rotate: float | None = None,
    pad: tuple[int, int, int, int] | None = None,
    quality: str = "bilinear",
    fill: tuple[int, ...] | int = 0,
    expand_rotation: bool = True,
    flip_h: bool = False,
    flip_v: bool = False,
) -> np.ndarray:
    """Apply a sequence of transformations to a frame.

    Transforms are applied in order: crop -> scale -> rotate -> pad -> flip.

    Args:
        frame: Input frame as numpy array with shape (H, W) or (H, W, C).
        crop: Crop region as (x1, y1, x2, y2) pixel coordinates.
        scale: Scale factors as (scale_x, scale_y).
        rotate: Rotation angle in degrees. Positive is clockwise.
        pad: Padding as (top, right, bottom, left) in pixels.
        quality: Interpolation quality. One of "nearest", "bilinear", "bicubic".
        fill: Fill value for out-of-bounds and padded regions.
        expand_rotation: If True (default), expand canvas to fit rotated image.
            If False, keep original dimensions (clips corners).
        flip_h: If True, flip horizontally (mirror left-right).
        flip_v: If True, flip vertically (mirror top-bottom).

    Returns:
        Transformed frame as numpy array.
    """
    result = frame

    if crop is not None:
        result = crop_frame(result, crop, fill=fill)

    if scale is not None:
        result = scale_frame(result, scale, quality=quality)

    if rotate is not None and rotate != 0:
        result = rotate_frame(
            result, rotate, quality=quality, fill=fill, expand=expand_rotation
        )

    if pad is not None:
        result = pad_frame(result, pad, fill=fill)

    if flip_h:
        result = flip_h_frame(result)

    if flip_v:
        result = flip_v_frame(result)

    return result