Python多线程摄像头程序:ShowVideo线程无法随tkinter窗口关闭终止
问题:多线程摄像头视频显示程序关闭时ShowVideo线程无法终止
问题详情
- 程序通过双线程实现:
CaptureVideo线程负责摄像头视频采集,ShowVideo线程负责将采集到的视频帧显示在tkinter窗口中 - 点击窗口关闭按钮时,
CaptureVideo线程能正常终止,但ShowVideo线程始终无法退出 - 经排查,
ShowVideo类中的ImageTk.PhotoImage(image=image)语句是导致线程无法终止的原因,移除该语句线程可正常停止,但该语句是视频显示的核心功能,不能删除 - 运行环境:Python 3.10.4,Pillow 9.5.0,Windows 11 22H2
程序运行输出:
> py .\main.py Start thread:thread_capture_video Start thread:thread_show_video Exit thread:thread_capture_video
原因分析
- tkinter GUI操作的线程限制:tkinter的所有GUI相关操作(包括
ImageTk.PhotoImage创建、Canvas更新)必须在主线程执行,子线程中调用这些操作会导致线程内部的tkinter事件循环阻塞,无法响应退出信号 - 退出信号检测不及时:
ShowVideo线程的循环中,当队列空时会释放锁,但如果卡在ImageTk.PhotoImage创建步骤,就无法检测THREAD_EXIT_SIGNAL,导致线程无法退出
修复方案
核心思路是把UI更新操作移到主线程,子线程只处理图像数据,同时优化线程退出逻辑:
ShowVideo线程不再直接更新UI,而是将处理好的Image对象传递给主线程,由主线程通过after()方法完成PhotoImage创建和Canvas更新- 给队列的
get()方法添加超时,确保线程能定期检查退出信号 - 优化全局退出信号的检测时机,避免线程长时间阻塞
修复后的完整代码
import tkinter as tk from PIL import ImageTk, Image import queue import threading import cv2 class Application(tk.Frame): def createWidgets(self): self.canvas = tk.Canvas(master=self, width=640, height=480) self.canvas.pack(side=tk.TOP, expand=True, fill=tk.BOTH) # 保存PhotoImage引用,避免被GC回收 self.photo = None def set_canvas(self, image): self.photo = ImageTk.PhotoImage(image=image) self.canvas.create_image(0, 0, image=self.photo, anchor='nw') def __init__(self, master=None): tk.Frame.__init__(self, master) self.pack(fill=tk.X) self.createWidgets() class CaptureVideo(threading.Thread): def __init__(self, threadID, name, data_queue, exit_signal): super().__init__() self.threadID = threadID self.name = name self.daemon = True self.data_queue = data_queue self.exit_signal = exit_signal def run(self): print(f"Start thread:{self.name}") self.capture_data() print(f"Exit thread:{self.name}") def capture_data(self): cap = cv2.VideoCapture(0, cv2.CAP_DSHOW) while cap.isOpened() and not self.exit_signal[0]: _, frame = cap.read() if frame is None: continue frame = cv2.flip(frame, 1) # 转换颜色空间后放入队列 try: self.data_queue.put(cv2.cvtColor(frame, cv2.COLOR_BGR2RGBA), timeout=0.1) except queue.Full: # 队列满时丢弃旧帧,避免阻塞 pass cap.release() class ShowVideo(threading.Thread): def __init__(self, threadID, name, data_queue, exit_signal, app): super().__init__() self.threadID = threadID self.name = name self.daemon = True self.data_queue = data_queue self.exit_signal = exit_signal self.app = app def run(self): print(f"Start thread:{self.name}") self.process_data() print(f"Exit thread:{self.name}") def process_data(self): while not self.exit_signal[0]: try: # 队列get添加超时,定期检查退出信号 data = self.data_queue.get(timeout=0.1) image = Image.fromarray(data) # 通知主线程更新UI self.app.master.after(0, self.app.set_canvas, image) except queue.Empty: continue def change_thread_exit_signal(): # 使用列表实现可变对象传递退出信号 exit_signal[0] = True tk_root.destroy() if __name__ == "__main__": tk_root = tk.Tk() tk_root.protocol("WM_DELETE_WINDOW", change_thread_exit_signal) form = Application(master=tk_root) # 使用列表存储退出信号,实现线程间可变引用 exit_signal = [False] queue_video_data = queue.Queue(10) # 缩小队列容量,避免内存占用过高 thread_capture_video = CaptureVideo(1, 'thread_capture_video', queue_video_data, exit_signal) thread_show_video = ShowVideo(2, 'thread_show_video', queue_video_data, exit_signal, form) thread_capture_video.start() thread_show_video.start() form.mainloop() thread_capture_video.join() thread_show_video.join()
关键修复点说明
- UI操作移至主线程:通过
self.app.master.after(0, self.app.set_canvas, image)将UI更新任务提交给主线程的事件循环执行,避免子线程调用tkinter API - 退出信号使用可变对象:用列表
exit_signal代替全局变量,确保线程能实时读取到最新的退出状态 - 队列操作添加超时:
data_queue.get(timeout=0.1)让线程不会一直阻塞在队列等待,能定期检查退出信号 - 保存PhotoImage引用:在Application类中保存
self.photo,避免PhotoImage被Python垃圾回收机制回收导致图像不显示
内容的提问来源于stack exchange,提问作者Peter
相关产品推荐
相关产品推荐

