-1

I'm pretty new to python and espcially tkinter and opencv.

I've got someway (a little way) to creating a gui that will eventually control a microscope and ai. But I've hit a stumbling block, trying to record the video that is displayed within the gui, I think its to do with the video feed already been captured in the display, but I can't find a way around it. It all works fine until I hit record then it crashes and i get the error: open VIDEOIO(V4L2:/dev/video0): can't open camera by index.

Apologies for the long code but I've cut it down to as much as I think possible.

The problem is in the root.recbtn and def rec sections.

import cv2
import tkinter as tk
import multiprocessing
from tkinter import *
from PIL import Image,ImageTk
from datetime import datetime
from tkinter import messagebox, filedialog

e = multiprocessing.Event()
p = None

# Defining CreateWidgets() function to create necessary tkinter widgets
def createwidgets():

    root.cameraLabel = Label(root, bg="gray25", borderwidth=3, relief="ridge")
    root.cameraLabel.grid(row=2, column=1, padx=10, pady=10, columnspan=3)

    root.browseButton = Button(root, bg="gray25", width=10, text="BROWSE", command=destBrowse)
    root.browseButton.grid(row=1, column=1, padx=10, pady=10)

    root.recbtn = Button(root, bg="gray25", width=10, text="Record", command=rec)
    root.recbtn.grid(row=1, column=5, padx=10, pady=10)

    root.saveLocationEntry = Entry(root, width=55, textvariable=destPath)
    root.saveLocationEntry.grid(row=1, column=2, padx=10, pady=10)

    # Calling ShowFeed() function
    ShowFeed()

# Defining ShowFeed() function to display webcam feed in the cameraLabel;

def ShowFeed():
    # t5  # Capturing frame by frame
    ret, frame = root.cap.read()

    if ret:
        # Flipping the frame vertically
        frame = cv2.flip(frame, 1)

        # Changing the frame color from BGR to RGB
        cv2image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGBA)

        # Creating an image memory from the above frame exporting array interface
        videoImg = Image.fromarray(cv2image)

        # Creating object of PhotoImage() class to display the frame
        imgtk = ImageTk.PhotoImage(image = videoImg)

        # Configuring the label to display the frame
        root.cameraLabel.configure(image=imgtk)

        # Keeping a reference
        root.cameraLabel.imgtk = imgtk

        # Calling the function after 10 milliseconds
        root.cameraLabel.after(10, ShowFeed)
    else:
        # Configuring the label to display the frame
        root.cameraLabel.configure(image='')

def destBrowse():
    # Presenting user with a pop-up for directory selection. initialdir argument is optional
    # Retrieving the user-input destination directory and storing it in destinationDirectory
    # Setting the initialdir argument is optional. SET IT TO YOUR DIRECTORY PATH
    destDirectory = filedialog.askdirectory(initialdir="YOUR DIRECTORY PATH")

    # Displaying the directory in the directory textbox
    destPath.set(destDirectory)

def rec():
    vid_name = datetime.now().strftime('%d-%m-%Y %H-%M-%S')

    # If the user has selected the destination directory, then get the directory and save it in image_path
    if destPath.get() != '':
        vid_path = destPath.get()
    # If the user has not selected any destination directory, then set the image_path to default directory
    else:
        messagebox.showerror("ERROR", "No Directory Selected!")

    # Concatenating the image_path with image_name and with .jpg extension and saving it in imgName variable
    vidName = vid_path + '/' + vid_name + ".avi"
    capture = cv2.VideoCapture(0)

    fourcc = cv2.VideoWriter_fourcc('X', 'V', 'I', 'D')
    videoWriter = cv2.VideoWriter(vidName, fourcc, 30.0, (640, 480))

    while (True):

        ret, frame = capture.read()

        if ret:
            cv2.imshow('video', frame)
            videoWriter.write(frame)

        if cv2.waitKey(1) == 27:
            break

    capture.release()
    videoWriter.release()

