{"id":3481,"date":"2025-05-01T07:02:26","date_gmt":"2025-05-01T07:02:26","guid":{"rendered":"https:\/\/mailitics.com\/index.php\/2025\/05\/01\/modern-gui-applications-for-computer-vision-in-python\/"},"modified":"2025-05-01T07:02:26","modified_gmt":"2025-05-01T07:02:26","slug":"modern-gui-applications-for-computer-vision-in-python","status":"publish","type":"post","link":"https:\/\/mailitics.com\/index.php\/2025\/05\/01\/modern-gui-applications-for-computer-vision-in-python\/","title":{"rendered":"Modern GUI Applications for Computer Vision in\u00a0Python"},"content":{"rendered":"<p>    Modern GUI Applications for Computer Vision in\u00a0Python<br \/>\n \t<BR><br \/>\n<BR><\/BR><br \/>\n    <!-- no image --><br \/>\n \t<BR><br \/>\n<BR><\/BR><\/p>\n<div>\n<figure class=\"wp-block-image aligncenter size-full\"><img data-recalc-dims=\"1\" decoding=\"async\" src=\"https:\/\/i0.wp.com\/contributor.insightmediagroup.io\/wp-content\/uploads\/2025\/04\/Screencastfrom04-24-2025113343PM-ezgif.com-video-to-gif-converter.gif?ssl=1\" alt=\"\" class=\"wp-image-602301\"><\/figure>\n<h2 class=\"wp-block-heading\"><mdspan datatext=\"el1746063411947\" class=\"mdspan-comment\">Introduction<\/mdspan><\/h2>\n<p class=\"wp-block-paragraph\">I\u2019m a huge fan of interactive visualizations. As a computer vision engineer, I deal almost daily with image processing related tasks and more often than not I am iterating on a problem where I need <strong>visual feedback<\/strong> to make decisions. Let\u2019s think of a very simple image processing pipeline with a single step that has some parameters to transform an image:<\/p>\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" height=\"168\" width=\"1024\" decoding=\"async\" src=\"https:\/\/i0.wp.com\/contributor.insightmediagroup.io\/wp-content\/uploads\/2025\/04\/image-133-1024x168.png?resize=1024%2C168&#038;ssl=1\" alt=\"Sample processing pipeline with missing visualization of output\" class=\"wp-image-602295\"><\/figure>\n<p class=\"wp-block-paragraph\">How do you know which parameters to adjust? Does the pipeline even work as expected? Without visualizing your output, you might miss out on some key insights and make sub optimal choices.<\/p>\n<p class=\"wp-block-paragraph\">Sometimes simply showing the output image and\/or some calculated metrics can be enough to iterate on the parameters. But I\u2019ve found myself in many situations where a tool would be immensely helpful to iterate quickly and interactively on my pipeline. So in this article I will show you how to work with simple built-in interactive elements from <code>OpenCV<\/code> as well as how to build more modern user interfaces for Computer Vision projects using <code>customtkinter<\/code>.<\/p>\n<h2 class=\"wp-block-heading\">Prerequisites<\/h2>\n<p class=\"wp-block-paragraph\">If you want to follow along, I recommend you to set up your local environment with <a href=\"https:\/\/docs.astral.sh\/uv\/\">uv<\/a> and install the following packages:<\/p>\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-bash\">uv add numpy opencv-<a href=\"https:\/\/towardsdatascience.com\/tag\/python\/\" title=\"Python\">Python<\/a> pillow customtkinter<\/code><\/pre>\n<h2 class=\"wp-block-heading\">Goal<\/h2>\n<p class=\"wp-block-paragraph\">Before we dive into the code of the project, let\u2019s quickly outline what we want to build. The application should use the webcam feed and allow the user to select different types of filters that will be applied to the stream. The processed image should be shown in real-time in the window. A rough sketch of a potential UI would look as follows:<\/p>\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" height=\"648\" width=\"1024\" decoding=\"async\" src=\"https:\/\/i0.wp.com\/contributor.insightmediagroup.io\/wp-content\/uploads\/2025\/04\/image-135-1024x648.png?resize=1024%2C648&#038;ssl=1\" alt=\"\" class=\"wp-image-602302\"><\/figure>\n<h2 class=\"wp-block-heading\">OpenCV \u2013 GUI<\/h2>\n<p class=\"wp-block-paragraph\">Let\u2019s start with a simple loop that fetches frames from your webcam and displays them in an OpenCV window.<\/p>\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-python\">import cv2\n\ncap = cv2.VideoCapture(0)\n\nwhile True:\n    ret, frame = cap.read()\n    if not ret:\n        break\n\n    cv2.imshow(\"Video Feed\", frame)\n    \n    key = cv2.waitKey(1) &amp; 0xFF\n    if key == ord('q'):\n        break\n\ncap.release()\ncv2.destroyAllWindows()<\/code><\/pre>\n<h3 class=\"wp-block-heading\">Keyboard Input<\/h3>\n<p class=\"wp-block-paragraph\">The simplest way to add interactivity here, is by adding keyboard inputs. For example, we can cycle through different filters with the number keys.<\/p>\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-python\">...\n\nfilter_type = \"normal\"\n\nwhile True:\n    ...\n\n    if filter_type == \"grayscale\":\n        frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)\n    elif filter_type == \"normal\":\n        pass\n\n    ...\n\n    if key == ord('1'):\n        filter_type = \"normal\"\n    if key == ord('2'):\n        filter_type = \"grayscale\"\n        \n    ...<\/code><\/pre>\n<p class=\"wp-block-paragraph\">Now you can switch between the normal image and the grayscale version by pressing the number keys 1 and 2. Let\u2019s also quickly add a caption to the image so we can actually see the name of the filter we\u2019re applying.<\/p>\n<p class=\"wp-block-paragraph\">Now we need to be careful here: if you take a look at the shape of the frame after the filter, you will notice that the dimensionality of the frame array has changed. Remember that OpenCV image arrays are ordered <strong>HWC<\/strong> (height, width, color) with color as <strong>BGR<\/strong> (green, blue, red), so the 640\u00d7480 image from my webcam has shape <code>(480, 640, 3)<\/code>.<\/p>\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-python\">print(filter_type, frame.shape)\n# normal (480, 640, 3)\n# grayscale (480, 640)<\/code><\/pre>\n<p class=\"wp-block-paragraph\">Now because the grayscale operation outputs a single channel image, the color dimension is dropped. If we now want to draw on top of this image, we either need to specify a single channel color for the grayscale image or we convert that image back to the original <strong>BGR<\/strong> format. The second option is a bit cleaner because we can unify the annotation of the image.<\/p>\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-python\">if filter_type == \"grayscale\":\n    frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)\nelif filter_type == \"normal\":\n    pass\n\nif len(frame.shape) == 2: # Convert grayscale to BGR\n    frame = cv2.cvtColor(frame, cv2.COLOR_GRAY2BGR)<\/code><\/pre>\n<h3 class=\"wp-block-heading\">Caption<\/h3>\n<p class=\"wp-block-paragraph\">I want to add a black border at the bottom of the image, on top of which the name of the filter will be shown. We can make use of the <code>copyMakeBorder<\/code> function to pad the image with a border color at the bottom. Then we can add the text on top of this border.<\/p>\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-python\"># Add a black border at the bottom of the frame\nborder_height = 50\nborder_color = (0, 0, 0)\nframe = cv2.copyMakeBorder(frame, 0, border_height, 0, 0, cv2.BORDER_CONSTANT, value=border_color)\n\n# Show the filter name\ncv2.putText(\n    frame,\n    filter_type,\n    (frame.shape[1] \/\/ 2 - 50, frame.shape[0] - border_height \/\/ 2 + 10),\n    cv2.FONT_HERSHEY_SIMPLEX,\n    1,\n    (255, 255, 255),\n    2,\n    cv2.LINE_AA,\n)<\/code><\/pre>\n<p class=\"wp-block-paragraph\">This is how the output should look, and you can switch between the normal and grayscale mode and the frames will be captioned accordingly.<\/p>\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" height=\"493\" width=\"1024\" decoding=\"async\" src=\"https:\/\/i0.wp.com\/contributor.insightmediagroup.io\/wp-content\/uploads\/2025\/04\/image-138-1024x493.png?resize=1024%2C493&#038;ssl=1\" alt=\"\" class=\"wp-image-602309\"><\/figure>\n<h3 class=\"wp-block-heading\">Sliders<\/h3>\n<p class=\"wp-block-paragraph\">Now instead of using the keyboard as input method, OpenCV offers a basic trackbar slider UI element. The trackbar needs to be initialized at the beginning of the script. We need to reference the same window as we will be showing our images in later, so I will create a variable for the name of the window. Using this name, we can create the trackbar and let it be a selector for the index in the list of filters.<\/p>\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-python\">filter_types = [\"normal\", \"grayscale\"]\n\nwin_name = \"Webcam Stream\"\ncv2.namedWindow(win_name)\n\ntb_filter = \"Filter\"\n# def createTrackbar(trackbarName: str, windowName: str, value: int, count: int, onChange: _typing.Callable[[int], None]) -&gt; None: ...\ncv2.createTrackbar(\n    tb_filter,\n    win_name,\n    0,\n    len(filter_types) - 1,\n    lambda _: None,\n)<\/code><\/pre>\n<p class=\"wp-block-paragraph\">Notice how we use an empty lambda for the <code>onChange<\/code> callback, we will fetch the value manually in the loop. Everything else will stay the same.<\/p>\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-python\">while True:\n    ...\n\n    # Get the selected filter type\n    filter_id = cv2.getTrackbarPos(tb_filter, win_name)\n    filter_type = filter_types[filter_id]\n\n    ...<\/code><\/pre>\n<p class=\"wp-block-paragraph\">And voil\u00e0, we have a trackbar to select our filter.<\/p>\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" height=\"510\" width=\"1024\" decoding=\"async\" src=\"https:\/\/i0.wp.com\/contributor.insightmediagroup.io\/wp-content\/uploads\/2025\/04\/image-140-1024x510.png?resize=1024%2C510&#038;ssl=1\" alt=\"\" class=\"wp-image-602311\"><\/figure>\n<p class=\"wp-block-paragraph\">Now we can also easily add more filters easily by extending our list and implementing each processing step.<\/p>\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-python\">filter_types = [\n    \"normal\",\n    \"grayscale\",\n    \"blur\",\n    \"threshold\",\n    \"canny\",\n    \"sobel\",\n    \"laplacian\",\n]\n\n...\n\n    if filter_type == \"grayscale\":\n        frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)\n    elif filter_type == \"blur\":\n        frame = cv2.GaussianBlur(frame, ksize=(15, 15), sigmaX=0)\n    elif filter_type == \"threshold\":\n        gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)\n        _, thresholded_frame = cv2.threshold(gray, thresh=127, maxval=255, type=cv2.THRESH_BINARY)\n    elif filter_type == \"canny\":\n        frame = cv2.Canny(frame, threshold1=100, threshold2=200)\n    elif filter_type == \"sobel\":\n        frame = cv2.Sobel(frame, ddepth=cv2.CV_64F, dx=1, dy=0, ksize=5)\n    elif filter_type == \"laplacian\":\n        frame = cv2.Laplacian(frame, ddepth=cv2.CV_64F)\n    elif filter_type == \"normal\":\n        pass\n\n    if frame.dtype != np.uint8:\n        # Scale the frame to uint8 if necessary\n        cv2.normalize(frame, frame, 0, 255, cv2.NORM_MINMAX)\n        frame = frame.astype(np.uint8)\n<\/code><\/pre>\n<figure class=\"wp-block-image size-full\"><img data-recalc-dims=\"1\" decoding=\"async\" src=\"https:\/\/i0.wp.com\/contributor.insightmediagroup.io\/wp-content\/uploads\/2025\/04\/image-141.png?ssl=1\" alt=\"\" class=\"wp-image-602312\"><\/figure>\n<h2 class=\"wp-block-heading\">Modern GUI with CustomTkinter<\/h2>\n<p class=\"wp-block-paragraph\">Now I don\u2019t know about you but the current user interface does not look very <em>modern<\/em> to me. Don\u2019t get me wrong, there is some beauty in the style of the interface, but I prefer cleaner, more modern designs. Plus we\u2019re already at the limit of what <strong>OpenCV<\/strong> offers out of the box in terms of UI elements. Yep, no buttons, text fields, dropdowns, checkboxes or radio buttons and no custom layouts. So let\u2019s see how we can transform the look and user experience of this basic application to a fresh and clean one.<\/p>\n<figure class=\"wp-block-image alignwide size-large\"><img data-recalc-dims=\"1\" height=\"357\" width=\"1024\" decoding=\"async\" src=\"https:\/\/i0.wp.com\/contributor.insightmediagroup.io\/wp-content\/uploads\/2025\/04\/image-132-1024x357.png?resize=1024%2C357&#038;ssl=1\" alt=\"\" class=\"wp-image-602286\"><\/figure>\n<p class=\"wp-block-paragraph\">So to get started, we first need to create a class for our app. We create two frames: the first one contains our filter selection on the left side and the second one wraps the image display. For now, let\u2019s start with a simple placeholder text. Unfortunately there\u2019s no out of the box opencv component from customtkinter directly, so we will need to quickly build our own in the next few steps. But let\u2019s first finish the basic UI layout.<\/p>\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-python\">import customtkinter\n\n\nclass App(customtkinter.CTk):\n    def __init__(self) -&gt; None:\n        super().__init__()\n\n        self.title(\"Webcam Stream\")\n        self.geometry(\"800x600\")\n\n        self.filter_var = customtkinter.IntVar(value=0)\n\n        # Frame for filters\n        self.filters_frame = customtkinter.CTkFrame(self)\n        self.filters_frame.pack(side=\"left\", fill=\"both\", expand=False, padx=10, pady=10)\n\n        # Frame for image display\n        self.image_frame = customtkinter.CTkFrame(self)\n        self.image_frame.pack(side=\"right\", fill=\"both\", expand=True, padx=10, pady=10)\n\n        self.image_display = customtkinter.CTkLabel(self.image_frame, text=\"Loading...\")\n        self.image_display.pack(fill=\"both\", expand=True, padx=10, pady=10)\n\napp = App()\napp.mainloop()<\/code><\/pre>\n<figure class=\"wp-block-image size-full\"><img data-recalc-dims=\"1\" decoding=\"async\" src=\"https:\/\/i0.wp.com\/contributor.insightmediagroup.io\/wp-content\/uploads\/2025\/04\/image-142.png?ssl=1\" alt=\"\" class=\"wp-image-602314\"><\/figure>\n<h3 class=\"wp-block-heading\">Filter Radio Buttons<\/h3>\n<p class=\"wp-block-paragraph\">Now that the skeleton is built, we can start filling in our components. For the left side, I will be using the same list of <code>filter_types<\/code> to populate a group of radio buttons to select the filter.<\/p>\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-python\">        # Create radio buttons for each filter type\n        self.filter_var = customtkinter.IntVar(value=0)\n        for filter_id, filter_type in enumerate(filter_types):\n            rb_filter = customtkinter.CTkRadioButton(\n                self.filters_frame,\n                text=filter_type.capitalize(),\n                variable=self.filter_var,\n                value=filter_id,\n            )\n            rb_filter.pack(padx=10, pady=10)\n\n            if filter_id == 0:\n                rb_filter.select()<\/code><\/pre>\n<figure class=\"wp-block-image size-full\"><img data-recalc-dims=\"1\" decoding=\"async\" src=\"https:\/\/i0.wp.com\/contributor.insightmediagroup.io\/wp-content\/uploads\/2025\/04\/image-143.png?ssl=1\" alt=\"\" class=\"wp-image-602315\"><\/figure>\n<h3 class=\"wp-block-heading\">Image Display Component<\/h3>\n<p class=\"wp-block-paragraph\">Now we can get started on the interesting part, how to get our OpenCV frames to show up in the image component. Because there\u2019s no built-in component, let\u2019s create our own based on the <code>CTKLabel<\/code>. This allows us to display a loading text while the webcam stream is starting up.<\/p>\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-python\">...\n\nclass CTkImageDisplay(customtkinter.CTkLabel):\n    \"\"\"\n    A reusable ctk widget widget to display opencv images.\n    \"\"\"\n\n    def __init__(\n        self,\n        master: Any,\n    ) -&gt; None:\n        self._textvariable = customtkinter.StringVar(master, \"Loading...\")\n        super().__init__(\n            master,\n            textvariable=self._textvariable,\n            image=None,\n        )\n\n...\n\nclass App(customtkinter.CTk):\n    def __init__(self) -&gt; None:\n        ...\n\n        self.image_display = CTkImageDisplay(self.image_frame)\n        self.image_display.pack(fill=\"both\", expand=True, padx=10, pady=10) <\/code><\/pre>\n<p class=\"wp-block-paragraph\">So far nothing has changed, we simply swapped out the existing label with our custom class implementation.  In our <code>CTKImageDisplay<\/code> class we can define an function to show an image in the component, let\u2019s call it <code>set_frame<\/code>.<\/p>\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-python\">import cv2\nimport numpy.typing as npt\nfrom PIL import Image\n\nclass CTkImageDisplay(customtkinter.CTkLabel):\n    ...\n\n    def set_frame(self, frame: npt.NDArray) -&gt; None:\n        \"\"\"\n        Set the frame to be displayed in the widget.\n\n        Args:\n            frame: The new frame to display, in opencv format (BGR).\n        \"\"\"\n        target_width, target_height = frame.shape[1], frame.shape[0]\n\n        # Convert the frame to PIL Image format\n        frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)\n        frame_pil = Image.fromarray(frame_rgb, \"RGB\")\n\n        ctk_image = customtkinter.CTkImage(\n            light_image=frame_pil,\n            dark_image=frame_pil,\n            size=(target_width, target_height),\n        )\n        self.configure(image=ctk_image, text=\"\")\n        self._textvariable.set(\"\")<\/code><\/pre>\n<p class=\"wp-block-paragraph\">Let\u2019s digest this. First we need to know how big our image component will be, we can extract that information from the shape property of our image array. To display the image in <code>tkinter<\/code>, we need a Pillow <code>Image<\/code> type, we cannot directly use the OpenCV array. To convert an OpenCV array to Pillow, we first need to convert the color space from <strong>BGR<\/strong> to <strong>RGB<\/strong> and then we can use the <code>Image.fromarray <\/code>function to create the Pillow Image object. Next we can create a CTKImage, where we use the same image no matter the theme and set the size according to our frame.  Finally we can use the configure method to set the image in our frame. At the end, we also reset the text variable to remove the <em>\u201cLoading\u2026\u201d<\/em> text, even though it would theoretically be hidden behind the image.<\/p>\n<p class=\"wp-block-paragraph\">To quickly test this, we can set the first image of our webcam in the constructor. (We will see in a second why this is not such a good idea)<\/p>\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-python\">class App(customtkinter.CTk):\n    def __init__(self) -&gt; None:\n        ...\n        \n        cap = cv2.VideoCapture(0)\n        _, frame0 = cap.read()\n        self.image_display.set_frame(frame0)<\/code><\/pre>\n<p class=\"wp-block-paragraph\">If you run this, you will notice that the window takes a bit longer to pop up, but after a short delay you should see a static image from your webcam. <\/p>\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\"><strong>NOTE:<\/strong> If you don\u2019t have a webcam ready you can also just use a local video file by passing the file path to the <code>cv2.VideoCapture <\/code>constructor call.<\/p>\n<\/blockquote>\n<figure class=\"wp-block-image size-full\"><img data-recalc-dims=\"1\" decoding=\"async\" src=\"https:\/\/i0.wp.com\/contributor.insightmediagroup.io\/wp-content\/uploads\/2025\/04\/image-144.png?ssl=1\" alt=\"\" class=\"wp-image-602351\"><\/figure>\n<p class=\"wp-block-paragraph\">Now this is not very exciting, since the frame doesn\u2019t update yet. So let\u2019s see what happens if we try to do this naively.<\/p>\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-python\">class App(customtkinter.CTk):\n    def __init__(self) -&gt; None:\n        ...\n\n        cap = cv2.VideoCapture(0)\n        while True:\n            ret, frame = cap.read()\n            if not ret:\n                break\n\n            self.image_display.set_frame(frame)<\/code><\/pre>\n<p class=\"wp-block-paragraph\">Almost the same as before, except now we run the frame loop as we did in the previous chapter with the OpenCV GUI. If you run this, you will see\u2026 exactly nothing. The window never shows up, since we\u2019re creating an infinite loop in the constructor of the app! This is also the reason why the program only showed up after a delay in the previous example, the opening of the Webcam stream is a blocking operation, and the event loop for the window cannot run, so it doesn\u2019t show up yet.<\/p>\n<p class=\"wp-block-paragraph\">So let\u2019s fix this by adding a slightly better implementation that allows the gui event loop to run while we also update the frame every once in a while. We can use the <code>after<\/code> method of <code>tkinter<\/code> to schedule a function call while yielding the process during the wait time.<\/p>\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-python\">\n        ...\n\n        self.cap = cv2.VideoCapture(0)\n        self.after(10, self.update_frame)\n\n    def update_frame(self) -&gt; None:\n        \"\"\"\n        Update the displayed frame.\n        \"\"\"\n        \n        ret, frame = self.cap.read()\n        if not ret:\n            return\n        \n        self.image_display.set_frame(frame)\n\n        self.after(10, self.update_frame)\n<\/code><\/pre>\n<p class=\"wp-block-paragraph\">So now we still set up the webcam stream in the constructor, so we haven\u2019t solved that problem yet. But at least we can see a continuous stream of frames in our image component.<\/p>\n<figure class=\"wp-block-image aligncenter size-full\"><img data-recalc-dims=\"1\" decoding=\"async\" src=\"https:\/\/i0.wp.com\/contributor.insightmediagroup.io\/wp-content\/uploads\/2025\/04\/2025-04-25_21-09-44-ezgif.com-video-to-gif-converter.gif?ssl=1\" alt=\"\" class=\"wp-image-602355\"><\/figure>\n<h3 class=\"wp-block-heading\">Applying Filters<\/h3>\n<p class=\"wp-block-paragraph\">Now that the frame loop is running. we can re-implement our filters from the beginning and apply them to our webcam stream. In the update_frame function, we can check the current filter variable and apply the corresponding filter function.<\/p>\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-python\">    def update_frame(self) -&gt; None:\n        ...\n        \n        # Get the selected filter type\n        filter_id = self.filter_var.get()\n        filter_type = filter_types[filter_id]\n\n        if filter_type == \"grayscale\":\n            frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)\n        elif filter_type == \"blur\":\n            frame = cv2.GaussianBlur(frame, ksize=(15, 15), sigmaX=0)\n        elif filter_type == \"threshold\":\n            gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)\n            _, frame = cv2.threshold(gray, thresh=127, maxval=255, type=cv2.THRESH_BINARY)\n        elif filter_type == \"canny\":\n            frame = cv2.Canny(frame, threshold1=100, threshold2=200)\n        elif filter_type == \"sobel\":\n            frame = cv2.Sobel(frame, ddepth=cv2.CV_64F, dx=1, dy=0, ksize=5)\n        elif filter_type == \"laplacian\":\n            frame = cv2.Laplacian(frame, ddepth=cv2.CV_64F)\n        elif filter_type == \"normal\":\n            pass\n\n        if frame.dtype != np.uint8:\n            # Scale the frame to uint8 if necessary\n            cv2.normalize(frame, frame, 0, 255, cv2.NORM_MINMAX)\n            frame = frame.astype(np.uint8)\n        if len(frame.shape) == 2:  # Convert grayscale to BGR\n            frame = cv2.cvtColor(frame, cv2.COLOR_GRAY2BGR)\n        \n        self.image_display.set_frame(frame)\n\n        self.after(10, self.update_frame)<\/code><\/pre>\n<p class=\"wp-block-paragraph\">And now we\u2019re back to the full functionality of the application, you can select any filter on the left side and it will be applied in real-time to the webcam feed!<\/p>\n<figure class=\"wp-block-image aligncenter size-full\"><img data-recalc-dims=\"1\" decoding=\"async\" src=\"https:\/\/i0.wp.com\/contributor.insightmediagroup.io\/wp-content\/uploads\/2025\/04\/2025-04-25_21-16-02-ezgif.com-video-to-gif-converter.gif?ssl=1\" alt=\"\" class=\"wp-image-602356\"><\/figure>\n<h3 class=\"wp-block-heading\">Multithreading and Synchronization<\/h3>\n<p class=\"wp-block-paragraph\">Now although the application runs as is, there are some problems with the current way we run our frame loop. Currently everything runs in a single thread, the main GUI thread. This is why in the beginning, we don\u2019t immediately see the window pop up, our webcam initialization blocks the main thread. Now imagine, if we did some heavier image processing, maybe running the images through neural network, you wouldn\u2019t want your user interface to always be blocked while the network is running inference. This will lead to a very unresponsive user experience when clicking the UI elements!<\/p>\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" height=\"733\" width=\"1024\" decoding=\"async\" src=\"https:\/\/i0.wp.com\/contributor.insightmediagroup.io\/wp-content\/uploads\/2025\/04\/image-145-1024x733.png?resize=1024%2C733&#038;ssl=1\" alt=\"\" class=\"wp-image-602360\"><\/figure>\n<p class=\"wp-block-paragraph\">A better way to handle this in our application is to <em>separate the image processing from the user interface<\/em>. Generally this is almost always a good idea to separate your GUI logic from any type of non-trivial processing. So in our case, we will run a separate thread that is responsible for the image loop. It will read the frames from the webcam stream and apply the filters. <\/p>\n<figure class=\"wp-block-image aligncenter size-large\"><img data-recalc-dims=\"1\" height=\"1024\" width=\"923\" decoding=\"async\" src=\"https:\/\/i0.wp.com\/contributor.insightmediagroup.io\/wp-content\/uploads\/2025\/04\/image-146-923x1024.png?resize=923%2C1024&#038;ssl=1\" alt=\"\" class=\"wp-image-602361\"><\/figure>\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\"><strong>NOTE:<\/strong> Python threads are not <em>\u201creal\u201d<\/em> threads in a sense that they do not have the capability to run on different logical cpu cores and hence will not <em>really<\/em> run in parallel. In Python multithreading the context will switch between the threads, but due to the GIL, the global interpreter lock, a single python process can only run one physical thread. If you want <em>\u201creal\u201d<\/em> parallel processing, you will need to use <strong>multiprocessing<\/strong>. Since our process here is not CPU bound but actually <strong>I\/O bound<\/strong>, multithreading suffices.<\/p>\n<\/blockquote>\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-python\">class App(customtkinter.CTk):\n    def __init__(self) -&gt; None:\n        ...\n\n        self.webcam_thread = threading.Thread(target=self.run_webcam_loop, daemon=True)\n        self.webcam_thread.start()\n\n    def run_webcam_loop(self) -&gt; None:\n        \"\"\"\n        Run the webcam loop in a separate thread.\n        \"\"\"\n        self.cap = cv2.VideoCapture(0)\n        if not self.cap.isOpened():\n            return\n\n        while True:\n            ret, frame = self.cap.read()\n            if not ret:\n                break\n\n            # Filters\n            ...\n\n            self.image_display.set_frame(frame)<\/code><\/pre>\n<p class=\"wp-block-paragraph\">If you run this, you will now see that our window opens up immediately and we even see our loading text while the webcam stream is opening up. However, as soon as the stream starts, the frames start to flicker. Depending on a lot of factors, you might experience different visual artifacts or errors at this stage.<\/p>\n<details class=\"wp-block-details is-layout-flow wp-block-details-is-layout-flow\">\n<summary>Warning: flashing image<\/summary>\n<figure class=\"wp-block-image size-full\"><img data-recalc-dims=\"1\" decoding=\"async\" src=\"https:\/\/i0.wp.com\/contributor.insightmediagroup.io\/wp-content\/uploads\/2025\/04\/2025-04-25_21-42-16-ezgif.com-video-to-gif-converter.gif?ssl=1\" alt=\"\" class=\"wp-image-602363\"><\/figure>\n<p class=\"wp-block-paragraph\">\n<\/details>\n<p class=\"wp-block-paragraph\">Now why is this happening? The problem is that we\u2019re simultaneously trying to update the new frame while the internal refresh loop of the user interface might be using the information of the array to draw it on the screen. They are both competing for the same frame array.<\/p>\n<p class=\"wp-block-paragraph\">It is generally not a good idea to directly update the UI elements from a different thread, in some frameworks this might even be prevented and will raise exceptions. In <strong>Tkinter<\/strong>, we can do it, but we will get weird results. We need some type of <strong><em>synchronization<\/em><\/strong> between our threads. That\u2019s where the <code>Queue<\/code> comes into play.<\/p>\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" height=\"853\" width=\"1024\" decoding=\"async\" src=\"https:\/\/i0.wp.com\/contributor.insightmediagroup.io\/wp-content\/uploads\/2025\/04\/image-149-1024x853.png?resize=1024%2C853&#038;ssl=1\" alt=\"\" class=\"wp-image-602366\"><\/figure>\n<p class=\"wp-block-paragraph\">You\u2019re probably familiar with queues from the grocery store or theme parks. The concept of the queue here is very similar: the first element that goes into the queue also leaves first (<strong>F<\/strong>irst <strong>I<\/strong>n <strong>F<\/strong>irst <strong>O<\/strong>ut).<\/p>\n<p class=\"wp-block-paragraph\">In this case, we actually just want a queue with a single element, a single slot queue. The queue implementation in Python is <em>thread-safe<\/em>, meaning we can <strong>put<\/strong> and <strong>get<\/strong> objects from the queue from different threads. Perfect for our use case, the processing thread will put the image arrays to the queue and the GUI thread will try to get an element, but not block if the queue is empty.<\/p>\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-python\">class App(customtkinter.CTk):\n    def __init__(self) -&gt; None:\n        ...\n\n        self.queue = queue.Queue(maxsize=1)\n\n        self.webcam_thread = threading.Thread(target=self.run_webcam_loop, daemon=True)\n        self.webcam_thread.start()\n\n        self.frame_loop_dt_ms = 16  # ~60 FPS\n        self.after(self.frame_loop_dt_ms, self._update_frame)\n    \n    def _update_frame(self) -&gt; None:\n        \"\"\"\n        Update the frame in the image display widget.\n        \"\"\"\n        try:\n            frame = self.queue.get_nowait()\n            self.image_display.set_frame(frame)\n        except queue.Empty:\n            pass\n\n        self.after(self.frame_loop_dt_ms, self._update_frame)\n\n    def run_webcam_loop(self) -&gt; None:\n        ...\n\n        while True:\n            ...\n\n            self.queue.put(frame)<\/code><\/pre>\n<p class=\"wp-block-paragraph\">Notice how we move the direct call to the <code>set_frame<\/code> function from the webcam loop which runs in its own thread to the <code>_update_frame<\/code> function that is running on the main thread, repeatedly scheduled in <strong>16ms<\/strong> intervals. <\/p>\n<p class=\"wp-block-paragraph\">Here it\u2019s important to use the <code>get_nowait<\/code> function in the main thread, otherwise if we would use the get function, we would be blocking there. This call does <em>not block<\/em>, but raises a <code>queue.Empty <\/code>exception if there\u2019s no element to fetch so we have to catch this and ignore it. In the webcam loop, we can use the blocking put function because it doesn\u2019t matter that we <em>block<\/em> the <code>run_webcam_loop<\/code>, there\u2019s nothing else needing to run there.<\/p>\n<figure class=\"wp-block-image aligncenter size-full\"><img data-recalc-dims=\"1\" decoding=\"async\" src=\"https:\/\/i0.wp.com\/contributor.insightmediagroup.io\/wp-content\/uploads\/2025\/04\/Screencastfrom04-24-2025113343PM-ezgif.com-video-to-gif-converter-1.gif?ssl=1\" alt=\"\" class=\"wp-image-602620\"><\/figure>\n<p class=\"wp-block-paragraph\">And now everything is running as expected, no more flashing frames!<\/p>\n<h2 class=\"wp-block-heading\">Conclusion<\/h2>\n<p class=\"wp-block-paragraph\">Combining a UI framework like <strong>Tkinter<\/strong> with <strong>OpenCV<\/strong> allows us to build modern looking applications with an interactive graphical user interface. Due to the UI running in the main thread, we run the image processing in a separate thread and synchronize the data between the threads using a single-slot queue. You can find a cleaned up version of this demo in the repository below with a more modular structure. Let me know if you build something interesting with this approach. Take care!<\/p>\n<hr class=\"wp-block-separator has-alpha-channel-opacity is-style-dotted\">\n<hr class=\"wp-block-separator has-alpha-channel-opacity is-style-ornamental\">\n<p class=\"has-text-align-center wp-block-paragraph\"><em>Checkout the full source code in the GitHub repo:<\/em><\/p>\n<p class=\"has-text-align-center has-heading-6-font-size wp-block-paragraph\"><a href=\"https:\/\/github.com\/trflorian\/ctk-opencv\"><code>https:\/\/github.com\/trflorian\/ctk-opencv<\/code><\/a><\/p>\n<hr class=\"wp-block-separator has-alpha-channel-opacity is-style-ornamental\">\n<p>The post <a href=\"https:\/\/towardsdatascience.com\/modern-gui-applications-for-computer-vision-in-python\/\">Modern GUI Applications for Computer Vision in\u00a0Python<\/a> appeared first on <a href=\"https:\/\/towardsdatascience.com\/\">Towards Data Science<\/a>.<\/p>\n<\/div>\n<p> \t<BR><br \/>\n <BR><\/BR><br \/>\n    Florian Trautweiler<br \/>\n \t<BR><br \/>\n<BR><\/BR><br \/>\n<a href=\"https:\/\/towardsdatascience.com\/modern-gui-applications-for-computer-vision-in-python\/\">Go to original source<\/a><br \/>\n \t<BR><br \/>\n <BR><\/BR><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Modern GUI Applications for Computer Vision in\u00a0Python Introduction I\u2019m a huge fan of interactive visualizations. As a computer vision engineer, I deal almost daily with image processing related tasks and more often than not I am iterating on a problem where I need visual feedback to make decisions. Let\u2019s think of a very simple image [&hellip;]<\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[62,221,82,67,2525,157,2526],"tags":[226,647,845],"class_list":["post-3481","post","type-post","status-publish","format-standard","hentry","category-aimldsaimlds","category-computer-vision","category-data-visualization","category-deep-dives","category-opencv-python","category-python","category-tkinter","tag-computer","tag-cv","tag-image"],"_links":{"self":[{"href":"https:\/\/mailitics.com\/index.php\/wp-json\/wp\/v2\/posts\/3481"}],"collection":[{"href":"https:\/\/mailitics.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/mailitics.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/mailitics.com\/index.php\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/mailitics.com\/index.php\/wp-json\/wp\/v2\/comments?post=3481"}],"version-history":[{"count":0,"href":"https:\/\/mailitics.com\/index.php\/wp-json\/wp\/v2\/posts\/3481\/revisions"}],"wp:attachment":[{"href":"https:\/\/mailitics.com\/index.php\/wp-json\/wp\/v2\/media?parent=3481"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/mailitics.com\/index.php\/wp-json\/wp\/v2\/categories?post=3481"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/mailitics.com\/index.php\/wp-json\/wp\/v2\/tags?post=3481"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}