Search code examples
pythontkintervideocameraalliedvision

How can I preview streaming images in tkinter with Allied Vision camera that uses Vimba SDK?


I want to display images from an Allied Vision camera inside a tkinter frame using OpenCV and the SDK for the camera, VimbaPython.

The only possible way to initialize the camera is with a Python with statement:

with Vimba.get_instance() as vimba:
    cams = vimba.get_all_cameras()
    with cams[0] as camera:
        camera.get_frame()
        # Convert frame to opencv image, then use Image.fromarray and ImageTk.PhotoImage to
        # display it on the tkinter GUI

Everything works fine so far. But I don't only need a single frame. Instead, I need to continuously get frames and display them on the screen so that it is streaming. I found that one way to do it is to call the .after(delay, function) method from a tkinter Label widget. So, after obtaining one frame, I want to call the same function to get a new frame and display it again. The code would look like that:

with Vimba.get_instance() as vimba:
    cams = vimba.get_all_cameras()
    with cams[0] as camera:

        def show_frame():
            frame = camera.get_frame()
            frame = frame.as_opencv_image()
            im = Image.fromarray(frame)
            img = Image.PhotoImage(im)
            lblVideo.configure(image=img)   # this is the Tkinter Label Widget
            lblVideo.image = img

        show_frame()
        lblVideo.after(20, show_frame)

Then this shows the first frame and stops, throwing an error saying that Vimba needs to be initialized with a with statement. I don't know much about Python, but it looks like when I call the function with the .after() method it ends the with statement.

I would like to know if it is possible to execute this show_frame() function without ending the with. Also, I can't initialize the camera every time because the program goes really slow. Thank you


Solution

  • You need to use thread to run the capture code and pass the frames read via queue. Then the main tkinter application reads the queue and show the frames periodically using .after().

    Below is an example based on your posted code:

    import threading
    from queue import SimpleQueue
    import tkinter as tk
    from PIL import Image, ImageTk
    from vimba import Vimba
    
    def camera_streaming(queue):
        global is_streaming
        is_streaming = True
        print("streaming started")
        with Vimba.get_instance() as vimba:
            with vimba.get_all_cameras()[0] as camera:
                while is_streaming:
                    frame = camera.get_frame()
                    frame = frame.as_opencv_image()
                    im = Image.fromarray(frame)
                    img = ImageTk.PhotoImage(im)
                    queue.put(img) # put the capture image into queue
        print("streaming stopped")
    
    def start_streaming():
        start_btn["state"] = "disabled" # disable start button to avoid running the threaded task more than once
        stop_btn["state"] = "normal"    # enable stop button to allow user to stop the threaded task
        show_streaming()
        threading.Thread(target=camera_streaming, args=(queue,), daemon=True).start()
    
    def stop_streaming():
        global is_streaming, after_id
        is_streaming = False  # terminate the streaming thread
        if after_id:
            lblVideo.after_cancel(after_id) # cancel the showing task
            after_id = None
        stop_btn["state"] = "disabled" # disable stop button
        start_btn["state"] = "normal"  # enable start button
    
    # periodical task to show frames in queue
    def show_streaming():
        global after_id
        if not queue.empty():
            image = queue.get()
            lblVideo.config(image=image)
            lblVideo.image = image
        after_id = lblVideo.after(20, show_streaming)
    
    queue = SimpleQueue() # queue for video frames
    after_id = None
    
    root = tk.Tk()
    
    lblVideo = tk.Label(root, image=tk.PhotoImage(), width=640, height=480)
    lblVideo.grid(row=0, column=0, columnspan=2)
    
    start_btn = tk.Button(root, text="Start", width=10, command=start_streaming)
    start_btn.grid(row=1, column=0)
    
    stop_btn = tk.Button(root, text="Stop", width=10, command=stop_streaming, state="disabled")
    stop_btn.grid(row=1, column=1)
    
    root.mainloop()
    

    Note that I don't have the camera and the SDK installed, the above code may not work for you. I just demonstrate how to use thread, queue and .after().

    Below is a testing vimba module (saved as vimba.py) I use to simulate VimbaPython module using OpenCV and a webcam:

    import cv2
    
    class Frame:
        def __init__(self, frame):
            self.frame = frame
    
        def as_opencv_image(self):
            return self.frame
    
    class Camera:
        def __init__(self, cam_id=0):
            self.cap = cv2.VideoCapture(cam_id, cv2.CAP_DSHOW)
    
        def __enter__(self):
            return self
        
        def __exit__(self, *args):
            self.cap.release()
            return self
    
        def get_frame(self):
            ret, frame = self.cap.read()
            if ret:
                return Frame(frame)
    
    class Vimba:
        _instance = None
        
        @classmethod
        def get_instance(self):
            if self._instance is None:
                self._instance = Vimba()
            return self._instance
    
        def __enter__(self):
            return self
    
        def __exit__(self, *args):
            return self
    
        def get_all_cameras(self):
            return (Camera(),)