Search code examples
pythontkinter

How to improve user experience by preparing the next image in advance for a viewer created?


I created a Tkinter program that displays images processed with OpenCV. The Tkinter GUI has buttons for selecting a folder, viewing the next image, and viewing the previous image, and a canvas showing these images. And when a person checks what type of image this image is using a radio button, the record is saved.

Here's what it looks like for users:

  1. When a person presses the view next image button,
  2. Load the image,
  3. Process with OpenCV (CPU: 0.8 seconds),
  4. Show the image on the canvas.
  5. Check the image and check the radio button (Human: 1 second)
  6. Goto 1

During the 1 second that 5's human makes the decision, I want to start processing and preparing the next image of 3, and then display it immediately when I press the button. I think that will dramatically improve the user experience.

How can I make it? I would appreciate it if you could tell me at least a keyword about what technologies I should look into.


I modified the program based on Ahmed AEK's answer. It worked well and obviously improved the user experience quite a bit by preparing the next image in advance. However, the canvas.draw() part, which draws a pre-prepared image in Tkinter, takes up time on its own. So I couldn't go far in improving it to a level where there is absolutely no delay. This is my fault for not thoroughly analyzing the time for each step in advance.

I couldn't try Redis because there was an initial entry barrier. Thank you both for your excellent answers.


Solution

  • you should be using multithreading, ie: by pushing work to other thread, so that the main thread will not be blocked, the most reliable way is using a Threadpool as follows.

    from multiprocessing.pool import ThreadPool
    
    class ImageRepository:
        def __init__(self):
            self.next_image_future = None
            self.threadpool = ThreadPool(1)  # 1 worker, we don't need more
    
        def start_image_processing(self):
            # initialize and start the first task
            self.next_image_future = self.threadpool.apply_async(self.load_next_image)
    
        def get_next_image(self):
            if self.next_image_future is not None:
                # get the image
                new_image = self.next_image_future.get()
    
                # submit task for the next one in future 
                self.next_image_future = \
                    self.threadpool.apply_async(self.load_next_image)
    
                return new_image
    
        def load_next_image(self):
            print("loading and processing image")
            return 5  # new image object
    
    repo = ImageRepository()
    repo.start_image_processing()  # start grabbing the first image
    
    # get next image and initiate work for the one after it
    new_image = repo.get_next_image()  
    

    and so every time you get an image, you will be assigning a task to another thread to load and process the next one.