Tkinter多线程冻结问题排查及GUI帧显示方案咨询
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
Labelto 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
GUI Freeze Fix:
- Moved
video_object.run()execution into a separate daemon thread instart_capture(), so it doesn't block the Tkinter main loop. - Removed
join()calls fromVideoGet.run()— daemon threads automatically exit when the main program closes.
- Moved
Tkinter Frame Display:
- Added
gui_frame_queueinVideoGetto send frames to the GUI safely. - Created
update_frame()method that usesafter()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).
- Added
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




