python-numpy-image-manipulation / py_image_manipulation / transformations.py
transformations.py
Raw
from py_image_manipulation.effect import ImageEffect
import numpy as np


class NoEffect(ImageEffect):
    def apply(pixels: np.ndarray) -> np.ndarray:
        return pixels


class Invert(ImageEffect):
    def apply(pixels: np.ndarray) -> np.ndarray:
        # NOTE: The following subtracts each element in the matrix from
        # 255 This is much faster than looping through the array because
        # numpy uses vectorized operations to accelerate the computation
        return 255 - pixels


class NoRed(ImageEffect):
    def apply(pixels: np.ndarray) -> np.ndarray:
        # To apply the no red filter, we are going to perform a single
        # numpy operation to set all red pixels to value 0 (i.e. converting
        # them to black-colored pixels)
        pixels[:, :, 0] = 0
        return pixels

class NoGreen(ImageEffect):
    def apply(pixels: np.ndarray) -> np.ndarray:
        # To apply the no green filter, we are going to perform a single
        # numpy operation to set all green pixels to value 0 (i.e. converting
        # them to black-colored pixels)
        pixels[:, :, 1] = 0
        return pixels

class NoBlue(ImageEffect):
    def apply(pixels: np.ndarray) -> np.ndarray:
        # To apply the no blue filter, we are going to perform a single
        # numpy operation to set all blue pixels to value 0 (i.e. converting
        # them to black-colored pixels)
        pixels[:, :, 2] = 0
        return pixels

class RedOnly(ImageEffect):
    def apply(pixels: np.ndarray) -> np.ndarray:
        # To apply the red-only filter, we are going to perform two numpy
        # operations to set green and blue pixels to 0
        pixels[:, :, 1] = 0
        pixels[:, :, 2] = 0
        return pixels

class GreenOnly(ImageEffect):
    def apply(pixels: np.ndarray) -> np.ndarray:
        # To apply the green-only filter, we are going to perform two numpy
        # operations to set red and blue pixels to 0
        pixels[:, :, 0] = 0
        pixels[:, :, 2] = 0
        return pixels

class BlueOnly(ImageEffect):
    def apply(pixels: np.ndarray) -> np.ndarray:
        # To apply the blue-only filter, we are going to perform two numpy
        # operations to set red and green pixels to 0
        pixels[:, :, 0] = 0
        pixels[:, :, 1] = 0
        return pixels

class Grayscale(ImageEffect):
    def apply(pixels: np.ndarray) -> np.ndarray:
        # To apply the grayscale filter, calculate the mean value of each pixel's
        # R,G & B values (i.e. (R+G+B)/3) stored in an array of means with shape (h, w).
        # Then we follow this by restoring the shape of "pixels" at (h, w, 3)
        pixels = np.mean(pixels, axis=2)
        pixels = np.repeat(pixels[:, :, np.newaxis], repeats=3, axis=2)
        return pixels

class Noiseify(ImageEffect):
    def apply(pixels: np.ndarray) -> np.ndarray:
        # Step 1: generate a random integer noise array with the same shape as "pixels"
        noise_pixels = np.random.randint(-5, 5, np.shape(pixels))
        # Step 2: add the noise vector to "pixels" & prevent any RGB value from going out
        # of the range 0-255 by using minimum & maximum functions
        return np.maximum(np.minimum(pixels + noise_pixels, 255), 0)

