ingredients

from pierogis.ingredients 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 through Ingredient() and cook obfuscated in the typical usage flow. prep can be seen as parameterizing the manipulation while cook applies it (to an array).

Here are some examples of Ingredient subtypes.

pierogi

Ingredient 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 by 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, a 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, pierogi=pierogi)
cooked_dish = dish.serve()

The recipe gets cooked sequentially for each pierogi in pierogis. The output cooked_dish has pierogi member set with cooked pixels.

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()
threshold = Threshold()

# season sort with threshold
sort.season(threshold)

cook() outputs a black and white array. That’s what makes it a seasoning. 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 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 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.