Make your Photos Look Trippy! Build a Photo Filter From Scratch with Python

Fidel I. Esquivel
Jun 22 · 10 min read

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:

  1. How to create a Graphical User Interface (GUI) using Tkinter that can display images
  2. How to use your webcam as a source to take pictures using Python Open-CV
  3. How to apply filters to pictures so they look Trippy
  4. How to connect the GUI with the backend code
  5. 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!

Graphical User Interface of Trippy Photo Filter app and example filters applied. No filter (upper left), Sobel filter (upper right), Gaussian blur (bottom left), and Image Delta (bottom right).

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.

First sketch for the trippy app, developed in OneNote

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.Label, tkinter.Button, and 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.

Trippy GUI on OSX

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:

  1. 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).
  2. 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.

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 window.mainloop() function).

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.

  1. 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_frame.

Function: 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.

Function: 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.

Function:frame = cv2.Sobel(gray,-1, dx=1, dy=0, ksize=11, scale=1, delta=0, borderType=cv2.BORDER_DEFAULT)

Sobel X filter
Sobel X-Y filter

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.

Function: frame = cv2.Laplacian(gray, -1, ksize=17, scale=1, delta=0, borderType=cv2.BORDER_DEFAULT)

Laplacian filter

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.

Function: frame = cv2.absdiff(frame1, gray)

Image Delta Filter

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.

Function: frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)

Blue shift filter

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.

Function: frame = cv2.threshold(cv2.absdiff(frame1, gray), 30, 255, cv2.THRESH_BINARY)[1]

Threshold filter

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:

  1. Create a dictionary with all the filter options and assign them a boolean value. [Lines 18–26 in code below.]
  2. 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.]
  3. 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.
  4. 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.

Install pyinstaller: pip3 install pyinstaller and wait.

Run pyinstaller: 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.

Troubleshooting

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

{my Code}

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:

Happy Coding!

Thanks to Jonas Pohlmann.

Fidel I. Esquivel

Written by