Build A Simple "PhotoShop" Using Python | Towards Data Science

When we want to edit a piece of photo, we usually come up with some professional tools such as PhotoShop. Even though there are a lot of much easier for use tools having a fair amount of features that could satisfy most of our needs, we still usually need to download, install and spend a little bit of time to learn how to use them.
However, if you are a programmer, or even better a Python developer, it might be much easier to edit your photos in Python. Or, maybe just for fun, it is still cool to understand how an image is represented in numbers and how to modify these low-level features to achieve high-level requirements.
In this article, I will demonstrate how to use two libraries in Python — PIL and NumPy — to achieve most of the basic photo editing features in only 2–3 lines of code.
We don’t need to do any preparation, because the library Pillow is built in the relatively newer Python. Also, I believe you must have NumPy in your Python environment if you program in Python because it is one of the most fundamental libraries that we must have.
What we need to do is just import these two libraries.
from PIL import Imageimport numpy as np
For the Pillow library, we only need its Image module. Also, if you are not familiar with Pillow, please be noted that the package we imported PIL is different from its library name. This is not uncommon, but just be aware of that.
Then, we need a photo to edit. One of my favourite places on this planet is the Twelve Apostles on the Great Ocean Road in Victoria, Australia. So, let’s use a photo from there. Thanks to JVAstudio on the website Pixabay, we have such an amazing one.
We have a photo now, so we can easily use PIL to load it into memory.
image = Image.open('apostles-2965525_640.jpg')
If we check the type of the object image, it is not something that can be directly worked on.
type(image)
Now, we can use NumPy. Under the neath the image object from PIL is a 3D array. We can easily convert it as a NumPy array. After that, because it is already converted into a multi-dimensional array, we can check its shape
img_data = np.array(image)img_data.shape
It is shown that the image has 377 pixels high, 640 pixels wide and the 3 is the channels that are red, green and blue (RGB) respectively. Don’t worry if you have other photos with different dimensions, everything in this article that I will introduce is going to be generic.
We can print the values for the first pixel, which is the one on the top left corner, but with all the 3 channels.
img_data[:1,:1,:]
That means, for the first pixel, we have 204 for red colour, 215 for green colour and 233 for blue colour. What do they mean? All the values that are used to represent a pixel must be in a range of [0, 255]. The colour we see in every photo consists of three values that combine to become a “colour” (here we just ignore the alpha which represents the transparency because it’s not relevant to this article). Also, the higher number means the lighter colour in this channel. In other words, (255, 255, 255) is purely white and (0, 0, 0) is purely black. Therefore, it makes sense that we have (204, 215, 233) for the top left corner pixel because it is part of the sky which is quite close to white.
Now, let’s see what we can do to edit the photo! As we just explained the RGB values, so we can separate the channel to have the photo be represented in a single meta colour.
Remember we have 3 channels as the third dimension of the array? To separate the channel is as easy as slicing the 3D array from the third dimension.
To make sure our new photo only has colour from one channel, we need to make sure all the values for the other two channels are zero. So, we need to initialise a 3D array with all zeros, and then update the corresponding channel with the values from the original photo.
img_chn_red = np.zeros(img_data.shape, dtype='uint8')img_chn_red[:,:,0] = img_data[:,:,0]image_red = Image.fromarray(img_chn_red)
Because “RGB” the red colour comes first, so we need to slice the 3D array to have all values from the 1st channel. Also, it is important to initialise the zeros using explicitly data type. Otherwise, the default zeros are of float type which is not supported to be utilised to generate an image.
The result is like this.
Similarly, we can also separate the green channel. Simply slice the original array to have the 2nd channel.
img_chn_green = np.zeros(img_data.shape, dtype='uint8')img_chn_green[:,:,1] = img_data[:,:,1]image_green = Image.fromarray(img_chn_green)
And the blue channel.
img_chn_blue = np.zeros(img_data.shape, dtype='uint8')img_chn_blue[:,:,2] = img_data[:,:,2]image_blue = Image.fromarray(img_chn_blue)
In this example, we completely filtered the array to have values from a specific channel. Imagine that if you apply a coefficient on a certain channel, you could change the colour temperature as well.
We have got the array of the image and we know the meaning of each dimension. So, it is quite easy to crop the image to size.
In this case, we don’t want to hurt the colour, so we should keep the 3rd dimension unchanged, and dice the array by defining the range of the 1st dimension (height) and the 2nd dimension (width).
img_cropped_data = img_data[60:250, 200:500, :]img_cropped = Image.fromarray(img_cropped_data)
If we flip the 3D array, then we flip the photo. Please be noticed that we can flip on the 1st axis (upside down), or flip on the 2nd axis (left-right flipped), but we can’t do that on the 3rd axis because it will change the colour.
img_flipped_data = np.flip(img_data, axis=1)img_flipped = Image.fromarray(img_flipped_data)
Once we know that each value in the array is of the range [0, 255], we can reverse the colour by subtracting each value from 255. This is extremely easy using the NumPy broadcasting feature.
img_reversed_data = 255 - img_dataimg_reversed = Image.fromarray(img_reversed_data)
Most of the time I’m using photo editing software is to rotate the photo, I believe you might be the same :) This is also easy because NumPy provides the function to rotate an array.
img_rotated_data = np.rot90(img_data)img_rotated = Image.fromarray(img_rotated_data)
In this article, I have introduced how to use the Python built-in library Pillow to read a photo into a NumPy array. Then, we can do a lot of things to this multi-dimensional NumPy array to achieve many different types of photo editing features.
Of course, there must be a lot more we could do to the photo. So, use your creativity to discover more cool editing features. Literally, what we can do with the NumPy multi-dimension array, must correspond to a type of editing of a photo.
All the code used in this article can be found in my Google Colab Notebook.
colab.research.google.com
medium.com
If you feel my articles are helpful, please consider joining Medium Membership to support me and thousands of other writers! (Click the link above)