Unverified Commit 6a202a9b authored by narugo1992's avatar narugo1992 Committed by GitHub
Browse files

Merge pull request #159 from deepghs/dev/preprocess

dev(narugo): add image padding into preprocessors
parents 1ebc45c7 8d03fc1f
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -15,4 +15,5 @@ imgutils.data
    decode
    image
    layer
    pad
    url
+15 −0
Original line number Diff line number Diff line
imgutils.data.pad
==========================

.. currentmodule:: imgutils.data.pad

.. automodule:: imgutils.data.pad


pad_image_to_size
---------------------------

.. autofunction:: pad_image_to_size


+2 −1
Original line number Diff line number Diff line
@@ -8,4 +8,5 @@ from .decode import *
from .encode import *
from .image import *
from .layer import *
from .pad import *
from .url import *

imgutils/data/pad.py

0 → 100644
+134 −0
Original line number Diff line number Diff line
"""
Image padding and resizing utilities.

This module provides functions for padding and resizing images to specified dimensions
while maintaining aspect ratio. It includes utilities for parsing size specifications,
color values, and handling different image modes.
"""

from typing import Union, Tuple, Literal

from PIL import ImageColor, Image

from .image import ImageTyping, load_image

__all__ = [
    'pad_image_to_size',
]


def _parse_size(size: Union[Tuple[int, int], int]):
    """
    Parse size parameter into a tuple of width and height.

    :param size: Size specification as an integer or tuple of two integers
    :type size: Union[Tuple[int, int], int]

    :return: Tuple containing width and height
    :rtype: Tuple[int, int]

    :raises TypeError: If size is not an int or tuple/list of two ints
    """
    if isinstance(size, int):
        return size, size
    elif isinstance(size, (list, tuple)) and len(size) == 2:
        return int(size[0]), int(size[1])
    else:
        raise TypeError("Size must be int or tuple of two ints")


def _parse_color_to_rgba(color: Union[str, int, Tuple[int, int, int], Tuple[int, int, int, int]]):
    """
    Convert various color formats to RGBA tuple.

    :param color: Color specification (string, integer, or tuple/list)
    :type color: Union[str, int, Tuple[int, int, int], Tuple[int, int, int, int]]

    :return: RGBA color tuple
    :rtype: Tuple[int, int, int, int]

    :raises TypeError: If color format is not supported
    """
    if isinstance(color, str):
        rgba = ImageColor.getrgb(color) + (255,)
        rgba = tuple([*rgba, *((255,) * (4 - len(rgba)))])
    elif isinstance(color, int):
        rgba = (color, color, color, 255)
    elif isinstance(color, (list, tuple)):
        rgba = tuple([*color, *((255,) * (4 - len(color)))])
    else:
        raise TypeError(f"Invalid color type: {type(color)}")

    return rgba


def _parse_color_to_mode(color, mode: Literal['RGB', 'RGBA', 'L', 'LA']):
    """
    Convert color to the specified image mode format.

    :param color: Color specification (string, integer, or tuple/list)
    :type color: Union[str, int, Tuple, list]
    :param mode: Target image mode ('RGB', 'RGBA', 'L', or 'LA')
    :type mode: Literal['RGB', 'RGBA', 'L', 'LA']

    :return: Color value in the specified mode format
    :rtype: Union[int, Tuple[int, int], Tuple[int, int, int], Tuple[int, int, int, int]]

    :raises ValueError: If the specified image mode is not supported
    """
    rgba = _parse_color_to_rgba(color)
    if mode == 'L':
        return int(0.299 * rgba[0] + 0.587 * rgba[1] + 0.114 * rgba[2])
    elif mode == "LA":
        gray = int(0.299 * rgba[0] + 0.587 * rgba[1] + 0.114 * rgba[2])
        return gray, rgba[3]
    elif mode == "RGB":
        return rgba[:3]
    elif mode == "RGBA":
        return rgba
    else:
        raise ValueError(f"Unsupported image mode: {mode!r}")


