You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

原因分析

  1. tkinter GUI操作的线程限制:tkinter的所有GUI相关操作(包括ImageTk.PhotoImage创建、Canvas更新)必须在主线程执行,子线程中调用这些操作会导致线程内部的tkinter事件循环阻塞,无法响应退出信号
  2. 退出信号检测不及时: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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.22 17:27:46