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