class Smooth(ImageEffect):
    def apply(pixels: np.ndarray) -> np.ndarray:
        # Step 1: store 1 array equal to "pixels" so we can use it instead
        # to retain "pixels" value as an input throughout the run. We define another 8 arrays
        # with every possible neighboring node through shifting
        # the elements of "pixels" in all possible directions using np.roll().
        pixels_temp = np.copy(pixels)
        pixels_right_neighbors = np.roll(pixels, -1,axis=1)
        pixels_left_neighbors = np.roll(pixels, 1, axis=1)
        pixels_upper_neighbors = np.roll(pixels, 1, axis=0)
        pixels_lower_neighbors = np.roll(pixels, -1, axis=0)
        pixels_upperright_neighbors = np.roll(pixels, (1, -1), axis=(0, 1))
        pixels_upperleft_neighbors = np.roll(pixels, (1, 1), axis=(0, 1))
        pixels_lowerright_neighbors = np.roll(pixels, (-1, -1), axis=(0, 1))
        pixels_lowerleft_neighbors = np.roll(pixels, (-1, 1), axis=(0, 1))
        # Step 2: Perform the necessary mean calculations
        # Step 2.1: Calculate the mean for the middle nodes (taking into consideration the 8 neighbors)
        pixels_temp[1:-1, 1:-1] = (pixels[1:-1, 1:-1] + pixels_upper_neighbors[1:-1, 1:-1] + pixels_lower_neighbors[1:-1, 1:-1]
                                   + pixels_right_neighbors[1:-1, 1:-1] + pixels_left_neighbors[1:-1,1:-1] + pixels_upperleft_neighbors[1:-1, 1:-1]
                                   + pixels_upperright_neighbors[1:-1, 1:-1] + pixels_lowerleft_neighbors[1:-1, 1:-1]+ pixels_lowerright_neighbors[1:-1, 1:-1]) / 9
        # Step 2.2: Calculate the mean of the corner nodes (taking into consideration only 3 neighbors)
        pixels_temp[0, 0] = (pixels[0, 0] + pixels_right_neighbors[0, 0] + pixels_lowerright_neighbors[0, 0] +pixels_lower_neighbors[0, 0]) / 4
        pixels_temp[0, -1] = (pixels[0, -1] + pixels_left_neighbors[0, -1] + pixels_lowerleft_neighbors[0, -1] +pixels_lower_neighbors[0, -1]) / 4
        pixels_temp[-1, 0] = (pixels[-1, 0] + pixels_right_neighbors[-1, 0] + pixels_upperright_neighbors[-1, 0] +pixels_upper_neighbors[-1, 0]) / 4
        pixels_temp[-1, -1] = (pixels[-1, -1] + pixels_left_neighbors[-1, -1] + pixels_upperleft_neighbors[-1, -1] +pixels_upper_neighbors[-1, -1]) / 4
        # Step 2.3: Calculate the mean of the edge nodes (taking into consideration only 5 neighbors)
        pixels_temp[1:-1, 0] = (pixels[1:-1, 0] + pixels_upper_neighbors[1:-1, 0] + pixels_upperright_neighbors[1:-1, 0]+ pixels_right_neighbors[1:-1, 0]
                                + pixels_lowerright_neighbors[1:-1,0] + pixels_lower_neighbors[1:-1,0]) / 6  # Left edge nodes
        pixels_temp[0, 1:-1] = (pixels[0, 1:-1] + pixels_left_neighbors[0, 1:-1] + pixels_lowerleft_neighbors[0, 1:-1]+ pixels_lower_neighbors[0, 1:-1]
                                + pixels_lowerright_neighbors[0,1:-1] + pixels_right_neighbors[0,1:-1]) / 6  # Top edge nodes
        pixels_temp[1:-1, -1] = (pixels[1:-1, -1] + pixels_upper_neighbors[1:-1, -1] + pixels_upperleft_neighbors[1:-1,-1]+ pixels_left_neighbors[1:-1, -1]
                                 + pixels_lowerleft_neighbors[1:-1,-1] + pixels_lower_neighbors[1:-1,-1]) / 6  # Right edge nodes
        pixels_temp[-1, 1:-1] = (pixels[-1, 1:-1] + pixels_left_neighbors[-1, 1:-1] + pixels_upperleft_neighbors[-1,1:-1]+ pixels_upper_neighbors[-1, 1:-1]
                                 + pixels_upperright_neighbors[-1,1:-1] + pixels_right_neighbors[-1,1:-1]) / 6  # Bottom edge nodes
        return pixels_temp

class Normalize(ImageEffect):
    def apply(pixels: np.ndarray) -> np.ndarray:
        pixels_max = np.max(pixels, axis=(0, 1)) # Calculate max value of R, G and B and store the 3 values in a single array
        pixels_min = np.min(pixels, axis=(0, 1)) # Calculate min value of R, G and B and store the 3 values in a single array
        if (pixels_max != pixels_min).all(): # Only execute the normalization if max(pixels) not equal to min(pixels) to avoid dividing by 0
            pixels = 255 * ((pixels-pixels_min)/(pixels_max-pixels_min)) # Apply element-wise normalization
        return pixels

class Threshold(ImageEffect):
    def apply(pixels: np.ndarray, cutoff: int = 127) -> np.ndarray:
        # Represent the image as a boolean array where 0 is any value <= 127
        # and 1 is any value > 127. Then we can scale this array to 0-255 by
        # multiplying by 255
        return (pixels > cutoff) * 255

class HorizontalMirror(ImageEffect):
    def apply(pixels: np.ndarray) -> np.ndarray:
        # Mirror the pixels about the Y-axis
        return np.flip(pixels,axis = 1)

class Binarize(ImageEffect):
    def apply(pixels: np.ndarray, cutoff: int = 127) -> np.ndarray:
        # To apply the binarize filter, apply grayscale first by calculating the mean value of each pixel's
        # R,G & B values (i.e. (R+G+B)/3) stored in an array of means with shape (h, w).
        # We follow this by restoring the shape of "pixels" at (h, w, 3) to obtain grayscale. We then perform a
        # threshold operation on the grayscale image
        pixels = np.mean(pixels, axis=2)
        pixels = np.repeat(pixels[:, :, np.newaxis], repeats=3, axis=2)
        return (pixels > cutoff) * 255

