"""
define an image wrapper ingredient
"""
from pathlib import Path
from typing import Callable, Union
import imageio
import numpy as np
from PIL import Image
from .ingredient import Ingredient
[docs]class Pierogi(Ingredient):
"""
image container for iterative pixel manipulation
create a Pierogi from:
- pixel array
- video file with a frame index
- image file
- PIL image
unless pixels are provided explicitly ( Pierogi(pixels=pixels) ),
the pixels member is a property that is lazy loaded
(usually from a file) and cached
"""
RESIZE_RESAMPLE = Image.NEAREST
"""default resize algorithm is nearest neighbor"""
ROTATE_RESAMPLE = Image.NEAREST
"""default rotate algorithm is nearest neighbor"""
_pixels: np.ndarray = None
"""underlying numpy pixels array"""
@property
def image(self) -> Image.Image:
"""
turn the numpy array into a PIL Image
"""
image = Image.fromarray(np.rot90(self.pixels), 'RGB')
return image
@property
def width(self) -> int:
"""
1st dimension of the underlying pixel array
"""
return self.pixels.shape[0]
@property
def height(self) -> int:
"""
2nd dimension of the underlying pixel array
"""
return self.pixels.shape[1]
[docs] def prep(
self,
pixels: np.ndarray = None,
loader: Callable[[], np.ndarray] = None,
**kwargs
) -> None:
"""
provide the source image in a number of ways
:param pixels: numpy array
:param loader: function that produces a pixels array
"""
if pixels is not None:
self._pixels = pixels
self._loader = lambda: pixels
elif loader is not None:
self._loader = loader
else:
raise Exception("one of pixels or loader must be provided")
[docs] @classmethod
def from_path(cls, path: str, frame_index: int = 0) -> 'Pierogi':
"""
:param path: file path to load from
:param frame_index: if path is a multiframe format (video),
use this specified frame
"""
def loader():
reader = imageio.get_reader(path)
reader.set_image_index(frame_index)
return np.rot90(np.array(reader.get_next_data(), dtype='uint8')[:, :, :3], axes=(1, 0))
return cls(loader=loader)
[docs] @classmethod
def from_shape(cls, shape: tuple) -> 'Pierogi':
"""
:param shape: (width, height) to make default pixels array
"""
def loader():
return np.full((*shape, 3), cls._default_pixel, dtype='uint8')
return cls(loader=loader)
[docs] @classmethod
def from_pil_image(cls, image: Image.Image):
"""
:param image: PIL Image that has already been loaded
"""
def loader():
return np.rot90(np.array(image.convert('RGB'), dtype='uint8'), axes=(1, 0))
return cls(loader=loader)
@property
def pixels(self) -> np.ndarray:
if self._pixels is None:
self.load()
return self._pixels
[docs] def load(self) -> None:
"""
use the loader return the contained pixels one time
"""
self._pixels = self._loader()
[docs] def cook(self, pixels: np.ndarray) -> np.ndarray:
"""
performs actions on a pixel array and returns a cooked array
"""
return self.pixels
[docs] def show(self) -> None:
"""
open an image viewer to display the array
"""
self.image.show()
[docs] def save(self, path: Union[str, Path], optimize: bool = False) -> None:
"""
save the image to the given path
"""
output_filename = path
self.image.save(output_filename, optimize=optimize)
[docs] def resize(self, width: int, height: int, resample: int = RESIZE_RESAMPLE):
"""
resize pixels to new width and height
:param width: width to resize to
:param height: height to resize to
:param resample: resample method to use in Image.resize.
PIL documentation:
> "An optional resampling filter.
> This can be one of PIL.Image.NEAREST (use nearest neighbour),
> PIL.Image.BILINEAR (linear interpolation),
> PIL.Image.BICUBIC (cubic spline interpolation),
> or PIL.Image.LANCZOS (a high-quality downsampling filter).
> If omitted, or if the image has mode “1” or “P”, it is set PIL.Image.NEAREST."
"""
if height != self.height and width != self.width:
self._pixels = np.array(
Image.fromarray(
self.pixels
).resize(
(height, width), resample
)
)
[docs] def rotate(self, angle: float, resample: int = ROTATE_RESAMPLE):
"""
rotate underlying pixel array by an angle
:param angle: angle to rotate
:param resample: resample method to use in Image.resize.
PIL documentation:
> "An optional resampling filter.
> This can be one of PIL.Image.NEAREST (use nearest neighbour),
> PIL.Image.BILINEAR (linear interpolation),
> PIL.Image.BICUBIC (cubic spline interpolation),
> or PIL.Image.LANCZOS (a high-quality downsampling filter).
> If omitted, or if the image has mode “1” or “P”, it is set PIL.Image.NEAREST."
"""
if angle != 0:
self._pixels = np.array(
Image.fromarray(self.pixels).rotate(-angle, resample=resample, expand=True, fillcolor=0)
)