Make your Photos Look Trippy! Build a Photo Filter From Scratch with Python
This tutorial will show you how to develop, completely from scratch, a stand-alone photo editing app to add filters to your photos using Python, Tkinter, and OpenCV!
— The full code and executable files can be found here. —
As a newcomer to Python, I’ve felt that one of the hardest parts of the learning process was to find useful “real-world” applications to write with Python. Command-line applications started to feel constraining, because they are not scalable, and can’t be easily shared.
As it turns out, while I was learning about python image processing and filtering methods, I discovered that most of these result in somewhat trippy modifications. Therefore, just like any other “highly focused” person, I switched projects and built a Photo Filter App (Trippy!) instead. This is the result of that.
In this tutorial I will cover the following:
- How to create a Graphical User Interface (GUI) using Tkinter that can display images
- How to use your webcam as a source to take pictures using Python Open-CV
- How to apply filters to pictures so they look Trippy
- How to connect the GUI with the backend code
- Finally, how to compile everything into a single .exe (Windows) or .app (MacOS) file.
We are what we repeatedly do. Excellence, therefore, is not an act, but a habit.” ― Will Durant (or Aristotle?)
Here is a glimpse of what the finished program looks like. These pictures were all taken using the Trippy Photo Filter app!
Developing the Front End (GUI) — Tkinter Time!
To get started I decided to begin with the front-end, leaving the (scary) back-end for a later time. One of the biggest challenges was to add the live video feed to the display window, therefore, I dedicated a section to explain that later.
To develop the user interface I went with simple Buttons and Labels. Using Tkinter’s grid organization, I laid out my first (beautiful) first sketch, see below.
Although hard to believe… my nascent art skills were sufficient to give me a clear understanding of how I wanted my app to look like. Every button and label now has it’s own coordinates, making it much easier to locate everything on the grid for development.
To create the buttons and labels I used Tkinter’s built-in methods for labels, buttons, and canvas:
tkinter.Canvas respectively. Some examples below:
To break it down, all these methods have the same structure. First, the window needs to be specified. In this case my tkinter window is just called “window”. Next, the text, width and height. Once those are specified, the
.gird() method allows to place the item in the corresponding location, by rows and columns. From my first sketch I see that I want the “Gauss” button to be located in the first row, and the thirteenth column. (This places the top left corner of the item on the desired spot.)
After repeating this process several times, I got the following GUI.
The full code for this section can be seen below:
In the code snipped above, the button commands have been commented out. These will be added later as part of the back-end to connect the photo filters with the GUI.
How to take pictures with a webcam and display them using OpenCV
Now that we have an empty canvas on Tkinter, the next step is to use a webcam to take and display pictures in the program. This is easily done using the following OpenCV’s built-in methods:
vid = cv2.videoCapture(0): This method calls for the default camera with index zero (usually your built-in webcam) and stores its functions in the variable
vid(that stands for video).
ret, frame = self.vid.read(): This method reads the current frame from the videoCapture method
vid. This frame is used to display current video on the canvas.
Running both of these commands will capture a still frame. In order to capture a video, the
vid.read() method can be used inside a while loop.
To display each frame, the canvas method
canvas.create_image(...,...,image=..,anchor=tkinter.NW) can be used. This will display the current frame inside canvas on the Trippy GUI.
Running a loop within a loop
One important consideration is the fact that both Tkinter and OpenCV run as infinite loops. This can become tricky when trying to use both of them together, and can cause runtime errors. To solve this, I learned a useful trick that uses an update() function within Tkinter’s main loop (i.e. before the
The way this works is that whenever Tkinter performs a single loop, the update function is called (line 92 of my code below). This causes tkinter to pause while OpenCV takes a picture, processes the image, and displays it in the main tkinter canvas. OpenCV stops, and the tkinter loop starts over. This process flow is seen below:
Applying Filters to make pictures look Trippy!
Now that tkinter is (hopefully) running and displaying a video on the main canvas, it is time to add the filters to the program. The filters I will be using are Greyscale, Gaussian blur, Sobel x, Sobel y, Laplacian, image delta, Blue-shift, and Threshold Filters.
- Simple grey image:
To turn any image into greyscale (useful for doing image analysis), I used tkinter’s built-in methods. After calling for the
frame = vid.read(), a 1280x720x3 (1280 pixels wide, 720 pixels high, 3xColors RGB pixels) image is created (based on the resolution of my webcam) and stored in the variable
frame. Using the convert-color function yields the following picture, which I store in the variable
grey_img = cv2.cvtColor(frame,cv2.COLOR_BGR2GRAY)
2. Gaussian Blur:
Gaussian blur is used to make an image look smoother and reduce noise. The gaussian blur is a low-pass filter, meaning that it attenuates high frequency noise. Luckily, the OpenCV library includes this function. The width and height of the kernel needs to be specified, which should be positive and odd (21,21 in this case). The standard deviation in x and y also needs to be specified (listed as 0 here). Using the built-in function yields the following picture.
frame = cv2.GaussianBlur(gray, (21,21), 0)
3. Sobel X and Sobel Y Filters
The Sobel operator, or Sobel filter is used mainly for edge detection. When applied, it creates and image emphasizing the edges. It operates by analyzing the image and creating a vector field showing the pixel gradient within the image. This gradient highlights all edges of the image. The sobel filter can be decomposed into images normalized by the x-gradient (Sobel-x) or by the y-gradient (Sobel-y). For this Trippy program, I used both, which are activated by repeatedly pressing the sobel x-xy button.
frame = cv2.Sobel(gray,-1, dx=1, dy=0, ksize=11, scale=1, delta=0, borderType=cv2.BORDER_DEFAULT)
4. Laplacian Filter
The Laplacian filter (or discrete Laplacian operator) is a 2D measure used to highligh the regions of rapid intensity change in an image. Similar to the 2D implementation of the Sobel filter, it can be used for finding edges. Usually, this filter is applied after the Gaussian filter, to reduce sensitivity to noise. Once again, OpenCV includes this filter in its library.
frame = cv2.Laplacian(gray, -1, ksize=17, scale=1, delta=0, borderType=cv2.BORDER_DEFAULT)
5. Image Delta (Absolute difference)
The image delta filter is not so much a filter than an image processing technique. It compares the pixel values of the current image taken by the camera, to a still image taken when the program is first started. This produces an image that is mostly black. If the camera records the same values for two pixels, their difference will be zero, or a black point. This is used to detect motion.
frame = cv2.absdiff(frame1, gray)
6. Blue Shift
Although this is a common filter, I happened to discover it by mistake. OpenCV’s default image storing format is BGR (which stands for Blue, Green, and Red). Normally, the convention for image storing is RGB (red, green and blue) format. Knowing this, I tried to fix it, by shifting all BGR values to RGB. But because the display method in OpenCV also uses BGR, the pixel values got switched and a blue image came out.
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
7. Threshold Filter
The threshold image filter builds up on the delta filter. It applies a threshold and determines if a difference is large enough and if so, it sets the value to 255 (white). If the pixel value is less than the threshold, it sets it to 0 or black.
frame = cv2.threshold(cv2.absdiff(frame1, gray), 30, 255, cv2.THRESH_BINARY)
Connecting the back-end
After developing the GUI and the filters what’s left is to connect the Tkinter buttons to apply the filters to the images. This is done by creating functions that can be called and do the image processing. As explained before, this is all done within the update function, this way the loops don’t interfere with each other.
This is done through the following steps:
- Create a dictionary with all the filter options and assign them a boolean value. [Lines 18–26 in code below.]
- Create a function for each button such that if the button is pressed, it modifies the dictionary to set the filter to True, and all the others to False. [Lines 139–160 in code below.]
- For every button, attach the function to the button object using the command option:
tkinter.Button(window,text=”…”,command=self.filter). Therefore, if the button is pressed, it’s filter value is assigned to True.
- Within the update function, create a series of if-elif-else statements to modify the current frame per the selected filter. [Lines 98–137 in code below.]
And that’s it. Now the app should be running flawlessly!
Compiling everything into executable code!
Probably one of the easiest parts of this entire tutorial is to compile the code into an executable file. This is done using the
pyinstaller module for Python3 .
In the command prompt or terminal, navigate to the project folder. In my case, I write the following:
~/home/$ cd Projects/trippy_app to access my trippy_app folder.
pip3 install pyinstaller and wait.
pyinstaller --onefile --windowed trippy_app.py
DONE! Now you will have a .app or .exe file in the newly created
dist folder in your current directory. This will depend on which app you operating system you are running on.
One thing that I noticed when I imported my code from a windows machine to a OSX computer is that my code stopped running completely. After a lot of troubleshooting, I realized that I had different versions of Tkinter, OpenCV, and other important libraries. This made my code obsolete, and wouldn’t run at all.
To fix this, I began to learn about virtual environments. I use conda for most of my projects, so before running the python script, I would activate the virtual environment and run it within. This solved my problem, and the computer stopped getting stuck. I highly recommend using virtual environments whenever you are developing!
For more info, and a detailed explanation on how to use conda look here! https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html
The full 192 lines of code I wrote for this program can be seen below. This should run on OSX without problems. To run on Windows, change line 11 to
folder = r"\trippy\". This is due the directory formatting in windows using backward slashes, while Linux/MacOS use foward slashes.
Note: my code is in no way optimized, but as a beginner’s project it does the job well and runs (mostly). If you have any comments on how this could be better, please let me know in the comments below.
This can also be found on my GitHub site here: https://github.com/phideltaee/2_trippy_app
This post took me 6 pomodoros to complete. If you also want to increase your productivity and learn how to build a pomodoro timer using python, read my other post here:
Build a Pomodoro Timer using Python
Create your own Pomodoro timer app using Python. Learn to focus and block distracting websites during “Pomodoro Time”!