class Dilate(ImageEffect):
    def apply(pixels: np.ndarray) -> np.ndarray:
        # Step 1: store 1 array equal to "pixels" so we can use it instead
        # to retain "pixels" value as an input throughout the run. We define another 8 arrays
        # with every possible neighboring node through shifting
        # the elements of "pixels" in all possible directions using np.roll().
        pixels_temp = np.copy(pixels)
        pixels_right_neighbors = np.roll(pixels, -1,axis=1)
        pixels_left_neighbors = np.roll(pixels, 1, axis=1)
        pixels_upper_neighbors = np.roll(pixels, 1, axis=0)
        pixels_lower_neighbors = np.roll(pixels, -1, axis=0)
        pixels_upperright_neighbors = np.roll(pixels, (1, -1), axis=(0, 1))
        pixels_upperleft_neighbors = np.roll(pixels, (1, 1), axis=(0, 1))
        pixels_lowerright_neighbors = np.roll(pixels, (-1, -1), axis=(0, 1))
        pixels_lowerleft_neighbors = np.roll(pixels, (-1, 1), axis=(0, 1))
        # Step 2: Perform the necessary mean calculations
        # Step 2.1: Calculate the element-wise max for the middle nodes (taking into consideration the 8 neighbors)
        pixels_temp[1:-1, 1:-1] = np.maximum.reduce([pixels[1:-1, 1:-1], pixels_upper_neighbors[1:-1, 1:-1], pixels_lower_neighbors[1:-1, 1:-1]
                                   , pixels_right_neighbors[1:-1, 1:-1] , pixels_left_neighbors[1:-1,1:-1] , pixels_upperleft_neighbors[1:-1, 1:-1]
                                   , pixels_upperright_neighbors[1:-1, 1:-1] , pixels_lowerleft_neighbors[1:-1, 1:-1], pixels_lowerright_neighbors[1:-1, 1:-1]])
        # Step 2.2: Calculate the element-wise max of the corner nodes (taking into consideration only 3 neighbors)
        pixels_temp[0, 0] = np.maximum.reduce([pixels[0, 0] , pixels_right_neighbors[0, 0] , pixels_lowerright_neighbors[0, 0] ,pixels_lower_neighbors[0, 0]])
        pixels_temp[0, -1] = np.maximum.reduce([pixels[0, -1] , pixels_left_neighbors[0, -1] , pixels_lowerleft_neighbors[0, -1] ,pixels_lower_neighbors[0, -1]])
        pixels_temp[-1, 0] = np.maximum.reduce([pixels[-1, 0] , pixels_right_neighbors[-1, 0] , pixels_upperright_neighbors[-1, 0] ,pixels_upper_neighbors[-1, 0]])
        pixels_temp[-1, -1] = np.maximum.reduce([pixels[-1, -1] , pixels_left_neighbors[-1, -1] , pixels_upperleft_neighbors[-1, -1] ,pixels_upper_neighbors[-1, -1]])
        # Step 2.3: Calculate the element-wise max of the edge nodes (taking into consideration only 5 neighbors)
        pixels_temp[1:-1, 0] = np.maximum.reduce([pixels[1:-1, 0] , pixels_upper_neighbors[1:-1, 0] , pixels_upperright_neighbors[1:-1, 0], pixels_right_neighbors[1:-1, 0]
                                , pixels_lowerright_neighbors[1:-1,0] , pixels_lower_neighbors[1:-1,0]])  # Left edge nodes
        pixels_temp[0, 1:-1] = np.maximum.reduce([pixels[0, 1:-1] , pixels_left_neighbors[0, 1:-1] , pixels_lowerleft_neighbors[0, 1:-1], pixels_lower_neighbors[0, 1:-1]
                                , pixels_lowerright_neighbors[0,1:-1] , pixels_right_neighbors[0,1:-1]])  # Top edge nodes
        pixels_temp[1:-1, -1] = np.maximum.reduce([pixels[1:-1, -1] , pixels_upper_neighbors[1:-1, -1] , pixels_upperleft_neighbors[1:-1,-1], pixels_left_neighbors[1:-1, -1]
                                 , pixels_lowerleft_neighbors[1:-1,-1] , pixels_lower_neighbors[1:-1,-1]])  # Right edge nodes
        pixels_temp[-1, 1:-1] = np.maximum.reduce([pixels[-1, 1:-1] , pixels_left_neighbors[-1, 1:-1] , pixels_upperleft_neighbors[-1,1:-1], pixels_upper_neighbors[-1, 1:-1]
                                 , pixels_upperright_neighbors[-1,1:-1] , pixels_right_neighbors[-1,1:-1]])  # Bottom edge nodes
        return pixels_temp

