skip to Main Content

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.

2

Answers


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

    Login or Signup to reply.
  2. I think you could make a very elegant solution using Redis which is a lightning-fast, in memory data-structure server – ideally suited to caching and prefetching. It has clients for C/C++, Python, Ruby, bash, Java and so on. In your case I would use it to store 2 data-structures:

    • a queue of images that need processing – this would be a Redis LIST
    • a store of processed images – these would be STRINGS in Redis parlance. But don’t let that put you off, you can use them to store strings, integers, floats, JSONs, HTML pages, images and so on. Each one can be up to 512MB in size and they are able to store binary data such as images.

    The basic idea would be to let the Tk app push a list of image names into the work queue on application startup and whenever the user changes directory, for example. And then pick up the processed image from Redis whenever it needs to display one.

    You would then start a worker that purely sits waiting for image names to appear in a queue and processes them and stores the result in a fast Redis cache ready for the Tk app to use.

    This has some advantages:

    • scalability – if you want better performance, you can simply add more worker processes, the code would be unchanged
    • decoupling – the image processing is completely decoupled from the Tk app, which is a benefit because of the way Python scales poorly with the GIL
    • simplicity – the processed images can be stored in Redis with a TTL "Time to Live", which means they get deleted automatically after 10s or 5 minutes, without needing any code and so that RAM doesn’t fill up. Also, if multiple users are viewing the images, the caching and processing won’t be repeated. Also, if the user goes backwards or forwards, any previously processed images will still be there instead of just the next one, single image.

    So the code for an image processing worker would be like this, and you can just run multiple, identical workers if you want faster throughput:

    connect to Redis
    while True:
        # Wait for image, popping next from queue
        image = BRPOP from work queue (implemented as Redis LIST)
    
        ... do processing with OpenCV
    
        # Store processed result as Redis STRING with expiry in 10 minutes
        SET imagename processedImage EX 600
    

    BRPOP is described here.

    SET is described here.


    The code within Tk, would look like this for pushing one or more filenames onto the job queue:

    connect to Redis
    
    LPUSH one or more filenames to work queue in Redis
    

    LPUSH is described here.

    And when wanting to display a processed image, check it exists in Redis and grab it if it does. If it doesn’t exist in Redis, re-request – probably with RPUSH so it gets higher priority, or process it directly yourself and store result in Redis

    GET processed image
    if image is NULL:
        re-request as above
        wait till ready
    

    GET is described here.


    Because Redis has a bash (i.e. command-line) client, it also makes this simple to debug. You can check the length of the work queue from your Terminal. You can push items onto the work queue and delete items from the work queue. You can see if an image has been processed and so on, from outside your app and without any need to change it.

    Examples:

    Let’s suppose you call your work queue WQUEUE and want to see its length, just using the Terminal:

    redis-cli LLEN WQUEUE
    

    Or you want to list all the items in the queue:

    redis-cli LRANGE WQUEUE 0 -1
    

    Or you want to push the image filename londonbridge.jpg into the work queue:

    redis-cli LPUSH WQUEUE londonbridge.jpg
    

    Or you want to see if image londonbridge.jpg has been processed and extract it into a file if so:

    redis-cli GET londonbridge.jpg > processedImage.jpg
    

    Or you want to see the names of all the processed images:

    redis-cli KEYS '*'
    

    Note that Redis is networked, so you can run Redis on one machine and connect to it from any other machine by specifying its hostname or IP address:

    # Get length of work queue on host 192.168.0.10
    redis-cli -h 192.168.0.10 LLEN WQUEUE
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search