def pad_image_to_size(pic: ImageTyping, size: Union[int, Tuple[int, int]],
                      background_color: Union[str, int, Tuple[int, int, int], Tuple[int, int, int, int]] = 'white',
                      interpolation: int = Image.BILINEAR):
    """
    Resize and pad an image to the specified size while maintaining aspect ratio.

    The function first resizes the image to fit within the target dimensions while
    preserving the aspect ratio, then pads the result with the specified background
    color to reach the exact target size.

    :param pic: Input image (PIL Image, file path, or other supported format)
    :type pic: ImageTyping
    :param size: Target size as an integer or tuple of (width, height)
    :type size: Union[int, Tuple[int, int]]
    :param background_color: Color to use for padding (name, RGB tuple, etc.)
    :type background_color: Union[str, int, Tuple[int, int, int], Tuple[int, int, int, int]]
    :param interpolation: PIL interpolation method for resizing
    :type interpolation: int

    :return: Resized and padded image
    :rtype: PIL.Image.Image

    :example:

    >>> from PIL import Image
    >>> img = Image.new('RGB', (100, 50))
    >>> padded = pad_image_to_size(img, (200, 200), background_color='blue')
    >>> padded.size
    (200, 200)
    """
    pic = load_image(pic, force_background=None, mode=None)
    target_w, target_h = _parse_size(size)
    original_w, original_h = pic.size
    ratio = min(target_w / original_w, target_h / original_h)
    new_w, new_h = round(original_w * ratio), round(original_h * ratio)

    resized = pic.resize((new_w, new_h), interpolation)
    bg_color = _parse_color_to_mode(background_color, pic.mode)
    canvas = Image.new(pic.mode, (target_w, target_h), bg_color)
    canvas.paste(resized, ((target_w - new_w) // 2, (target_h - new_h) // 2))

    return canvas
+105 −1
Original line number Diff line number Diff line
@@ -19,7 +19,7 @@ import numpy as np
from PIL import Image

from .base import NotParseTarget
from ..data import load_image
from ..data import load_image, pad_image_to_size

# noinspection PyUnresolvedReferences
_INT_TO_PILLOW = {
@@ -800,6 +800,110 @@ def _parse_rescale(obj):
    }


class PillowPadToSize:
    """
    A class for padding images to a specified size.

    This class provides functionality to pad images to a target size while maintaining
    the original image content. It supports various padding colors and interpolation methods.

    :param size: Target size as (width, height) tuple or single integer for square
    :param background_color: Color for padding area (RGB, RGBA, string color name, or integer)
    :param interpolation: PIL interpolation method for resizing
    :type interpolation: int
    """

    def __init__(self, size: Union[Tuple[int, int], int],
                 background_color: Union[str, int, Tuple[int, int, int], Tuple[int, int, int, int]] = 'white',
                 interpolation: int = Image.BILINEAR):
        from ..data.pad import _parse_size, _parse_color_to_rgba
        self.size = _parse_size(size)
        self.background_color = (tuple(background_color)
                                 if isinstance(background_color, (list, tuple)) else background_color)
        self.interpolation = interpolation
        _parse_color_to_rgba(self.background_color)

    def __call__(self, pic):
        """
        Apply padding transformation to the input image.

        :param pic: Input PIL Image
        :type pic: PIL.Image.Image

        :return: Padded image
        :rtype: PIL.Image.Image
        :raises TypeError: If input is not a PIL Image
        """
        if not isinstance(pic, Image.Image):
            raise TypeError('pic should be PIL Image. Got {}'.format(type(pic)))

        return pad_image_to_size(
            pic=pic,
            size=self.size,
            background_color=self.background_color,
            interpolation=self.interpolation,
        )

    def __repr__(self) -> str:
        """
        Return string representation of the class.

        :return: String representation
        :rtype: str
        """
        interpolate_str = _PILLOW_TO_STR[self.interpolation]
        detail = f"(size={self.size}, interpolation={interpolate_str}, background_color={self.background_color})"
        return f"{self.__class__.__name__}{detail}"


@register_pillow_transform('pad_to_size')
def _create_pad_to_size(size: Union[Tuple[int, int], int],
                        background_color: Union[str, int, Tuple[int, int, int], Tuple[int, int, int, int]] = 'white',
                        interpolation: str = 'bilinear'):
    """
    Factory function to create PillowPadToSize instance.

    :param size: Target size for padding
    :type size: Union[Tuple[int, int], int]
    :param background_color: Color for padding area
    :type background_color: Union[str, int, Tuple[int, int, int], Tuple[int, int, int, int]]
    :param interpolation: Interpolation method name
    :type interpolation: str

    :return: PillowPadToSize instance
    :rtype: PillowPadToSize
    """
    return PillowPadToSize(
        size=size,
        background_color=background_color,
        interpolation=_STR_TO_PILLOW[interpolation],
    )


@register_pillow_parse('pad_to_size')
def _parse_pad_to_size(obj):
    """
    Parse PillowPadToSize object to dictionary configuration.

    :param obj: Object to parse
    :type obj: Any

    :return: Configuration dictionary
    :rtype: dict
    :raises NotParseTarget: If object is not PillowPadToSize instance
    """
    if not isinstance(obj, PillowPadToSize):
        raise NotParseTarget

    obj: PillowPadToSize
    return {
        'size': list(obj.size),
        'background_color': (list(obj.background_color)
                             if isinstance(obj.background_color, (list, tuple)) else obj.background_color),
        'interpolation': _PILLOW_TO_STR[obj.interpolation],
    }


class PillowCompose:
    """
    Composes several transforms together into a single transform.
Loading