# Creating object of tk class
root = tk.Tk()

# Creating object of class VideoCapture with webcam index
root.cap = cv2.VideoCapture(0)

# Setting width and height
width, height = 1200, 1200
root.cap.set(cv2.CAP_PROP_FRAME_WIDTH, width)
root.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, height)

# Setting the title, window size, background color and disabling the resizing property
root.title("Test-AI-tes")
root.geometry("1600x1024")
root.resizable(True, True)
root.configure(background = "gray18")

# Creating tkinter variables
destPath = StringVar()
imagePath = StringVar()

createwidgets()
root.mainloop()

Thanks!

Bryan Oakley
  • 370,779
  • 53
  • 539
  • 685
Paul Tansley
  • 171
  • 7
  • 1
    Why don't you reuse `root.cap` instead of creating a new one here: `capture = cv2.VideoCapture(0)`. Also apart from the fact that you shouldn't be using `while True` loops when using `tkinter`, the question doesn't seem to focus on `tkinter`. If so please remove the `tkinter` tag – TheLizzard Aug 13 '21 at 11:16
  • Do you want to show the video in real-time in the tkinter window or do you want to save the frames then display it? – Art Aug 13 '21 at 11:54
  • Thanks for the reply, switched to capture = root.cap following the comment by @TheLizzard , it records but the real time video freezes. I would like the video to remain in real time but also be able to save parts of the feed when record is hit. – Paul Tansley Aug 13 '21 at 11:58
  • @PaulTansley Look at [this](https://stackoverflow.com/a/459131/11106801). Implement that `while True` loop as a `tkinter` loop. – TheLizzard Aug 13 '21 at 12:00
  • Thanks @TheLizzard. I think I'm pretty much there now, just need to define a stop recording function. – Paul Tansley Aug 13 '21 at 12:10

2 Answers2

1

This answer is similar to @Art's answer but I removed the after_id and queue.

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


def stop_rec():
    global running
    running = False

    start_button.config(state="normal")
    stop_button.config(state="disabled")

def start_capture():
    global capture, last_frame

    capture = cv2.VideoCapture(0)
    
    fourcc = cv2.VideoWriter_fourcc("X", "V", "I", "D")
    video_writer = cv2.VideoWriter(r"sample.avi", fourcc, 30.0, (640, 480))

    while running:
        rect, frame =  capture.read()

        if rect:
            cv2image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGBA)
            last_frame = Image.fromarray(cv2image)
            video_writer.write(frame)

    capture.release()
    video_writer.release()

def update_frame():
    if last_frame is not None:
        tk_img = ImageTk.PhotoImage(master=video_label, image=last_frame)
        video_label.config(image=tk_img)
        video_label.tk_img = tk_img

    if running:
        root.after(10, update_frame)

def start_rec():
    global running

    running = True
    thread = threading.Thread(target=start_capture, daemon=True)
    thread.start()
    update_frame()

    start_button.config(state="disabled")
    stop_button.config(state="normal")

def closeWindow():
    stop_rec()
    root.destroy()


running = False
after_id = None
last_frame = None

root = tk.Tk()
root.protocol("WM_DELETE_WINDOW", closeWindow)

video_label = tk.Label()
video_label.pack(expand=True, fill="both")

start_button = tk.Button(text="Start", command=start_rec)
start_button.pack()
stop_button = tk.Button(text="Stop", command=stop_rec, state="disabled")
stop_button.pack()

root.mainloop()

It uses the boolean flag running instead of using after_id. Also instead of storing the images in a queue then showing it, I only keep the last image. That way it can run in real time on my computer. Don't worry all of the frames are still being stored in the video file.

TheLizzard
  • 7,248
  • 2
  • 11
  • 31
0

You cannot have infinite while loop along with the GUI's loop. You should instead make use of threading, whenever you have an IO operation to complete.

Example code:

import cv2
import threading
import tkinter as tk
from PIL import Image, ImageTk
from queue import Queue

