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

Viewed 992

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

2 Answers

I tried to read the frames in openCV and display them in tkinter label. I was able to do so using the below code:

import tkinter as tk
import cv2
from PIL import ImageTk, Image

video_path = "SAMPLE/STORED_VIDEO/PATH"

root = tk.Tk()
base_img = Image.open("PATH/TO/DEFAULT/LABLE/IMAGE")
img_obj = ImageTk.PhotoImage(base_img)
lblVideo = tk.Label(root, image=img_obj)
lblVideo.pack()
cap = cv2.VideoCapture(video_path)

if cap.isOpened():
    def show_frame():
        _, frame = cap.read()
        im = Image.fromarray(frame)
        img = ImageTk.PhotoImage(im)
        lblVideo.configure(image=img)
        lblVideo.image = img
        lblVideo.after(1, show_frame)  # Need to create callback here   
    show_frame()
root.mainloop()

Although this doesnot contain the with statement, you can try replacing the after() callback inside the show_frame function itself.

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(),)
Related