class Erode(ImageEffect):
    def apply(pixels: np.ndarray) -> np.ndarray:
        # Step 1: store 1 array equal to "pixels" so we can use it instead
        # to retain "pixels" value as an input throughout the run. We define another 8 arrays
        # with every possible neighboring node through shifting
        # the elements of "pixels" in all possible directions using np.roll().
        pixels_temp = np.copy(pixels)
        pixels_right_neighbors = np.roll(pixels, -1,axis=1)
        pixels_left_neighbors = np.roll(pixels, 1, axis=1)
        pixels_upper_neighbors = np.roll(pixels, 1, axis=0)
        pixels_lower_neighbors = np.roll(pixels, -1, axis=0)
        pixels_upperright_neighbors = np.roll(pixels, (1, -1), axis=(0, 1))
        pixels_upperleft_neighbors = np.roll(pixels, (1, 1), axis=(0, 1))
        pixels_lowerright_neighbors = np.roll(pixels, (-1, -1), axis=(0, 1))
        pixels_lowerleft_neighbors = np.roll(pixels, (-1, 1), axis=(0, 1))
        # Step 2: Perform the necessary mean calculations
        # Step 2.1: Calculate the element-wise min for the middle nodes (taking into consideration the 8 neighbors)
        pixels_temp[1:-1, 1:-1] = np.minimum.reduce([pixels[1:-1, 1:-1], pixels_upper_neighbors[1:-1, 1:-1], pixels_lower_neighbors[1:-1, 1:-1]
                                   , pixels_right_neighbors[1:-1, 1:-1] , pixels_left_neighbors[1:-1,1:-1] , pixels_upperleft_neighbors[1:-1, 1:-1]
                                   , pixels_upperright_neighbors[1:-1, 1:-1] , pixels_lowerleft_neighbors[1:-1, 1:-1], pixels_lowerright_neighbors[1:-1, 1:-1]])
        # Step 2.2: Calculate the element-wise min of the corner nodes (taking into consideration only 3 neighbors)
        pixels_temp[0, 0] = np.minimum.reduce([pixels[0, 0] , pixels_right_neighbors[0, 0] , pixels_lowerright_neighbors[0, 0] ,pixels_lower_neighbors[0, 0]])
        pixels_temp[0, -1] = np.minimum.reduce([pixels[0, -1] , pixels_left_neighbors[0, -1] , pixels_lowerleft_neighbors[0, -1] ,pixels_lower_neighbors[0, -1]])
        pixels_temp[-1, 0] = np.minimum.reduce([pixels[-1, 0] , pixels_right_neighbors[-1, 0] , pixels_upperright_neighbors[-1, 0] ,pixels_upper_neighbors[-1, 0]])
        pixels_temp[-1, -1] = np.minimum.reduce([pixels[-1, -1] , pixels_left_neighbors[-1, -1] , pixels_upperleft_neighbors[-1, -1] ,pixels_upper_neighbors[-1, -1]])
        # Step 2.3: Calculate the element-wise min of the edge nodes (taking into consideration only 5 neighbors)
        pixels_temp[1:-1, 0] = np.minimum.reduce([pixels[1:-1, 0] , pixels_upper_neighbors[1:-1, 0] , pixels_upperright_neighbors[1:-1, 0], pixels_right_neighbors[1:-1, 0]
                                , pixels_lowerright_neighbors[1:-1,0] , pixels_lower_neighbors[1:-1,0]])  # Left edge nodes
        pixels_temp[0, 1:-1] = np.minimum.reduce([pixels[0, 1:-1] , pixels_left_neighbors[0, 1:-1] , pixels_lowerleft_neighbors[0, 1:-1], pixels_lower_neighbors[0, 1:-1]
                                , pixels_lowerright_neighbors[0,1:-1] , pixels_right_neighbors[0,1:-1]])  # Top edge nodes
        pixels_temp[1:-1, -1] = np.minimum.reduce([pixels[1:-1, -1] , pixels_upper_neighbors[1:-1, -1] , pixels_upperleft_neighbors[1:-1,-1], pixels_left_neighbors[1:-1, -1]
                                 , pixels_lowerleft_neighbors[1:-1,-1] , pixels_lower_neighbors[1:-1,-1]])  # Right edge nodes
        pixels_temp[-1, 1:-1] = np.minimum.reduce([pixels[-1, 1:-1] , pixels_left_neighbors[-1, 1:-1] , pixels_upperleft_neighbors[-1,1:-1], pixels_upper_neighbors[-1, 1:-1]
                                 , pixels_upperright_neighbors[-1,1:-1] , pixels_right_neighbors[-1,1:-1]])  # Bottom edge nodes
        return pixels_temp