def stop_rec():
    global running, after_id
    running = False

    if after_id:
        root.after_cancel(after_id)
        after_id = None
        
    with frame_queue.mutex:
        frame_queue.queue.clear()

def start_capture():
    global capture

    capture = cv2.VideoCapture(0)
    
    fourcc = cv2.VideoWriter_fourcc('X', 'V', 'I', 'D')
    video_writer = cv2.VideoWriter(r"sample.avi", fourcc, 30.0, (640, 480))

    while running:
        
        rect, frame =  capture.read()

        if rect:
            cv2image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGBA)
            videoImg = Image.fromarray(cv2image)
            # current_frame = ImageTk.PhotoImage(image = videoImg)
            frame_queue.put(videoImg)
            video_writer.write(frame)
            
    capture.release()
    video_writer.release()

def update_frame():
    global after_id

    if not frame_queue.empty():
        video_label.image_frame = ImageTk.PhotoImage(frame_queue.get_nowait())
        video_label.config(image=video_label.image_frame)
    
    after_id = root.after(10, update_frame)

def start_rec():
    global running

    stop_rec()

    running = True
    thread = threading.Thread(target=start_capture, daemon=True)
    thread.start()
    update_frame()


def closeWindow():
    stop_rec()
    root.destroy()


running = False
after_id = None
frame_queue = Queue()

root = tk.Tk()
root.protocol("WM_DELETE_WINDOW", closeWindow)

video_label = tk.Label()
video_label.pack(expand=True, fill="both")

tk.Button(text="Start", command=start_rec).pack()
tk.Button(text="stop", command=stop_rec).pack()

root.mainloop()

Quick explanation:

  • start the record function in a new thread.
  • Use Queue to store the frames.
  • Then make use of [widget].after to update the label at regular intervals.
  • To stop the recording make use of [widget].after_cancel(after_id)(after_id is returned when you use .after method) and set the running variable to False to stop the loop.
Art
  • 2,836
  • 4
  • 17
  • 34
  • Is it more intuitive to use a boolean variable like `recording` instead of the `.after_cancel`? – TheLizzard Aug 13 '21 at 12:32
  • @TheLizzard I didn't quite get that. Do you mean to ask if it is better to remove the `after_cancel` and use `running=True` as a check before `after` method? – Art Aug 13 '21 at 12:37
  • Not 100% sure what you mean but I think so. I will write my own answer. Also you shouldn't call `ImageTk.PhotoImage` from the other thread. It counts as a `tkinter` function call and `tkinter` isn't thread safe. – TheLizzard Aug 13 '21 at 12:39
  • @TheLizzard Thx for the info, I wasn't aware of that. – Art Aug 13 '21 at 12:45
  • Also this doesn't run in real time for me. There is a significant lag that increases with time. I think `tkinter` is too slow when pulling images from the queue. If that is true, I wouldn't be surprised if the memory usage also keep increasing. – TheLizzard Aug 13 '21 at 12:50
  • @TheLizzard tkinter doesn't get values from the queue, its is the get method that gets the frame. I simply don't see a significant lag, might be the system you are running in. The size of queue increases to max 3 image objects as there is 10 ms gap in each update. The queue is used to make sure that it doesn't miss any frames. The code you provided is fine if you don't require all the frames to be displayed, your current code will always overwrite the previous image even before it has a chance to be displayed, hence will miss out on some frames. – Art Aug 13 '21 at 13:06
  • It's not a problem if you skip frames when showing it to the user. I have never seen a problem caused by frames being skipped when displaying a video. It might look a bit choppy but it will be real-time. It's better than to have more than 1 sec of lag when the program has been running for 5 sec. Also I know my computer is bad. – TheLizzard Aug 13 '21 at 13:13
  • @TheLizzard Yes, as I said it's not a problem if OP doesn't mind some frames not being displayed. Besides, If OP wants he/she can always set the max-size of the queue – Art Aug 13 '21 at 13:21