Source code for imgutils.data.layer

from typing import Union, Tuple, Optional

import numpy as np
from PIL import ImageColor, Image

from .image import load_image, ImageTyping

__all__ = [
    'istack',
]


def _load_image_or_color(image) -> Union[str, Image.Image]:
    if isinstance(image, str):
        try:
            _ = ImageColor.getrgb(image)
        except ValueError:
            pass
        else:
            return image

    return load_image(image, mode='RGBA', force_background=None)


def _process(item):
    if isinstance(item, tuple):
        image, alpha = item
    else:
        image, alpha = item, 1

    return _load_image_or_color(image), alpha


_AlphaTyping = Union[float, np.ndarray]


def _add_alpha(image: Image.Image, alpha: _AlphaTyping) -> Image.Image:
    data = np.array(image.convert('RGBA')).astype(np.float32)
    data[:, :, 3] = (data[:, :, 3] * alpha).clip(0, 255)
    return Image.fromarray(data.astype(np.uint8), mode='RGBA')


[docs]def istack(*items: Union[ImageTyping, str, Tuple[ImageTyping, _AlphaTyping], Tuple[str, _AlphaTyping]], size: Optional[Tuple[int, int]] = None) -> Image.Image: """ Overview: Layer multiple images (which may contain transparent areas) and color blocks together into a new image, similar to a layering technique in PS. :param items: The layers that need to be stacked. If a PIL object or the file path of an image is given, the image will be used as a layer; if a color is given, the color will be used as a layer. Additionally, if a tuple is given, the second element represents the transparency, with a value range of :math:`\\left[0, 1\\right]`. It can be a float type or a two-dimensional numpy array in the format of ``float32[H, W]`` which represents the transparency of each position. :param size: The size of the target image. By default, the size of the first image object in the `items` list will be used. However, when all layers are solid colors, this parameter is required. :return: Stacked image. Examples:: >>> from imgutils.data import istack >>> >>> # pure color >>> istack('lime', 'nian.png').save('nian_lime.png') >>> >>> # transparency >>> istack(('yellow', 0.5), ('nian.png', 0.9)).save('nian_trans.png') >>> >>> # custom mask >>> import numpy as np >>> from PIL import Image >>> width, height = Image.open('nian.png').size >>> hs1 = (1 - np.abs(np.linspace(-1 / 3, 1, height))) ** 0.5 >>> ws1 = (1 - np.abs(np.linspace(-1, 1, width))) ** 0.5 >>> nian_mask = hs1[..., None] * ws1 # HxW >>> istack(('nian.png', nian_mask)).save('nian_mask.png') The result should be .. image:: grid_istack.plot.py.svg :align: center """ if size is None: height, width = None, None items = list(map(_process, items)) for item, alpha in items: if isinstance(item, Image.Image): height, width = item.height, item.width break else: width, height = size if height is None: raise ValueError('Unable to determine image size, please make sure ' 'you have provided at least one image object (image path or PIL object).') retval = Image.fromarray(np.zeros((height, width, 4), dtype=np.uint8), mode='RGBA') for item, alpha in items: if isinstance(item, str): current = Image.new("RGBA", (width, height), item) elif isinstance(item, Image.Image): current = item else: assert False, f'Invalid type - {item!r}. If you encounter this situation, ' \ f'it means there is a bug in the code. Please contact the developer.' # pragma: no cover current = _add_alpha(current, alpha) retval.paste(current, mask=current) return retval