pyrogis

ingredients

"pierogis is the name of the framework;

pyrogis is the name of the python package and cli tool"

- a wise man

from pyrogis import Pierogi, SpatialQuantize, Sort, Threshold, Dish, Recipe

A processing factory, called an Ingredient, has a prep method for receiving parameters, and a cook method for operating on a numpy array to produce a programmatic output.

These two methods are usually called implicitly, prep on __init__ and cook obfuscated in the typical usage flow. prep can be seen as parameterizing the manipulation while cook applies it (to an array).

pierogi

Pierogi is one of the simplest Ingredient types. It stores pixels, usually loaded from an image.

Pierogi is unique in that the array it returns from its cook function is not based on the input

pierogi = Pierogi(file="/Users/kyle/Desktop/image.jpg")
pierogi = Pierogi(pixels=np.array(
    [[[0, 0, 0], [0, 0, 0]], [[255, 255, 255], [255, 255, 255]]]
)

quantize

Quantize is another Ingredient. When cooked, it will process an incoming numpy array and return an array where every pixel has been quantized to the closest color in the palette.

There is also the SpatialQuantize variant which is used for the cli tool.

palette = [
    [0, 0, 0],
    [127, 127, 127],
    [255, 255, 255]
]

quantize = Quantize(palette=palette)
quantized_pixels = quantize.cook(pierogi.pixels)

This should produce a pixel for pixel quantized version of the input array.

As you can see above, an Pierogi has a pixels member. This is the internal numpy pixel array of that Pierogi with shape (width, height, 3).

Some other Ingredient types include: Threshold, Flip, and Rotate.

recipe

A typical flow allows you to create a pipeline of Ingredient types that sequentially apply their cook method on to the previous array of pixels.

A pipeline in pierogis is called a Recipe. It is an Ingredient itself.

recipe = Recipe(ingredients=[pierogi, quantize])
recipe.cook()

recipe = Recipe(ingredients=[quantize])
recipe.cook(pierogi.pixels)

The two will produce the same result. But there's a better way.

dish

"get to the point already"

- a wiser man

We could also use a Dish to serve this recipe. This is the recommended way to use Recipe.

dish = Dish(recipe=recipe, pierogis=[pierogi])
cooked_dish = dish.serve()

The recipe gets cooked sequentially for each pierogi in pierogis. The output dish has pierogis member set with cooked pierogis. The save method of dish can be used as well.

seasoning

There is also a concept of seasonings. They can be used to apply something like a mask to other ingredients that affect the pixels they act on.

sort = Sort()
seasoning = Threshold()

sort.season(seasoning)

Threshold.cook outputs a black and white array. Now that sort is seasoned with the Threshold, it will only sort pixels that have been "colored" white by the Threshold

extending

To create a custom Ingredient type, it must subclass Ingredient and override the cook and prep methods.

class Custom(Ingredient):
    def prep(self, brighten: int, scale: int, **kwargs):
        self.brighten = brighten
        self.scale = scale
    def cook(self, pixels: np.ndarray):
        return (self.pixels + self.brighten) /*self.scale

prep

Override prep to parameterize your manipulation.

This means any settings, constants, or inputs that configure the new functionality. Think about the palette used with quantization.

def prep(self, brighten: int, scale: int, *args, **kwargs):
    self.brighten = brighten
    self.scale = scale

cook

Override cook to perform the manipulation.

This is the function that you acts on an input pixel grid. More specifically, this function receives a (width, height, 3) ndarray and should return a 3d array that is also size 3 in the last dimension.

def cook(self, pixels: np.ndarray):
    return (self.pixels + self.brighten) * self.scale

This function increases the r, g, and b of every pixel by self.brighten then multiplies that sum for each by self.scale.

Numpy operations can be pretty fast if you can keep them vectorized. This means try to avoid looping over the columns and rows of an array.

View Source
"""
<a name="ingredients"></a>
## ingredients

>*"`pierogis` is the name of the framework;*
>
>*`pyrogis` is the name of the python package and cli tool"*
>
>\- a wise man

```python
from pyrogis import Pierogi, SpatialQuantize, Sort, Threshold, Dish, Recipe
```

A processing factory, called an `Ingredient`,
has a `prep` method for receiving parameters,
and a `cook` method for operating
on a numpy array to produce a programmatic output.

These two methods are usually called implicitly,
`prep` on __init__ and `cook` obfuscated in the typical usage flow.
`prep` can be seen as parameterizing the manipulation
while `cook` applies it (to an array).

### pierogi

`Pierogi` is one of the simplest `Ingredient` types.
It stores pixels, usually loaded from an image.

`Pierogi` is unique in that the array it returns from its cook function
is not based on the input

```python
pierogi = Pierogi(file="/Users/kyle/Desktop/image.jpg")
pierogi = Pierogi(pixels=np.array(
    [[[0, 0, 0], [0, 0, 0]], [[255, 255, 255], [255, 255, 255]]]
)
```

### quantize

`Quantize` is another `Ingredient`.
When cooked, it will process an incoming numpy array and return an array
where every pixel has been quantized to the closest color in the `palette`.

There is also the `SpatialQuantize` variant which is used for the cli tool.

```python
palette = [
    [0, 0, 0],
    [127, 127, 127],
    [255, 255, 255]
]

quantize = Quantize(palette=palette)
quantized_pixels = quantize.cook(pierogi.pixels)
```

This should produce a pixel for pixel quantized version of the input array.

As you can see above, an `Pierogi` has a `pixels` member.
This is the internal numpy pixel array of that `Pierogi`
with shape `(width, height, 3)`.

Some other `Ingredient` types include: `Threshold`, `Flip`, and `Rotate`.

### recipe

A typical flow allows you to create a pipeline of `Ingredient` types
that sequentially apply their `cook` method on to
the previous array of pixels.

A pipeline in `pierogis` is called a `Recipe`. It is an `Ingredient` itself.

```python
recipe = Recipe(ingredients=[pierogi, quantize])
recipe.cook()

recipe = Recipe(ingredients=[quantize])
recipe.cook(pierogi.pixels)
```

The two will produce the same result. But there's a better way.

### dish

>*"get to the point already"*
>
>\- a wiser man

We could also use a `Dish` to serve this recipe.
This is the recommended way to use `Recipe`.

```python
dish = Dish(recipe=recipe, pierogis=[pierogi])
cooked_dish = dish.serve()
```

The recipe gets cooked sequentially for each pierogi in `pierogis`.
The output dish has `pierogis` member set with cooked pierogis.
The `save` method of `dish` can be used as well.

### seasoning

There is also a concept of seasonings.
They can be used to apply something like a mask
to other ingredients that affect the pixels they act on.

```python
sort = Sort()
seasoning = Threshold()

sort.season(seasoning)
```

`Threshold.cook` outputs a black and white array.
Now that `sort` is seasoned with the `Threshold`,
it will only sort pixels that have been "colored"
white by the `Threshold`

## extending

To create a custom `Ingredient` type,
it must subclass `Ingredient` and override the `cook` and `prep`
methods.

```python
class Custom(Ingredient):
    def prep(self, brighten: int, scale: int, **kwargs):
        self.brighten = brighten
        self.scale = scale
    def cook(self, pixels: np.ndarray):
        return (self.pixels + self.brighten) /*self.scale
```

### prep

*Override `prep` to parameterize your manipulation.*

This means any settings, constants,
or inputs that configure the new functionality.
Think about the `palette` used with
quantization.

```python
def prep(self, brighten: int, scale: int, *args, **kwargs):
    self.brighten = brighten
    self.scale = scale
```

### cook

*Override `cook` to perform the manipulation.*

This is the function that you acts on an input pixel grid.
More specifically, this function receives
a `(width, height, 3)` `ndarray`
and should return a 3d array that is also size 3 in the last dimension.

```python
def cook(self, pixels: np.ndarray):
    return (self.pixels + self.brighten) * self.scale
```

This function increases the r, g, and b of every pixel by `self.brighten`
then multiplies that sum for each by `self.scale`.

Numpy operations can be pretty fast if you can keep them vectorized.
This means try to avoid looping over the columns
and rows of an array.
"""

from .chef import Chef
from .ingredients import Dish
from .ingredients import Ingredient
from .ingredients import Pierogi
from .ingredients import Quantize
from .ingredients import Recipe
from .ingredients import Sort
from .ingredients import SpatialQuantize
from .ingredients import Threshold

__version__ = '0.2.0'

__all__ = [
    'Pierogi',
    'Dish',
    'Ingredient',
    'Recipe',
    'Pierogi',
    'Quantize',
    'SpatialQuantize',
    'Sort',
    'Threshold',
    'Chef'
]
#   class Pierogi(pyrogis.ingredients.ingredient.Ingredient):
View Source
class Pierogi(Ingredient):
    """
    image container for iterative pixel manipulation

    uses PIL.Image to load to self.pixels
    """

    RESAMPLE = Image.NEAREST
    """default resize algorithm is nearest neighbor"""

    pixels: np.ndarray
    """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]

    def prep(
            self,
            pixels: np.ndarray = None,
            shape: tuple = (0, 0),
            image: Image.Image = None,
            file: str = None,
            **kwargs
    ) -> None:
        """
        provide the source image in a number of ways

        :param pixels: numpy array

        :param shape: shape to make simple pixels array

        :param image: PIL Image that has already been loaded

        :param file: file path to load from
        """

        if pixels is None:
            if image is not None:
                # rotate the image array on receipt so that
                # array dimensions are (width, height, 3)
                pixels = np.rot90(np.array(image.convert('RGB')), axes=(1, 0))
            elif file is not None:
                self.file = file
                pixels = np.rot90(np.array(imageio.imread(file)), axes=(1, 0))
            elif shape is not None:
                pixels = np.full((*shape, 3), self._default_pixel)
            else:
                raise Exception("one of image, file, or shape must be provided")

        self.pixels = pixels

    def cook(self, pixels: np.ndarray) -> np.ndarray:
        """
        performs actions on a pixel array and returns a cooked array
        """
        return self.pixels

    def show(self) -> None:
        """
        open an image viewer to display the array
        """
        self.image.show()

    def save(self, path: str, optimize: bool =False) -> None:
        """
        save the image to the given path
        """

        output_filename = path
        if os.path.isdir(path):
            output_filename = os.path.join(
                path, os.path.split(self.file)[1]
            )

        self.image.save(output_filename, optimize=optimize)

    def resize(self, width: int, height: int, resample: int = 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."
        """

        self.pixels = np.array(
            Image.fromarray(
                self.pixels
            ).resize(
                (height, width), resample
            )
        )

image container for iterative pixel manipulation

uses PIL.Image to load to self.pixels

#   RESAMPLE = 0

default resize algorithm is nearest neighbor

#   pixels: numpy.ndarray

underlying numpy pixels array

#   image: PIL.Image.Image

turn the numpy array into a PIL Image

#   width: int

1st dimension of the underlying pixel array

#   height: int

2nd dimension of the underlying pixel array

#   def prep( self, pixels: numpy.ndarray = None, shape: tuple = (0, 0), image: PIL.Image.Image = None, file: str = None, **kwargs ) -> None:
View Source
    def prep(
            self,
            pixels: np.ndarray = None,
            shape: tuple = (0, 0),
            image: Image.Image = None,
            file: str = None,
            **kwargs
    ) -> None:
        """
        provide the source image in a number of ways

        :param pixels: numpy array

        :param shape: shape to make simple pixels array

        :param image: PIL Image that has already been loaded

        :param file: file path to load from
        """

        if pixels is None:
            if image is not None:
                # rotate the image array on receipt so that
                # array dimensions are (width, height, 3)
                pixels = np.rot90(np.array(image.convert('RGB')), axes=(1, 0))
            elif file is not None:
                self.file = file
                pixels = np.rot90(np.array(imageio.imread(file)), axes=(1, 0))
            elif shape is not None:
                pixels = np.full((*shape, 3), self._default_pixel)
            else:
                raise Exception("one of image, file, or shape must be provided")

        self.pixels = pixels

provide the source image in a number of ways

:param pixels: numpy array

:param shape: shape to make simple pixels array

:param image: PIL Image that has already been loaded

:param file: file path to load from

#   def cook(self, pixels: numpy.ndarray) -> numpy.ndarray:
View Source
    def cook(self, pixels: np.ndarray) -> np.ndarray:
        """
        performs actions on a pixel array and returns a cooked array
        """
        return self.pixels

performs actions on a pixel array and returns a cooked array

#   def show(self) -> None:
View Source
    def show(self) -> None:
        """
        open an image viewer to display the array
        """
        self.image.show()

open an image viewer to display the array

#   def save(self, path: str, optimize: bool = False) -> None:
View Source
    def save(self, path: str, optimize: bool =False) -> None:
        """
        save the image to the given path
        """

        output_filename = path
        if os.path.isdir(path):
            output_filename = os.path.join(
                path, os.path.split(self.file)[1]
            )

        self.image.save(output_filename, optimize=optimize)

save the image to the given path

#   def resize(self, width: int, height: int, resample: int = 0):
View Source
    def resize(self, width: int, height: int, resample: int = 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."
        """

        self.pixels = np.array(
            Image.fromarray(
                self.pixels
            ).resize(
                (height, width), 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."

Inherited Members
pyrogis.ingredients.ingredient.Ingredient
Ingredient
opacity
mask
mask_pixels
season
#   class Dish(pyrogis.ingredients.ingredient.Ingredient):
View Source
class Dish(Ingredient):
    """
    crop and cook an entire recipe for all pixels
    """

    @property
    def frames(self):
        return len(self.pierogis)

    def prep(
            self, recipe=None,
            pierogis: List[Pierogi] = None,
            file=None,
            path=None,
            **kwargs
    ):
        """
        set the recipe to cook for this dish

        :param recipe: something callable that returns a
        cook(pixels) method.
        Any Ingredient (including recipe) is an example of this

        :param pierogis: a list of Pierogi to cook

        :param file: a file to use as input to

        :param path: a list of Pierogi to cook
        """

        if pierogis is None:
            pierogis = []

            if file is not None:
                try:
                    # first try to load as video/animation
                    images = imageio.mimread(file, memtest=False)
                    for image in images:
                        pierogis.append(
                            Pierogi(pixels=np.rot90(
                                np.asarray(image), axes=(1, 0)
                            ))
                        )

                except ValueError:
                    # then load as a single image
                    pierogis = [Pierogi(file=file)]

            elif path is not None:
                pierogis = self.get_path_pierogis(path)

            else:
                raise ValueError("Could not create pierogis")

        self.pierogis = pierogis

        if recipe is None:
            Recipe()
        self.recipe = recipe

    @staticmethod
    def get_path_pierogis(path: str) -> List[Pierogi]:
        pierogis = []

        for file in os.listdir(path):
            if not os.path.isfile(file):
                continue

            pierogis.append(Pierogi(file=file))

        return pierogis

    def cook(self, pixels: np.ndarray) -> np.ndarray:
        return self.recipe(0, 0).cook(self.pierogis[0].pixels)

    def serve(self) -> 'Dish':
        """
        cook the recipe and set the output to this object's pixel array
        """

        cooked_pierogis = []

        for frame in range(self.frames):
            pierogi = self.pierogis[frame]
            # cook with these pixels as first input
            recipe = self.recipe(frame + 1, self.frames)
            cooked_pixels = recipe.cook(pierogi.pixels)
            # ensure that the cooked pixels do not overflow 0-255
            clipped_pixels = np.clip(cooked_pixels, 0, 255)
            # # set the objects own pixels to the result of cooking
            cooked_pierogi = Pierogi(pixels=clipped_pixels)

            cooked_pierogis.append(cooked_pierogi)

        return Dish(pierogis=cooked_pierogis)

    def save(
            self, path, optimize: bool = True, duration: float = None, fps: int = 25
    ) -> None:
        """
        :param duration: s duration between frames
        """
        if len(self.pierogis) > 1:
            ims = [np.asarray(pierogi.image) for pierogi in self.pierogis]
            if duration is not None:
                imageio.mimwrite(
                    path,
                    ims=ims,
                    duration=duration,
                    fps=fps
                )
            else:
                imageio.mimwrite(
                    path,
                    ims=ims,
                    fps=fps
                )

            if optimize and os.path.splitext(path)[1] == ".gif":
                try:
                    import pygifsicle
                    pygifsicle.optimize(path)
                except FileNotFoundError as err:
                    print(err)

        elif len(self.pierogis) == 1:
            self.pierogis[0].save(path)

        else:
            raise Exception("Dish has no pierogis")

crop and cook an entire recipe for all pixels

#   frames
#   def prep( self, recipe=None, pierogis: List[pyrogis.ingredients.pierogi.Pierogi] = None, file=None, path=None, **kwargs ):
View Source
    def prep(
            self, recipe=None,
            pierogis: List[Pierogi] = None,
            file=None,
            path=None,
            **kwargs
    ):
        """
        set the recipe to cook for this dish

        :param recipe: something callable that returns a
        cook(pixels) method.
        Any Ingredient (including recipe) is an example of this

        :param pierogis: a list of Pierogi to cook

        :param file: a file to use as input to

        :param path: a list of Pierogi to cook
        """

        if pierogis is None:
            pierogis = []

            if file is not None:
                try:
                    # first try to load as video/animation
                    images = imageio.mimread(file, memtest=False)
                    for image in images:
                        pierogis.append(
                            Pierogi(pixels=np.rot90(
                                np.asarray(image), axes=(1, 0)
                            ))
                        )

                except ValueError:
                    # then load as a single image
                    pierogis = [Pierogi(file=file)]

            elif path is not None:
                pierogis = self.get_path_pierogis(path)

            else:
                raise ValueError("Could not create pierogis")

        self.pierogis = pierogis

        if recipe is None:
            Recipe()
        self.recipe = recipe

set the recipe to cook for this dish

:param recipe: something callable that returns a cook(pixels) method. Any Ingredient (including recipe) is an example of this

:param pierogis: a list of Pierogi to cook

:param file: a file to use as input to

:param path: a list of Pierogi to cook

#  
@staticmethod
def get_path_pierogis(path: str) -> List[pyrogis.ingredients.pierogi.Pierogi]:
View Source
    @staticmethod
    def get_path_pierogis(path: str) -> List[Pierogi]:
        pierogis = []

        for file in os.listdir(path):
            if not os.path.isfile(file):
                continue

            pierogis.append(Pierogi(file=file))

        return pierogis
#   def cook(self, pixels: numpy.ndarray) -> numpy.ndarray:
View Source
    def cook(self, pixels: np.ndarray) -> np.ndarray:
        return self.recipe(0, 0).cook(self.pierogis[0].pixels)

performs actions on a pixel array and returns a cooked array

View Source
    def serve(self) -> 'Dish':
        """
        cook the recipe and set the output to this object's pixel array
        """

        cooked_pierogis = []

        for frame in range(self.frames):
            pierogi = self.pierogis[frame]
            # cook with these pixels as first input
            recipe = self.recipe(frame + 1, self.frames)
            cooked_pixels = recipe.cook(pierogi.pixels)
            # ensure that the cooked pixels do not overflow 0-255
            clipped_pixels = np.clip(cooked_pixels, 0, 255)
            # # set the objects own pixels to the result of cooking
            cooked_pierogi = Pierogi(pixels=clipped_pixels)

            cooked_pierogis.append(cooked_pierogi)

        return Dish(pierogis=cooked_pierogis)

cook the recipe and set the output to this object's pixel array

#   def save( self, path, optimize: bool = True, duration: float = None, fps: int = 25 ) -> None:
View Source
    def save(
            self, path, optimize: bool = True, duration: float = None, fps: int = 25
    ) -> None:
        """
        :param duration: s duration between frames
        """
        if len(self.pierogis) > 1:
            ims = [np.asarray(pierogi.image) for pierogi in self.pierogis]
            if duration is not None:
                imageio.mimwrite(
                    path,
                    ims=ims,
                    duration=duration,
                    fps=fps
                )
            else:
                imageio.mimwrite(
                    path,
                    ims=ims,
                    fps=fps
                )

            if optimize and os.path.splitext(path)[1] == ".gif":
                try:
                    import pygifsicle
                    pygifsicle.optimize(path)
                except FileNotFoundError as err:
                    print(err)

        elif len(self.pierogis) == 1:
            self.pierogis[0].save(path)

        else:
            raise Exception("Dish has no pierogis")

:param duration: s duration between frames

Inherited Members
pyrogis.ingredients.ingredient.Ingredient
Ingredient
opacity
mask
mask_pixels
season
#   class Ingredient:
View Source
class Ingredient:
    """
    wrapper for a function to be applied to a grid of pixels.

    this base class defines some basic methods to be inherited and built upon,
    as well as a container for media represented as a numpy array.

    the two methods to be inherited and overridden are prep and cook.

    prep defines and sets the parameters of this image manipulation,
    and cook performs the manipulation of the array.

    in this base class, cook and prep do nothing
    """

    # used to fill in empty spots when cooked
    _black_pixel = np.array([0, 0, 0])
    _white_pixel = np.array([255, 255, 255])

    _default_pixel = _white_pixel

    def __init__(self, opacity: int = 100, mask: np.ndarray = None, **kwargs):
        """
        :param opacity: cook will overlay this % on input pixels

        :param mask: only cook with the pixels that are white in this mask

        :param kwargs: extra arguments to be passed to prep
        """
        self.opacity = opacity
        """opacity when overlaying on previous output"""
        self.mask = mask
        """opacity when overlaying"""
        self.prep(**kwargs)
        self.seasonings = []

    def __call__(self, frame: int, frames: int):
        return self

    def prep(self, **kwargs) -> None:
        """
        parameterize the cook function
        """
        pass

    def cook(self, pixels: np.ndarray) -> np.ndarray:
        """
        performs actions on a pixel array and returns a cooked array
        """
        return pixels

    def mask_pixels(self, pixels):
        """
        create a black and white mask from pixels
        and the seasonings attached to this ingredient

        :param pixels: pixels to create a mask from
        """
        white_pixels = np.resize(self._white_pixel, pixels.shape)

        base_mask = self.mask
        if base_mask is None:
            base_mask = white_pixels

        masks = [base_mask]

        for seasoning in self.seasonings:
            masks.append(seasoning.cook(pixels))

        binary_array = np.all(np.asarray(masks) == 255, axis=0)

        black_pixels = np.resize(self._black_pixel, pixels.shape)

        mask = black_pixels

        mask[binary_array] = white_pixels[binary_array]

        return mask

    def season(self, seasoning: 'Ingredient'):
        """
        add an ingredient whose cook function is
        used to produce this ingredients mask
        """
        self.seasonings.append(seasoning)

wrapper for a function to be applied to a grid of pixels.

this base class defines some basic methods to be inherited and built upon, as well as a container for media represented as a numpy array.

the two methods to be inherited and overridden are prep and cook.

prep defines and sets the parameters of this image manipulation, and cook performs the manipulation of the array.

in this base class, cook and prep do nothing

#   Ingredient(opacity: int = 100, mask: numpy.ndarray = None, **kwargs)
View Source
    def __init__(self, opacity: int = 100, mask: np.ndarray = None, **kwargs):
        """
        :param opacity: cook will overlay this % on input pixels

        :param mask: only cook with the pixels that are white in this mask

        :param kwargs: extra arguments to be passed to prep
        """
        self.opacity = opacity
        """opacity when overlaying on previous output"""
        self.mask = mask
        """opacity when overlaying"""
        self.prep(**kwargs)
        self.seasonings = []

:param opacity: cook will overlay this % on input pixels

:param mask: only cook with the pixels that are white in this mask

:param kwargs: extra arguments to be passed to prep

#   opacity

opacity when overlaying on previous output

#   mask

opacity when overlaying

#   def prep(self, **kwargs) -> None:
View Source
    def prep(self, **kwargs) -> None:
        """
        parameterize the cook function
        """
        pass

parameterize the cook function

#   def cook(self, pixels: numpy.ndarray) -> numpy.ndarray:
View Source
    def cook(self, pixels: np.ndarray) -> np.ndarray:
        """
        performs actions on a pixel array and returns a cooked array
        """
        return pixels

performs actions on a pixel array and returns a cooked array

#   def mask_pixels(self, pixels):
View Source
    def mask_pixels(self, pixels):
        """
        create a black and white mask from pixels
        and the seasonings attached to this ingredient

        :param pixels: pixels to create a mask from
        """
        white_pixels = np.resize(self._white_pixel, pixels.shape)

        base_mask = self.mask
        if base_mask is None:
            base_mask = white_pixels

        masks = [base_mask]

        for seasoning in self.seasonings:
            masks.append(seasoning.cook(pixels))

        binary_array = np.all(np.asarray(masks) == 255, axis=0)

        black_pixels = np.resize(self._black_pixel, pixels.shape)

        mask = black_pixels

        mask[binary_array] = white_pixels[binary_array]

        return mask

create a black and white mask from pixels and the seasonings attached to this ingredient

:param pixels: pixels to create a mask from

#   def season(self, seasoning: pyrogis.ingredients.ingredient.Ingredient):
View Source
    def season(self, seasoning: 'Ingredient'):
        """
        add an ingredient whose cook function is
        used to produce this ingredients mask
        """
        self.seasonings.append(seasoning)

add an ingredient whose cook function is used to produce this ingredients mask

#   class Recipe(pyrogis.ingredients.ingredient.Ingredient):
View Source
class Recipe(Ingredient):
    """
    ingredient used to coordinate cooking of several ingredients

    when a recipe is cooked, its ingredients are cooked in order
    """

    def prep(self, ingredients: list = None, **kwargs):
        """
        provide a list of ingredients to cook in sequence

        :param ingredients: list of Ingredient objects
        """
        if ingredients is None:
            ingredients = []
        if isinstance(ingredients, list):
            self.ingredients = ingredients
        else:
            raise TypeError("kwarg 'ingredients' must be of type list")

    def cook(self, pixels: np.ndarray):
        """
        sequentially cooks each ingredient
        uses the pixels resulting from the previous cook
        """
        # input array used to select the
        under_pixels = pixels

        for ingredient in self.ingredients:
            # cook the lower layer
            cooked_pixels = ingredient.cook(under_pixels)
            mask = ingredient.mask_pixels(cooked_pixels)
            binary_array = np.all(mask == self._white_pixel, axis=2)

            # resize under array to cooked array
            cooked_width = cooked_pixels.shape[0]
            cooked_height = cooked_pixels.shape[1]
            resized_pixels = np.resize(under_pixels, (cooked_width, cooked_height, 3))

            masked_pixels = resized_pixels

            # layer cooked pixels over uncooked for true pixels (white in mask)
            masked_pixels[binary_array] = cooked_pixels[binary_array]

            # mix them based on the overlaying opacity
            mixed_pixels = (masked_pixels.astype(np.dtype(float))
                            * ingredient.opacity
                            +
                            resized_pixels.astype(np.dtype(float))
                            * (100 - ingredient.opacity)) / 100
            # reset for loop
            under_pixels = mixed_pixels.astype('uint8')

        # keep in range
        clipped_pixels = np.clip(under_pixels, 0, 255)

        return clipped_pixels

    def add(self, ingredient: Ingredient):
        """
        Add an ingredient
        """
        self.ingredients.append(ingredient)

ingredient used to coordinate cooking of several ingredients

when a recipe is cooked, its ingredients are cooked in order

#   def prep(self, ingredients: list = None, **kwargs):
View Source
    def prep(self, ingredients: list = None, **kwargs):
        """
        provide a list of ingredients to cook in sequence

        :param ingredients: list of Ingredient objects
        """
        if ingredients is None:
            ingredients = []
        if isinstance(ingredients, list):
            self.ingredients = ingredients
        else:
            raise TypeError("kwarg 'ingredients' must be of type list")

provide a list of ingredients to cook in sequence

:param ingredients: list of Ingredient objects

#   def cook(self, pixels: numpy.ndarray):
View Source
    def cook(self, pixels: np.ndarray):
        """
        sequentially cooks each ingredient
        uses the pixels resulting from the previous cook
        """
        # input array used to select the
        under_pixels = pixels

        for ingredient in self.ingredients:
            # cook the lower layer
            cooked_pixels = ingredient.cook(under_pixels)
            mask = ingredient.mask_pixels(cooked_pixels)
            binary_array = np.all(mask == self._white_pixel, axis=2)

            # resize under array to cooked array
            cooked_width = cooked_pixels.shape[0]
            cooked_height = cooked_pixels.shape[1]
            resized_pixels = np.resize(under_pixels, (cooked_width, cooked_height, 3))

            masked_pixels = resized_pixels

            # layer cooked pixels over uncooked for true pixels (white in mask)
            masked_pixels[binary_array] = cooked_pixels[binary_array]

            # mix them based on the overlaying opacity
            mixed_pixels = (masked_pixels.astype(np.dtype(float))
                            * ingredient.opacity
                            +
                            resized_pixels.astype(np.dtype(float))
                            * (100 - ingredient.opacity)) / 100
            # reset for loop
            under_pixels = mixed_pixels.astype('uint8')

        # keep in range
        clipped_pixels = np.clip(under_pixels, 0, 255)

        return clipped_pixels

sequentially cooks each ingredient uses the pixels resulting from the previous cook

View Source
    def add(self, ingredient: Ingredient):
        """
        Add an ingredient
        """
        self.ingredients.append(ingredient)

Add an ingredient

Inherited Members
pyrogis.ingredients.ingredient.Ingredient
Ingredient
opacity
mask
mask_pixels
season
#   class Quantize(pyrogis.ingredients.ingredient.Ingredient):
View Source
class Quantize(Ingredient):
    """
    quantize reduces the color palette of the input pixels to a smaller set.
    """

    def prep(self,
             colors=None, **kwargs):
        """
        parameters for spatial color quantization

        :param colors: colors to use. can be a list of str
        or pixel array likes
        """

        if colors is None:
            colors = np.asarray([[]])
        elif type(colors) is list:
            rgb_colors = []
            for color in colors:
                if type(color) is str:
                    if color[0] != '#':
                        color = '#' + color
                    rgb_colors.append(ImageColor.getcolor(color, "RGB"))

            colors = np.asarray(rgb_colors)

        else:
            colors = np.array(colors)

        self.palette = colors.astype(np.dtype('uint8'))
        """palette to use"""

    def cook(self, pixels: np.ndarray):
        """
        get the closest rgb color in the palette to each pixel rgb
        "snap" to the colors in the palette
        """
        # pixels -> (width, height, 1, 3)
        # palette -> (1, 1, n, 3)
        # subtract -> (width, height, n, 3)

        # the difference between pixel r, g, b (3) and color
        # for each pixel (width, height),
        # for each color in the palette (n, 3)
        differences = (
                np.expand_dims(pixels, axis=2)
                - np.expand_dims(self.palette, axis=(0, 1))
        )

        # sum up the last axis (r + g + b)
        # and sqrt that sum
        # -> (width, height, n)
        distances = np.sqrt(np.sum(differences ** 2, axis=3))

        # get the minimum among the n color
        # smallest value in each n group last dimension (smallest sqrt sum)
        # -> (width, height, 1)
        nearest_palette_index = np.argmin(distances, axis=2)

        # replace the min index identified with the corresponding color
        # -> (width, height, 3)
        return self.palette[nearest_palette_index]

quantize reduces the color palette of the input pixels to a smaller set.

#   def prep(self, colors=None, **kwargs):
View Source
    def prep(self,
             colors=None, **kwargs):
        """
        parameters for spatial color quantization

        :param colors: colors to use. can be a list of str
        or pixel array likes
        """

        if colors is None:
            colors = np.asarray([[]])
        elif type(colors) is list:
            rgb_colors = []
            for color in colors:
                if type(color) is str:
                    if color[0] != '#':
                        color = '#' + color
                    rgb_colors.append(ImageColor.getcolor(color, "RGB"))

            colors = np.asarray(rgb_colors)

        else:
            colors = np.array(colors)

        self.palette = colors.astype(np.dtype('uint8'))
        """palette to use"""

parameters for spatial color quantization

:param colors: colors to use. can be a list of str or pixel array likes

#   def cook(self, pixels: numpy.ndarray):
View Source
    def cook(self, pixels: np.ndarray):
        """
        get the closest rgb color in the palette to each pixel rgb
        "snap" to the colors in the palette
        """
        # pixels -> (width, height, 1, 3)
        # palette -> (1, 1, n, 3)
        # subtract -> (width, height, n, 3)

        # the difference between pixel r, g, b (3) and color
        # for each pixel (width, height),
        # for each color in the palette (n, 3)
        differences = (
                np.expand_dims(pixels, axis=2)
                - np.expand_dims(self.palette, axis=(0, 1))
        )

        # sum up the last axis (r + g + b)
        # and sqrt that sum
        # -> (width, height, n)
        distances = np.sqrt(np.sum(differences ** 2, axis=3))

        # get the minimum among the n color
        # smallest value in each n group last dimension (smallest sqrt sum)
        # -> (width, height, 1)
        nearest_palette_index = np.argmin(distances, axis=2)

        # replace the min index identified with the corresponding color
        # -> (width, height, 3)
        return self.palette[nearest_palette_index]

get the closest rgb color in the palette to each pixel rgb "snap" to the colors in the palette

Inherited Members
pyrogis.ingredients.ingredient.Ingredient
Ingredient
opacity
mask
mask_pixels
season
#   class SpatialQuantize(pyrogis.ingredients.quantize.Quantize):
View Source
class SpatialQuantize(Quantize):
    """
    use the Spatial Color Quantization algorithm
    implemented in rust with rscolorq

    also performs dithering to make the palette appear richer.
    """
    PALETTE_SIZE = 8
    ITERATIONS = 3
    REPEATS = 1
    INITIAL_TEMP = 1
    FINAL_TEMP = .001
    FILTER_SIZE = 3
    DITHERING_LEVEL = .8

    palette_size: int
    """number of colors"""
    iterations: int
    """number of iterations to do at each coarseness level"""
    repeats: int
    """number of repeats to do of each annealing temp"""
    initial_temp: float
    """starting annealing temp (around 1)"""
    final_temp: float
    """final annealing temp (decimal near but above 0)"""
    filter_size: int
    """filter size for dithering"""
    dithering_level: float
    """relative amount of dithering (.5-1.5)"""
    seed: int
    """seed for rng"""

    def prep(
            self, palette_size=PALETTE_SIZE,
            iterations=ITERATIONS, repeats=REPEATS,
            initial_temp=INITIAL_TEMP, final_temp=FINAL_TEMP,
            dithering_level=DITHERING_LEVEL, seed=0, **kwargs
    ):
        """

        """
        super().prep(**kwargs)

        self.palette_size = palette_size
        """number of colors"""
        self.iterations = iterations
        """number of iterations to do at each coarseness level"""
        self.repeats = repeats
        """number of repeats to do of each annealing temp"""
        self.initial_temp = initial_temp
        """starting annealing temp (around 1)"""
        self.final_temp = final_temp
        """final annealing temp (decimal near but above 0)"""
        self.filter_size = self.FILTER_SIZE
        self.dithering_level = dithering_level
        """relative amount of dithering (.5-1.5)"""
        self.seed = seed
        """seed for rng"""

    def cook(self, pixels: np.ndarray):
        """
        use the binding to the rscolorq package in rust
        to perform an optimization in quantizing and dithering
        """

        # rotating and unrotating because different orientation is expected
        cooked_pixels = np.rot90(algorithms.quantize(
            np.ascontiguousarray(np.rot90(pixels), dtype=np.dtype('uint8')),
            self.palette,
            palette_size=self.palette_size,
            iters_per_level=self.iterations,
            repeats_per_temp=self.repeats,
            initial_temp=self.initial_temp,
            final_temp=self.final_temp,
            filter_size=self.filter_size,
            dithering_level=self.dithering_level,
            seed=self.seed
        ), axes=(1, 0))

        return cooked_pixels

use the Spatial Color Quantization algorithm implemented in rust with rscolorq

also performs dithering to make the palette appear richer.

#   PALETTE_SIZE = 8
#   ITERATIONS = 3
#   REPEATS = 1
#   INITIAL_TEMP = 1
#   FINAL_TEMP = 0.001
#   FILTER_SIZE = 3
#   DITHERING_LEVEL = 0.8
#   palette_size: int

number of colors

#   iterations: int

number of iterations to do at each coarseness level

#   repeats: int

number of repeats to do of each annealing temp

#   initial_temp: float

starting annealing temp (around 1)

#   final_temp: float

final annealing temp (decimal near but above 0)

#   filter_size: int

filter size for dithering

#   dithering_level: float

relative amount of dithering (.5-1.5)

#   seed: int

seed for rng

#   def prep( self, palette_size=8, iterations=3, repeats=1, initial_temp=1, final_temp=0.001, dithering_level=0.8, seed=0, **kwargs ):
View Source
    def prep(
            self, palette_size=PALETTE_SIZE,
            iterations=ITERATIONS, repeats=REPEATS,
            initial_temp=INITIAL_TEMP, final_temp=FINAL_TEMP,
            dithering_level=DITHERING_LEVEL, seed=0, **kwargs
    ):
        """

        """
        super().prep(**kwargs)

        self.palette_size = palette_size
        """number of colors"""
        self.iterations = iterations
        """number of iterations to do at each coarseness level"""
        self.repeats = repeats
        """number of repeats to do of each annealing temp"""
        self.initial_temp = initial_temp
        """starting annealing temp (around 1)"""
        self.final_temp = final_temp
        """final annealing temp (decimal near but above 0)"""
        self.filter_size = self.FILTER_SIZE
        self.dithering_level = dithering_level
        """relative amount of dithering (.5-1.5)"""
        self.seed = seed
        """seed for rng"""
#   def cook(self, pixels: numpy.ndarray):
View Source
    def cook(self, pixels: np.ndarray):
        """
        use the binding to the rscolorq package in rust
        to perform an optimization in quantizing and dithering
        """

        # rotating and unrotating because different orientation is expected
        cooked_pixels = np.rot90(algorithms.quantize(
            np.ascontiguousarray(np.rot90(pixels), dtype=np.dtype('uint8')),
            self.palette,
            palette_size=self.palette_size,
            iters_per_level=self.iterations,
            repeats_per_temp=self.repeats,
            initial_temp=self.initial_temp,
            final_temp=self.final_temp,
            filter_size=self.filter_size,
            dithering_level=self.dithering_level,
            seed=self.seed
        ), axes=(1, 0))

        return cooked_pixels

use the binding to the rscolorq package in rust to perform an optimization in quantizing and dithering

Inherited Members
pyrogis.ingredients.ingredient.Ingredient
Ingredient
opacity
mask
mask_pixels
season
#   class Sort(pyrogis.ingredients.ingredient.Ingredient):
View Source
class Sort(Ingredient):
    """
    sort a pixel array

    uses its mask to determine which groups of pixels to sort
    (white pixels get sorted)

    can use a seasoning to create that mask when cooking,
    or have it preloaded using a season method
    """

    delimiter: np.ndarray
    """pixel used as the sort subgroup delimiter"""
    rotate: Rotate
    """define the direction to rotate on"""

    def prep(
            self, rotate: Rotate = None,
            delimiter: np.ndarray = np.asarray([255, 255, 255]),
            **kwargs
    ):
        """
        extra kwargs get passed to the Rotate if one is not provided
        """
        self.delimiter = delimiter

        if rotate is None:
            rotate = Rotate(turns=0)

        self.rotate = rotate

    def cook(self, pixels: np.ndarray):
        """
        cook sorts from bottom to top after rotation, then unrotates.
        sort within each sequence group of contiguous white pixels
        in the mask (may be all white)
        """
        # rotate self.mask and pixels to correspond to self.angle
        rotate = self.rotate

        mask = self.mask_pixels(pixels)

        rotated_mask = rotate.cook(mask)
        rotated_pixels = rotate.cook(pixels)

        # false indicates that the pixel should not be sorted
        boolean_array = np.all(rotated_mask == self._white_pixel, axis=2)

        sorted_pixels = rotated_pixels
        # loop through one axis
        for i in range(rotated_pixels.shape[0]):
            # get that axis
            axis = rotated_pixels[i]
            # and the axis for the mask-truth
            boolean_axis = boolean_array[i]
            # get the indices for this row on the mask that are false
            masked_indices_axis = np.nonzero(np.invert(boolean_axis))[0]
            # split up the axis into sub groups at indices where mask is black (false)
            sort_groups = np.split(axis, masked_indices_axis)

            sorted_groups = []
            # loop through the groups
            for group in sort_groups:
                # np.sort(group)
                # if the subgroup to be sorted contains 0 or 1 pixels, ignore
                if group.size > 3:
                    # intensity as the sorting criterion
                    intensities = np.sum(
                        group * np.asarray([0.299, 0.587, 0.114]), axis=1
                    )
                    # get "sort order" indices of the intensities of this group
                    indices = np.argsort(intensities)
                    # sort the group by these indices
                    group = group[indices]
                sorted_groups.append(group)

            # concatenate the row back together, sorted in the mask
            sorted_pixels[i] = np.concatenate(sorted_groups)

        # unrotate sorted_pixels to return to correct orientation
        unrotate = Rotate.unrotate(rotate)
        sorted_pixels = unrotate.cook(sorted_pixels)

        return sorted_pixels

sort a pixel array

uses its mask to determine which groups of pixels to sort (white pixels get sorted)

can use a seasoning to create that mask when cooking, or have it preloaded using a season method

#   delimiter: numpy.ndarray

pixel used as the sort subgroup delimiter

define the direction to rotate on

#   def prep( self, rotate: pyrogis.ingredients.rotate.Rotate = None, delimiter: numpy.ndarray = array([255, 255, 255]), **kwargs ):
View Source
    def prep(
            self, rotate: Rotate = None,
            delimiter: np.ndarray = np.asarray([255, 255, 255]),
            **kwargs
    ):
        """
        extra kwargs get passed to the Rotate if one is not provided
        """
        self.delimiter = delimiter

        if rotate is None:
            rotate = Rotate(turns=0)

        self.rotate = rotate

extra kwargs get passed to the Rotate if one is not provided

#   def cook(self, pixels: numpy.ndarray):
View Source
    def cook(self, pixels: np.ndarray):
        """
        cook sorts from bottom to top after rotation, then unrotates.
        sort within each sequence group of contiguous white pixels
        in the mask (may be all white)
        """
        # rotate self.mask and pixels to correspond to self.angle
        rotate = self.rotate

        mask = self.mask_pixels(pixels)

        rotated_mask = rotate.cook(mask)
        rotated_pixels = rotate.cook(pixels)

        # false indicates that the pixel should not be sorted
        boolean_array = np.all(rotated_mask == self._white_pixel, axis=2)

        sorted_pixels = rotated_pixels
        # loop through one axis
        for i in range(rotated_pixels.shape[0]):
            # get that axis
            axis = rotated_pixels[i]
            # and the axis for the mask-truth
            boolean_axis = boolean_array[i]
            # get the indices for this row on the mask that are false
            masked_indices_axis = np.nonzero(np.invert(boolean_axis))[0]
            # split up the axis into sub groups at indices where mask is black (false)
            sort_groups = np.split(axis, masked_indices_axis)

            sorted_groups = []
            # loop through the groups
            for group in sort_groups:
                # np.sort(group)
                # if the subgroup to be sorted contains 0 or 1 pixels, ignore
                if group.size > 3:
                    # intensity as the sorting criterion
                    intensities = np.sum(
                        group * np.asarray([0.299, 0.587, 0.114]), axis=1
                    )
                    # get "sort order" indices of the intensities of this group
                    indices = np.argsort(intensities)
                    # sort the group by these indices
                    group = group[indices]
                sorted_groups.append(group)

            # concatenate the row back together, sorted in the mask
            sorted_pixels[i] = np.concatenate(sorted_groups)

        # unrotate sorted_pixels to return to correct orientation
        unrotate = Rotate.unrotate(rotate)
        sorted_pixels = unrotate.cook(sorted_pixels)

        return sorted_pixels

cook sorts from bottom to top after rotation, then unrotates. sort within each sequence group of contiguous white pixels in the mask (may be all white)

Inherited Members
pyrogis.ingredients.ingredient.Ingredient
Ingredient
opacity
mask
mask_pixels
season
#   class Threshold(pyrogis.ingredients.seasonings.seasoning.Seasoning):
View Source
class Threshold(Seasoning):
    """
    a seasoning that compares the brightness value of each pixel
    in the :param target pixel array.

    when used in a mix, the threshold will target the pixel array below it
    if it has not been initialized with target.

    as it is a subclass of seasoning,
    a Threshold instance has a season method to work with or without a target
    """

    LOWER_THRESHOLD = 64
    UPPER_THRESHOLD = 180

    lower_threshold: int
    """pixels below are `True`"""
    upper_threshold: int
    """pixels above are `True`"""

    def prep(
            self,
            lower_threshold: int = None,
            upper_threshold: int = None,
            **kwargs
    ):
        """
        set the threshold intensity levels

        calls Seasoning.prep with leftover kwargs

        pixels lower than lower_threshold
        or higher that upper_threshold
        are true (include_pixel)
        """
        # set include/exclude_pixel and target, if provided
        super().prep(**kwargs)

        if lower_threshold is None and upper_threshold is None:
            lower_threshold = self.LOWER_THRESHOLD
            upper_threshold = self.UPPER_THRESHOLD

        elif lower_threshold is None:
            lower_threshold = 0

        elif upper_threshold is None:
            upper_threshold = 255

        self.lower_threshold = lower_threshold
        self.upper_threshold = upper_threshold

    def cook(self, pixels: np.ndarray):
        """
        pixels with brightness >= upper_threshold
        or <= lower_threshold are replaced by include pixel (white)

        brightness = r * 0.299 + g * 0.587 + b * 0.114

        parallel computation in rust is 10x speedup
        """
        cooked_pixels = pixels.copy()

        # cook using the rust function
        cooked_pixels = algorithms.threshold(
            cooked_pixels.astype(np.dtype('uint8')),
            self.lower_threshold, self.upper_threshold,
            self.include_pixel.astype(np.dtype('uint8')),
            self.exclude_pixel.astype(np.dtype('uint8'))
        )

        return cooked_pixels

    def cook_np(self, pixels: np.ndarray):
        """
        perform the same operation as Threshold.cook, but only in numpy
        """

        include_pixels = np.resize(self.include_pixel, pixels.shape)
        exclude_pixels = np.resize(self.exclude_pixel, pixels.shape)

        # use exclude_pixels as the base
        cooked_pixels = exclude_pixels
        # get intensities from average of rgb
        intensities_array = np.sum(
            pixels * np.asarray([0.299, 0.587, 0.114]), axis=2
        )
        # if intensity <= lower or >= upper, True
        boolean_array = np.logical_or(
            intensities_array >= self.upper_threshold,
            intensities_array <= self.lower_threshold
        )

        # set True values in boolean_array to include_pixel
        cooked_pixels[boolean_array] = include_pixels[boolean_array]

        return cooked_pixels

a seasoning that compares the brightness value of each pixel in the :param target pixel array.

when used in a mix, the threshold will target the pixel array below it if it has not been initialized with target.

as it is a subclass of seasoning, a Threshold instance has a season method to work with or without a target

#   LOWER_THRESHOLD = 64
#   UPPER_THRESHOLD = 180
#   lower_threshold: int

pixels below are True

#   upper_threshold: int

pixels above are True

#   def prep( self, lower_threshold: int = None, upper_threshold: int = None, **kwargs ):
View Source
    def prep(
            self,
            lower_threshold: int = None,
            upper_threshold: int = None,
            **kwargs
    ):
        """
        set the threshold intensity levels

        calls Seasoning.prep with leftover kwargs

        pixels lower than lower_threshold
        or higher that upper_threshold
        are true (include_pixel)
        """
        # set include/exclude_pixel and target, if provided
        super().prep(**kwargs)

        if lower_threshold is None and upper_threshold is None:
            lower_threshold = self.LOWER_THRESHOLD
            upper_threshold = self.UPPER_THRESHOLD

        elif lower_threshold is None:
            lower_threshold = 0

        elif upper_threshold is None:
            upper_threshold = 255

        self.lower_threshold = lower_threshold
        self.upper_threshold = upper_threshold

set the threshold intensity levels

calls Seasoning.prep with leftover kwargs

pixels lower than lower_threshold or higher that upper_threshold are true (include_pixel)

#   def cook(self, pixels: numpy.ndarray):
View Source
    def cook(self, pixels: np.ndarray):
        """
        pixels with brightness >= upper_threshold
        or <= lower_threshold are replaced by include pixel (white)

        brightness = r * 0.299 + g * 0.587 + b * 0.114

        parallel computation in rust is 10x speedup
        """
        cooked_pixels = pixels.copy()

        # cook using the rust function
        cooked_pixels = algorithms.threshold(
            cooked_pixels.astype(np.dtype('uint8')),
            self.lower_threshold, self.upper_threshold,
            self.include_pixel.astype(np.dtype('uint8')),
            self.exclude_pixel.astype(np.dtype('uint8'))
        )

        return cooked_pixels

pixels with brightness >= upper_threshold or <= lower_threshold are replaced by include pixel (white)

brightness = r * 0.299 + g * 0.587 + b * 0.114

parallel computation in rust is 10x speedup

#   def cook_np(self, pixels: numpy.ndarray):
View Source
    def cook_np(self, pixels: np.ndarray):
        """
        perform the same operation as Threshold.cook, but only in numpy
        """

        include_pixels = np.resize(self.include_pixel, pixels.shape)
        exclude_pixels = np.resize(self.exclude_pixel, pixels.shape)

        # use exclude_pixels as the base
        cooked_pixels = exclude_pixels
        # get intensities from average of rgb
        intensities_array = np.sum(
            pixels * np.asarray([0.299, 0.587, 0.114]), axis=2
        )
        # if intensity <= lower or >= upper, True
        boolean_array = np.logical_or(
            intensities_array >= self.upper_threshold,
            intensities_array <= self.lower_threshold
        )

        # set True values in boolean_array to include_pixel
        cooked_pixels[boolean_array] = include_pixels[boolean_array]

        return cooked_pixels

perform the same operation as Threshold.cook, but only in numpy

Inherited Members
pyrogis.ingredients.ingredient.Ingredient
Ingredient
opacity
mask
mask_pixels
season
#   class Chef:
View Source
class Chef:
    """
    handles text and json representations of pierogis constructs

    implements parsing into a standard representation
    and cooking a parsed representation
    """
    ingredient_classes = {
        'sort': Sort,
        'quantize': SpatialQuantize,
        'threshold': Threshold,
        'rotate': Rotate,
        'resize': Resize
    }

    menu = menu

    def create_pierogi_objects(self, pierogi_descs: dict, file_links: dict):
        pierogi_objects = {}

        for pierogi_key, pierogi_desc in pierogi_descs.items():
            file = file_links[pierogi_desc.files_key]
            pierogi = Pierogi(file=file)

            pierogi_objects[pierogi_key] = pierogi

        return pierogi_objects

    def create_ingredient_objects(
            self,
            ingredient_descs: dict,
            pierogis: dict
    ):
        """
        create a dict of uuid->Ingredient
        from an ingredient description and a lookup for file paths

        :param ingredient_descs: map of uuid keys to inner dict
        with type, args, and kwargs keys
        :param file_links: map of uuid keys to file paths,
        usually for Pierogi
        """
        ingredients: Dict[str, Ingredient] = {}

        for ingredient_name, ingredient_desc in ingredient_descs.items():
            # if path is one of the kwargs
            # look it up in the linking paths dictionary
            pierogi_name = ingredient_desc.kwargs.get('pierogi')
            if pierogi_name is not None:
                pierogi = pierogis[pierogi_name]
                ingredient_desc.kwargs['pierogi'] = pierogi

            ingredient = self.get_or_create_ingredient(
                ingredients, ingredient_descs, ingredient_name
            )

            ingredients[ingredient_name] = ingredient

        return ingredients

    def get_or_create_ingredient(
            self,
            ingredients: dict,
            ingredient_descs: dict,
            ingredient_name
    ):
        """
        look to see if an ingredient object has already been created
        otherwise create it and swap it in the dictionary
        """
        ingredient = ingredients.get(ingredient_name)

        if ingredient is None:
            ingredient_desc = ingredient_descs[ingredient_name]

            # search kwargs for keys that are type names
            # swap the value of that kwarg (reference key)
            # for the created ingredient obj
            for ingredient_type_name in self.ingredient_classes.keys():
                ingredient_name = ingredient_desc.kwargs.get(
                    ingredient_type_name
                )

                if ingredient_name is not None:
                    ingredient = self.get_or_create_ingredient(
                        ingredients, ingredient_descs, ingredient_name
                    )
                    ingredient_desc.kwargs[ingredient_type_name] = ingredient

            # now create an ingredient as specified in the description
            ingredient = ingredient_desc.create(self.ingredient_classes)

        return ingredient

    def apply_seasonings(self, ingredients, seasoning_links):
        for seasoning, recipient in seasoning_links.items():
            seasoning = ingredients[seasoning]
            recipient = ingredients[recipient]

            recipient.season(seasoning)

    def create_recipe_object(
            self,
            ingredients: Dict[str, Ingredient],
            recipe_order: List[str]
    ):
        """
        create a recipe from:
        ingredients map
        seasoning name map
        order of ingredients

        :param ingredients: map of uuid key to ingredient object value
        :param seasoning_links: map of "recipient ingredient" uuid keys
        to seasoning uuid values
        :param recipe_order: order of ingredients by uuid
        """

        recipe = Recipe(ingredients=[])
        # loop through the ingredient keys specified by the recipe
        for ingredient_key in recipe_order:
            # there might be a bug here with the ingredient being out
            # of sync with another reference to that ingredient
            ingredient = ingredients[ingredient_key]

            # add this created ingredient to the dish recipe for return
            recipe.add(ingredient)

        return recipe

    def cook_dish_desc(self, dish_description: DishDescription):
        """
        cook a dish from a series of descriptive dicts
        """
        pierogi_descs = dish_description.pierogis
        ingredient_descs = dish_description.ingredients
        files = dish_description.files
        dish_kwargs = dish_description.dish
        seasoning_links = dish_description.seasoning_links

        pierogis = self.create_pierogi_objects(
            pierogi_descs,
            files
        )

        ingredients = self.create_ingredient_objects(
            ingredient_descs,
            pierogis
        )

        self.apply_seasonings(
            ingredients,
            seasoning_links
        )

        recipe = self.create_recipe_object(
            ingredients,
            dish_kwargs['recipe']
        )

        dish = Dish(
            pierogis=[pierogis[dish_kwargs['pierogi']]],
            recipe=recipe
        )

        return dish.serve()

handles text and json representations of pierogis constructs

implements parsing into a standard representation and cooking a parsed representation

#   Chef()
#   ingredient_classes = {'sort': <class 'pyrogis.ingredients.sort.Sort'>, 'quantize': <class 'pyrogis.ingredients.quantize.SpatialQuantize'>, 'threshold': <class 'pyrogis.ingredients.seasonings.threshold.Threshold'>, 'rotate': <class 'pyrogis.ingredients.rotate.Rotate'>, 'resize': <class 'pyrogis.ingredients.resize.Resize'>}
#   def create_pierogi_objects(self, pierogi_descs: dict, file_links: dict):
View Source
    def create_pierogi_objects(self, pierogi_descs: dict, file_links: dict):
        pierogi_objects = {}

        for pierogi_key, pierogi_desc in pierogi_descs.items():
            file = file_links[pierogi_desc.files_key]
            pierogi = Pierogi(file=file)

            pierogi_objects[pierogi_key] = pierogi

        return pierogi_objects
#   def create_ingredient_objects(self, ingredient_descs: dict, pierogis: dict):
View Source
    def create_ingredient_objects(
            self,
            ingredient_descs: dict,
            pierogis: dict
    ):
        """
        create a dict of uuid->Ingredient
        from an ingredient description and a lookup for file paths

        :param ingredient_descs: map of uuid keys to inner dict
        with type, args, and kwargs keys
        :param file_links: map of uuid keys to file paths,
        usually for Pierogi
        """
        ingredients: Dict[str, Ingredient] = {}

        for ingredient_name, ingredient_desc in ingredient_descs.items():
            # if path is one of the kwargs
            # look it up in the linking paths dictionary
            pierogi_name = ingredient_desc.kwargs.get('pierogi')
            if pierogi_name is not None:
                pierogi = pierogis[pierogi_name]
                ingredient_desc.kwargs['pierogi'] = pierogi

            ingredient = self.get_or_create_ingredient(
                ingredients, ingredient_descs, ingredient_name
            )

            ingredients[ingredient_name] = ingredient

        return ingredients

create a dict of uuid->Ingredient from an ingredient description and a lookup for file paths

:param ingredient_descs: map of uuid keys to inner dict with type, args, and kwargs keys :param file_links: map of uuid keys to file paths, usually for Pierogi

#   def get_or_create_ingredient(self, ingredients: dict, ingredient_descs: dict, ingredient_name):
View Source
    def get_or_create_ingredient(
            self,
            ingredients: dict,
            ingredient_descs: dict,
            ingredient_name
    ):
        """
        look to see if an ingredient object has already been created
        otherwise create it and swap it in the dictionary
        """
        ingredient = ingredients.get(ingredient_name)

        if ingredient is None:
            ingredient_desc = ingredient_descs[ingredient_name]

            # search kwargs for keys that are type names
            # swap the value of that kwarg (reference key)
            # for the created ingredient obj
            for ingredient_type_name in self.ingredient_classes.keys():
                ingredient_name = ingredient_desc.kwargs.get(
                    ingredient_type_name
                )

                if ingredient_name is not None:
                    ingredient = self.get_or_create_ingredient(
                        ingredients, ingredient_descs, ingredient_name
                    )
                    ingredient_desc.kwargs[ingredient_type_name] = ingredient

            # now create an ingredient as specified in the description
            ingredient = ingredient_desc.create(self.ingredient_classes)

        return ingredient

look to see if an ingredient object has already been created otherwise create it and swap it in the dictionary

#   def apply_seasonings(self, ingredients, seasoning_links):
View Source
    def apply_seasonings(self, ingredients, seasoning_links):
        for seasoning, recipient in seasoning_links.items():
            seasoning = ingredients[seasoning]
            recipient = ingredients[recipient]

            recipient.season(seasoning)
#   def create_recipe_object( self, ingredients: Dict[str, pyrogis.ingredients.ingredient.Ingredient], recipe_order: List[str] ):
View Source
    def create_recipe_object(
            self,
            ingredients: Dict[str, Ingredient],
            recipe_order: List[str]
    ):
        """
        create a recipe from:
        ingredients map
        seasoning name map
        order of ingredients

        :param ingredients: map of uuid key to ingredient object value
        :param seasoning_links: map of "recipient ingredient" uuid keys
        to seasoning uuid values
        :param recipe_order: order of ingredients by uuid
        """

        recipe = Recipe(ingredients=[])
        # loop through the ingredient keys specified by the recipe
        for ingredient_key in recipe_order:
            # there might be a bug here with the ingredient being out
            # of sync with another reference to that ingredient
            ingredient = ingredients[ingredient_key]

            # add this created ingredient to the dish recipe for return
            recipe.add(ingredient)

        return recipe

create a recipe from: ingredients map seasoning name map order of ingredients

:param ingredients: map of uuid key to ingredient object value :param seasoning_links: map of "recipient ingredient" uuid keys to seasoning uuid values :param recipe_order: order of ingredients by uuid

#   def cook_dish_desc( self, dish_description: pyrogis.chef.dish_description.DishDescription ):
View Source
    def cook_dish_desc(self, dish_description: DishDescription):
        """
        cook a dish from a series of descriptive dicts
        """
        pierogi_descs = dish_description.pierogis
        ingredient_descs = dish_description.ingredients
        files = dish_description.files
        dish_kwargs = dish_description.dish
        seasoning_links = dish_description.seasoning_links

        pierogis = self.create_pierogi_objects(
            pierogi_descs,
            files
        )

        ingredients = self.create_ingredient_objects(
            ingredient_descs,
            pierogis
        )

        self.apply_seasonings(
            ingredients,
            seasoning_links
        )

        recipe = self.create_recipe_object(
            ingredients,
            dish_kwargs['recipe']
        )

        dish = Dish(
            pierogis=[pierogis[dish_kwargs['pierogi']]],
            recipe=recipe
        )

        return dish.serve()

cook a dish from a series of descriptive dicts