Commit 6d96949d authored by narugo1992's avatar narugo1992
Browse files

dev(narugo): add image pad to size function

parent 1ebc45c7
Loading
Loading
Loading
Loading
+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
+65 −0
Original line number Diff line number Diff line
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):
    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):
    if isinstance(color, str):
        rgba = ImageColor.getrgb(color) + (255,)
        if len(rgba) < 4:
            rgba = rgba[:3] + (255,)
    elif isinstance(color, int):
        rgba = (color, color, color, 255)
    elif isinstance(color, tuple):
        rgba = 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', 'P', 'L', 'LA']):
    rgba = _parse_color_to_rgba(color)
    if mode == 'L' or mode == 'P':
        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 mode: {mode}")


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):
    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
+52 −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,57 @@ def _parse_rescale(obj):
    }


class PillowPadToSize:
    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 = background_color
        self.interpolation = interpolation
        _parse_color_to_rgba(self.background_color)

    def __call__(self, pic):
        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:
        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'):
    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):
    if not isinstance(obj, PillowPadToSize):
        raise NotParseTarget

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


class PillowCompose:
    """
    Composes several transforms together into a single transform.
+88 −1
Original line number Diff line number Diff line
@@ -9,14 +9,20 @@ and normalization. It provides a flexible framework for extending with additiona

import copy
from functools import wraps
from typing import Union
from typing import Union, Tuple

from PIL import Image

from .base import NotParseTarget
from ..data import pad_image_to_size

try:
    import torchvision
    import torch
except (ImportError, ModuleNotFoundError):
    _HAS_TORCHVISION = False
    torchvision = None
    torch = None
else:
    _HAS_TORCHVISION = True

@@ -71,6 +77,22 @@ def _get_interpolation_mode(value):
        raise TypeError(f'Unknown type of interpolation mode - {value!r}.')


def _get_int_from_interpolation_mode(value):
    from torchvision.transforms import InterpolationMode
    if not isinstance(value, InterpolationMode):
        raise TypeError(f'Unknown type of interpolation mode, cannot be transformed to int - {value!r}')

    _INTERMODE_TO_INT = {
        InterpolationMode.NEAREST: 0,
        InterpolationMode.BILINEAR: 2,
        InterpolationMode.BICUBIC: 3,
        InterpolationMode.BOX: 4,
        InterpolationMode.HAMMING: 5,
        InterpolationMode.LANCZOS: 1,
    }
    return _INTERMODE_TO_INT[value]


_TRANS_CREATORS = {}


@@ -327,6 +349,71 @@ def _parse_normalize(obj):
    }


if _HAS_TORCHVISION:
    from torchvision.transforms import InterpolationMode


    class PadToSize(torch.nn.Module):
        """
        Resize and center-pad PIL image to target size with background color.
        TorchVision-compatible transform that can be composed.
        """

        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: InterpolationMode = InterpolationMode.BILINEAR):
            super().__init__()
            from ..data.pad import _parse_size, _parse_color_to_rgba
            self.size: Tuple[int, int] = _parse_size(size)
            self.background_color = background_color
            self.interpolation: InterpolationMode = interpolation
            _parse_color_to_rgba(self.background_color)

        def forward(self, pic):
            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=_get_int_from_interpolation_mode(self.interpolation),
            )

        def __repr__(self) -> str:
            detail = f"(size={self.size}, interpolation={self.interpolation.value}, background_color={self.background_color})"
            return f"{self.__class__.__name__}{detail}"

else:
    PadToSize = None


@_register_transform('pad_to_size', safe=False)
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='bilinear'):
    assert PadToSize is not None
    return PadToSize(
        size=size,
        background_color=background_color,
        interpolation=_get_interpolation_mode(interpolation),
    )


@_register_parse('pad_to_size', safe=False)
def _parse_pad_to_size(obj):
    assert PadToSize is not None
    if not isinstance(obj, PadToSize):
        raise NotParseTarget

    obj: PadToSize
    return {
        'size': list(obj.size),
        'background_color': obj.background_color,
        'interpolation': obj.interpolation.value,
    }


def create_torchvision_transforms(tvalue: Union[list, dict]):
    """
    Create torchvision transforms from config.