Make your Photos Look Trippy! Build a Photo Filter From Scratch with Python
By: Fidel I. Esquivel
Tags: Photo Editing, Opencv, Programming, Tkinter, Python

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:

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

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:

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.

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

Afterthought:

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!