You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Tkinter多线程冻结问题排查及GUI帧显示方案咨询

Fixing Your Tkinter GUI Freeze & Integrating Frame Display into Tkinter

Hey there! Let's break down and solve your two key issues: the frozen Tkinter GUI when starting capture, and moving frame display from cv2 windows into your Tkinter interface.

1. Why Your GUI Freezes

The root cause is that your play_video method calls self.video_object.run() directly, which in turn uses thread.join() for all producer/consumer threads. Calling join() blocks the Tkinter main event loop until those threads finish, making your GUI unresponsive (you can't click the Stop button) until the entire capture process ends.

To fix this, we need to run the run() method in a separate background thread, so it doesn't block the main GUI loop.

2. Displaying Frames in Tkinter Instead of cv2 Windows

cv2's imshow() runs its own event loop, which conflicts with Tkinter's. Instead, we'll:

  • Convert OpenCV frames to PIL ImageTk format (since Tkinter can't directly handle cv2 frames)
  • Use a Tkinter Label to display the frame
  • Use Tkinter's after() method to safely update the frame from the main event loop (never modify Tkinter widgets from a background thread directly!)

Modified Working Code

Here's the updated code with fixes for both issues:

import multiprocessing
from tkinter import *
from tkinter import messagebox
from tkinter import filedialog
import PIL.Image, PIL.ImageTk
import cv2
from threading import Thread, Lock, Semaphore
import queue
import time
from tifffile import imsave
import numpy as np
import os

class VideoGet():
    def __init__(self, folder):
        self.record = False
        self.counter = 0
        self.folder = folder
        # Queue to send frames to GUI for display (limit size to prevent memory bloat)
        self.gui_frame_queue = queue.Queue(maxsize=10)

    def consumer(self, q):
        # Keep running until record stops AND save queue is empty
        while self.record or not q.empty():
            try:
                frame_get = q.get(timeout=0.5)
                imsave(os.path.join(self.folder, f"{self.counter}.tiff"), frame_get)
                self.counter += 1
                q.task_done()
            except queue.Empty:
                continue

    def producer(self, buffer, save_queue):
        while self.record:
            frame = np.zeros_like(buffer)
            # Send frame to save queue and GUI display queue
            save_queue.put(frame)
            if not self.gui_frame_queue.full():
                self.gui_frame_queue.put(frame.copy())  # Copy to avoid overwriting in transit
            time.sleep(0.03)  # Simulate ~30fps camera framerate, reduce CPU load
            del frame

    def run(self, buffer, save_queue):
        self.record = True
        self.counter = 0
        # Use daemon threads so they exit when main program closes
        prod_thread = Thread(target=self.producer, args=(buffer, save_queue,), daemon=True)
        con_thread = Thread(target=self.consumer, args=(save_queue,), daemon=True)
        prod_thread.start()
        con_thread.start()

class VideoGUI():
    def __init__(self, window, window_title):
        self.window = window
        self.window.title(window_title)
        
        # Frame for video display
        self.video_frame = Frame(self.window)
        self.video_frame.pack(padx=10, pady=10)
        self.video_label = Label(self.video_frame)
        self.video_label.pack()
        
        # Bottom button frame
        bottom_frame = Frame(self.window)
        bottom_frame.pack(side=BOTTOM, pady=5)
        
        self.btn_select = Button(bottom_frame, text="Select a folder", width=15, command=self.open_file)
        self.btn_select.grid(row=0, column=0)
        
        self.btn_play = Button(bottom_frame, text="Start Capture", width=15, command=self.start_capture, state=DISABLED)
        self.btn_play.grid(row=0, column=1)
        
        self.btn_pause = Button(bottom_frame, text="Stop Capture", width=15, command=self.stop_capture, state=DISABLED)
        self.btn_pause.grid(row=0, column=2)
        
        self.buffer = np.zeros(shape=(513, 640), dtype=np.uint16)
        self.save_queue = queue.Queue(maxsize=20)
        self.video_object = None
        self.update_id = None  # Track the frame update loop to cancel it later
        
        self.window.mainloop()

    def open_file(self):
        self.folder = filedialog.askdirectory(title="Select folder")
        self.video_object = VideoGet(self.folder)
        # Enable start button once folder is selected
        self.btn_play.config(state=NORMAL)

    def start_capture(self):
        if not self.video_object:
            messagebox.showwarning("Warning", "Please select a folder first!")
            return
        self.btn_play.config(state=DISABLED)
        self.btn_pause.config(state=NORMAL)
        # Run capture in background thread to avoid blocking GUI
        Thread(target=self.video_object.run, args=(self.buffer, self.save_queue,), daemon=True).start()
        # Start updating frame display
        self.update_frame()

    def stop_capture(self):
        if self.video_object:
            self.video_object.record = False
        # Cancel the frame update loop
        if self.update_id:
            self.window.after_cancel(self.update_id)
        self.btn_play.config(state=NORMAL)
        self.btn_pause.config(state=DISABLED)
        # Clear the display label
        self.video_label.config(image='')

    def update_frame(self):
        if self.video_object and self.video_object.record:
            try:
                frame = self.video_object.gui_frame_queue.get(timeout=0.01)
                # Normalize 16-bit frame to 8-bit for Tkinter display
                frame_normalized = (frame / np.max(frame) * 255).astype(np.uint8) if np.max(frame) !=0 else frame.astype(np.uint8)
                img = PIL.Image.fromarray(frame_normalized)
                imgtk = PIL.ImageTk.PhotoImage(image=img)
                self.video_label.imgtk = imgtk  # Keep reference to prevent garbage collection
                self.video_label.config(image=imgtk)
            except queue.Empty:
                pass
            # Schedule next frame update (~30fps)
            self.update_id = self.window.after(30, self.update_frame)

# Create a window and pass it to videoGUI Class
if __name__=='__main__':
    video_object=VideoGUI(Tk(),"Camera Capture System")

Key Changes Explained

  1. GUI Freeze Fix:

    • Moved video_object.run() execution into a separate daemon thread in start_capture(), so it doesn't block the Tkinter main loop.
    • Removed join() calls from VideoGet.run() — daemon threads automatically exit when the main program closes.
  2. Tkinter Frame Display:

    • Added gui_frame_queue in VideoGet to send frames to the GUI safely.
    • Created update_frame() method that uses after() to periodically fetch frames, convert them to PIL ImageTk format, and update the Tkinter Label.
    • Normalized 16-bit frames to 8-bit for proper Tkinter display (Tkinter doesn't handle 16-bit images natively).
  3. UI Polish:

    • Disabled buttons when they're not usable (e.g., Stop button before capture starts).
    • Added a warning if the user tries to start capture without selecting a folder.
    • Added small delays in the producer to simulate real camera framerate and reduce CPU usage.

内容的提问来源于stack exchange,提问作者Adrian

火山引擎 最新活动