1
votes

I am trying to build a GUI using tkinter in python 3.6.4 64-bit on Windows 8 by integrating opencv components into the program. I can get the video to play, but there is significant flicker going on. Namely, a screen the same color as the native tkinter background shows up briefly several times a second. I've tested several cameras with similar results, and double-checked that the cameras work properly via native video playback software. Here is my code:

from tkinter import *
from PIL import Image, ImageTk
import cv2
import threading

cap = cv2.VideoCapture(0)

root = Tk()
def videoLoop():
    global root
    global cap
    vidLabel = None
    while True:
        ret, frame = cap.read()
        frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        frame = Image.fromarray(frame)
        frame = ImageTk.PhotoImage(frame)
        if vidLabel: vidLabel.configure(image=frame)
        else:
            vidLabel = Label(root, image=frame, anchor=NW)
            vidLabel.pack(expand=YES, fill=BOTH)

videoThread = threading.Thread(target=videoLoop, args=())
videoThread.start()
root.mainloop()

Could anyone suggest what I might be doing wrong? I did hear that tkinter doesn't always play well with threads, but that's about all I can think of. In response to a comment suggesting the flicker is caused by label updating, I added some code that still reads from video and updates the label within the loop, but uses an image loaded outside of the loop to update the label. The flicker then goes away, although (to my understanding) the efficiency of the loop and updating of the label hasn't changed. Here is the changed videoLoop function (with flicker gone): def videoLoop(): global root vidLabel = None cap = cv2.VideoCapture(0)

    ret, frame = cap.read()
    frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
    frame = Image.fromarray(frame)
    frame = ImageTk.PhotoImage(frame)

    while True:
        ret, lame = cap.read()
        lame = cv2.cvtColor(lame, cv2.COLOR_BGR2RGB)
        lame = Image.fromarray(lame)
        lame = ImageTk.PhotoImage(lame)

        if vidLabel:
            vidLabel.configure(image=None)
            vidLabel.configure(image=frame)
        else:
            vidLabel = Label(root, image=frame, anchor=NW)
            vidLabel.pack(expand=YES, fill=BOTH)
2
Just for your code(not for question), do you make your code indent of this line: cap.release() is correct?Kinght 金
It wasn't indented correctly, thank you for catching that. I deleted it completely (flicker persists) since I don't really need to release the camera.anvoice
No wrong, just because it is not efficient. Two reasons: (1) read from video is time costing (about 100ms per frame on my PC) (2) label updating is time costing (about 100 ms per update). Then in one loop, those two step cost about 200 ms. When image label updates, it flickers.Kinght 金
Don't think that's true. I tried adjusting the code so that it still reads from video in loop, and updates the label, but uses an image loaded outside the loop to update. No flicker that way, but there would be if the update was causing it I believe. I'll update question to reflect this.anvoice
I have the time costing data, while you don't.Kinght 金

2 Answers

4
votes

Solution: Make sure the image configure call precedes storing the image in the label image attribute. I've managed to solve this issue, however I don't fully understand WHY it works as I am a python/tkinter novice. I will post the solution for now and will update the answer when I manage to find a proper explanation to this behavior. My best guess is that the storing of the image in the label attribute is what actually causes it to update on the screen, while the configure method just declares that there will be an image attached, which causes the loop to have to go through another iteration before getting to the update image update statement. The below code works fine without flicker:

from tkinter import *
from PIL import Image, ImageTk
import cv2
import threading

cap = cv2.VideoCapture(0)

root = Tk()
def videoLoop():
    global root
    global cap
    vidLabel = Label(root, anchor=NW)
    vidLabel.pack(expand=YES, fill=BOTH)
    while True:
        ret, frame = cap.read()
        frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        frame = Image.fromarray(frame)
        frame = ImageTk.PhotoImage(frame)
        vidLabel.configure(image=frame)
        vidLabel.image = frame

videoThread = threading.Thread(target=videoLoop, args=())
videoThread.start()
root.mainloop()
0
votes

In tkinter in order to display images, the image needs to have a global reference reference that doesn't go out of scope and then gets garbage-collected, perhaps flickering is caused by lack of such reference. See below code that has such reference to the image, and also ditched the if/else with better structuring:

from tkinter import *
from PIL import Image, ImageTk
import cv2
import threading

cap = cv2.VideoCapture(0)

root = Tk()
def videoLoop():
    global root
    global cap
    vidLabel = Label(root, anchor=NW)
    vidLabel.pack(expand=YES, fill=BOTH)
    while True:
        ret, frame = cap.read()
        frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        frame = Image.fromarray(frame)
        vidLabel.image = ImageTk.PhotoImage(frame)
        vidLabel.configure(image=vidLabel.image)

videoThread = threading.Thread(target=videoLoop, args=())
videoThread.start()
root.mainloop()