pyrogis
ingredients
"
pierogisis the name of the framework;
pyrogisis 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' ]
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
default resize algorithm is nearest neighbor
underlying numpy pixels array
turn the numpy array into a PIL Image
1st dimension of the underlying pixel array
2nd dimension of the underlying pixel array
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
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
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
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
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
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
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
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
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
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
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
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 when overlaying on previous output
opacity when overlaying
View Source
def prep(self, **kwargs) -> None: """ parameterize the cook function """ pass
parameterize the cook function
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
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
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
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
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
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
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.
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
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
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.
number of colors
number of iterations to do at each coarseness level
number of repeats to do of each annealing temp
starting annealing temp (around 1)
final annealing temp (decimal near but above 0)
filter size for dithering
relative amount of dithering (.5-1.5)
seed for rng
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"""
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
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
pixel used as the sort subgroup delimiter
define the direction to rotate on
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
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
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
pixels below are True
pixels above are True
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)
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
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
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
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
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
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
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)
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
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