2

I am developing an psychology experiment that analyzes facial expressions made by users while completing a behavioral task. The application mostly runs through Tkinter, and i'm using openCV to capture video.

In a minimal case, i need to start and stop recording based on user responses. For example, in the code below, i want the user to specify when to start and stop recording video using the mouse to press a button.

import Tkinter as tk
import cv2

# -------begin capturing and saving video
def startrecording():
    cap = cv2.VideoCapture(0)
    fourcc = cv2.cv.CV_FOURCC(*'XVID')
    out = cv2.VideoWriter('output.avi',fourcc,  20.0, (640,480))

    while(cap.isOpened()):
        ret, frame = cap.read()
        if ret==True:
            out.write(frame)
        else:
            break

# -------end video capture and stop tk
def stoprecording():
    cap.release()
    out.release()
    cv2.destroyAllWindows()

    root.quit()
    root.destroy()

# -------configure window
root = tk.Tk()
root.geometry("%dx%d+0+0" % (100, 100))
startbutton=tk.Button(root,width=10,height=1,text='START',command = startrecording)
stopbutton=tk.Button(root,width=10,height=1,text='STOP', command = stoprecording)
startbutton.pack()
stopbutton.pack()

# -------begin
root.mainloop()

The problem is that OpenCV uses a loop to record video, during which Tkinter is unable to listen for user responses. The program gets stuck in the OpenCV loop and the user is unable to continue. How can i simultaneously record video and listen for user responses?

I have looked into parallel processing (e.g., Display an OpenCV video in tkinter using multiprocessing), but this sounds like a larger endeavor than seems necessary.

I have also looked into using root.after command (e.g., Show webcam sequence TkInter), but using this it appears that you can only capture a single frame, whereas i want a video.

Is there another way? will i need to use two processing streams?

Community
  • 1
  • 1
Nolan Conaway
  • 2,639
  • 1
  • 26
  • 42

1 Answers1

4

Handling this via multiprocessing is easier than you think:

import multiprocessing
import Tkinter as tk
import cv2

e = multiprocessing.Event()
p = None

# -------begin capturing and saving video
def startrecording(e):
    cap = cv2.VideoCapture(0)
    fourcc = cv2.cv.CV_FOURCC(*'XVID')
    out = cv2.VideoWriter('output.avi',fourcc,  20.0, (640,480))

    while(cap.isOpened()):
        if e.is_set():
            cap.release()
            out.release()
            cv2.destroyAllWindows()
            e.clear()
        ret, frame = cap.read()
        if ret==True:
            out.write(frame)
        else:
            break

def start_recording_proc():
    global p
    p = multiprocessing.Process(target=startrecording, args=(e,))
    p.start()

# -------end video capture and stop tk
def stoprecording():
    e.set()
    p.join()

    root.quit()
    root.destroy()

if __name__ == "__main__":
    # -------configure window
    root = tk.Tk()
    root.geometry("%dx%d+0+0" % (100, 100))
    startbutton=tk.Button(root,width=10,height=1,text='START',command=start_recording_proc)
    stopbutton=tk.Button(root,width=10,height=1,text='STOP', command=stoprecording)
    startbutton.pack()
    stopbutton.pack()

    # -------begin
    root.mainloop()

All we've done is added a call to multiprocessing.Process so that your video capture code runs in a child process, and moved the code to clean up when the capturing is done into that process as well. The only additional wrinkle compared to the single-process version is the use of a multiprocessing.Event to signal the child process when its time to shut down, which is necessary because the parent process doesn't have access to out or cap.

You could try using threading instead (just replace multiprocessing.Process with threading.Thread, and multiprocessing.Event with threading.Event), but I suspect the GIL will trip you up and hurt the performance of the GUI thread. For the same reason, I don't think it's worth trying to integrate reading/writing the streams into your event loop via root.after - it's only going to hurt performance, and since you're not trying to integrate what you're doing into the GUI itself, there's no reason to try to keep it in the same thread/process as the event loop.

dano
  • 91,354
  • 19
  • 222
  • 219
  • Thanks, i will try this! though i'm still hoping for a single process solution – Nolan Conaway Aug 06 '14 at 01:06
  • @nolanconaway No problem. As I mentioned in my answer, my solution can be converted to a single-process (but multi-threaded) solution by using objects from the `threading` module instead of the `multiprocessing` module. Give it a try while you're testing the `multiprocessing` approach. I'm worried that the GIL will make the performance poor, but your worker looks to be I/O-bound, which means the GIL should be released most of the time the thread is running. – dano Aug 06 '14 at 01:21
  • I had to edit the sandbox script quite a bit to make this compatible with windows, but it now works perfectly. Implementing multiprocessing in my larger and more complicated application, however, has been much more difficult. Specifically, i find that my target process is always skipped, and instead the second process becomes a glitchy version of the first. I am able to emulate this process in the toy script by placing "if _name_ == '_main_'..." before p.start() instead of root.mainloop(), but am otherwise quite clueless. Do you have any intuition? – Nolan Conaway Aug 09 '14 at 03:11
  • @nolanconaway It's probably because you need to protect the entry-point to the script with an `if __name__ == "__main__":` guard. I actually forgot to include it in my answer (sorry!). Because Windows lacks `os.fork`, it needs to re-import your main module to spawn the child process you create via `multiprocessing.Process`. If you don't protect the entry-point of the script with `if __name__ == "__main__":`, all that code will end up getting executed again in the child. That sounds a lot like "*instead the second process becomes a glitchy version of the first*". – dano Aug 09 '14 at 03:24
  • @nolanconaway See [here](https://docs.python.org/2/library/multiprocessing.html#windows) for more info on how to keep Windows compatibility with `multiprocessing`. – dano Aug 09 '14 at 03:25