使用Tkinter录制若干GIF后摄像头预览卡顿问题求助
问题修复:录制多次GIF后程序卡顿的解决方案
问题根源分析
- 摄像头资源竞争:预览线程和GIF录制线程同时调用
cam.read(),两个线程抢用摄像头资源,导致帧读取混乱,多次操作后出现卡顿。 - 帧率控制不合理:用
time.sleep()硬控帧率,和摄像头实际采集帧率不匹配,导致帧队列堆积,占用内存和CPU。 - 内存未及时释放:GIF录制时将所有帧存入列表,多次录制后内存占用持续升高;预览线程中每次创建新的
ImageTk.PhotoImage但旧对象可能未被正确回收。 - 线程管理疏漏:预览线程设置为非守护线程,页面切换时如果没完全停止,会残留后台线程占用资源;GIF录制线程的循环逻辑可能导致重复执行。
修复方案及修改代码
修改后的WebcamPreview.py
import cv2 import threading import time import ImageTk from PIL import Image import imageio class WebcamPreview: def __init__(self, preview_size=(960, 540), fps=30): self.preview_size = preview_size self.fps = fps self.frame_interval = 1.0 / fps self.cam = None self.current_frame = None self.running = False self.lock = threading.Lock() # 用于帧同步的线程锁 def start_preview(self): if self.cam is None: self.cam = cv2.VideoCapture(cv2.CAP_DSHOW) self.cam.set(cv2.CAP_PROP_FPS, self.fps) # 设置摄像头原生帧率 self.running = True # 启动帧读取守护线程,程序退出时自动终止 threading.Thread(target=self._read_frames, daemon=True).start() def _read_frames(self): last_time = time.time() while self.running and self.cam.isOpened(): # 通过时间间隔控制读帧速度,避免过快 current_time = time.time() if current_time - last_time < self.frame_interval: time.sleep(0.001) continue last_time = current_time ret, frame = self.cam.read() if ret: frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) frame = cv2.resize(frame, self.preview_size) # 加锁更新共享帧,保证线程安全 with self.lock: self.current_frame = frame.copy() def update_preview_label(self, label): if not self.running: return with self.lock: if self.current_frame is not None: img = Image.fromarray(self.current_frame) photo = ImageTk.PhotoImage(image=img) label.config(image=photo) label.image = photo # 用tkinter的after循环更新预览,替代线程sleep label.after(int(self.frame_interval * 1000), self.update_preview_label, label) def capture_image(self, filename): with self.lock: if self.current_frame is not None: cv2.imwrite(filename, cv2.cvtColor(self.current_frame, cv2.COLOR_RGB2BGR)) def capture_Gif(self, duration, filename): frames = [] start_time = time.time() frame_count = 0 target_fps = 10 # GIF目标帧率 gif_interval = 1.0 / target_fps while time.time() - start_time < duration: current_time = time.time() if current_time - start_time < frame_count * gif_interval: time.sleep(0.001) continue with self.lock: if self.current_frame is not None: frames.append(self.current_frame.copy()) frame_count += 1 if frames: imageio.mimsave(filename, frames, duration=gif_interval) del frames[:] # 清空列表释放内存 def capture_video(self, filename="Assets/temp_video.avi", duration=5): target_fps = 30 num_frames = int(target_fps * duration) fourcc = cv2.VideoWriter_fourcc(*'DIVX') out = cv2.VideoWriter(filename, fourcc, target_fps, self.preview_size) frame_count = 0 start_time = time.time() video_interval = 1.0 / target_fps while frame_count < num_frames and self.running: current_time = time.time() if current_time - start_time < frame_count * video_interval: time.sleep(0.001) continue with self.lock: if self.current_frame is not None: frame_bgr = cv2.cvtColor(self.current_frame, cv2.COLOR_RGB2BGR) out.write(frame_bgr) frame_count += 1 out.release() def stop_preview(self): self.running = False if self.cam is not None: self.cam.release() self.cam = None # 清空共享帧,释放内存 with self.lock: self.current_frame = None
修改后的CapturePage.py
import tkinter as tk from threading import Thread, Event from WebcamPreview import WebcamPreview class CapturePage(tk.Frame): def __init__(self, parent, controller): tk.Frame.__init__(self, parent) self.controller = controller self.thread_stop_event = Event() self['bg']='#DEF7E5' self.back_button = tk.Button(self, text="◀ Back", font=("Arbutus Slab", 10), command=lambda: controller.show_frame("DefaultPage"), background='#DEF7E5', activebackground='#DEF7E5', bd=0) self.back_button.place(x=40, y=40) self.webcam_preview = WebcamPreview() self.preview_label = tk.Label(self) self.preview_label.pack(pady=60) self.image = tk.PhotoImage(file="Assets/Capture_button.png") button = tk.Button(self, image=self.image, borderwidth=0, relief="flat", highlightthickness=0, background='#DEF7E5', command=self.capture) button.pack() self.White_square = tk.Label(self, bg="white") self.transparent_black_square = tk.Label(self, bg="black") self.processing_label = tk.Label(self.transparent_black_square, text="Processing...", fg="white", font=("Arial", 16)) def capture(self): format = self.controller.format_global if format == "Image": self.capture_image() elif format == "GIF": self.capture_gif() elif format == "Video": self.capture_video() def capture_image(self): self.webcam_preview.capture_image("Assets/Temp_image.jpg") self.controller.show_frame("CheckMedia") def capture_gif(self): self.thread_stop_event.clear() gifCapture = Thread(target=self.gif_thread, daemon=True) gifCapture.start() self.Light() self.after(5000, self.turn_off_light) self.after(5000, self.Proccesing) def gif_thread(self): try: self.webcam_preview.capture_Gif(5, "Assets/Temp_gif.gif") if not self.thread_stop_event.is_set(): # 回到主线程更新UI,避免跨线程操作问题 self.controller.after(0, lambda: self.controller.show_frame("CheckMedia")) finally: self.thread_stop_event.set() def capture_video(self): self.thread_stop_event.clear() videoCapture = Thread(target=self.Video_thread, daemon=True) videoCapture.start() self.Light() self.after(5000, self.turn_off_light) self.after(5000, self.Proccesing) def Video_thread(self): try: self.webcam_preview.capture_video() if not self.thread_stop_event.is_set(): self.controller.after(0, lambda: self.controller.show_frame("CheckMedia")) finally: self.thread_stop_event.set() def Light(self): self.White_square.place(x=0, y=0, relwidth=1, relheight=1) def turn_off_light(self): self.White_square.place_forget() def Proccesing(self): self.transparent_black_square.place(x=0, y=0, relwidth=1, relheight=1) self.processing_label.place(relx=0.5, rely=0.5, anchor="center") def on_show_frame(self): self.webcam_preview.start_preview() # 启动预览UI更新 self.webcam_preview.update_preview_label(self.preview_label) def on_hide_frame(self): self.White_square.place_forget() self.transparent_black_square.place_forget() self.processing_label.place_forget() self.thread_stop_event.set() self.webcam_preview.stop_preview() # 彻底释放摄像头资源
关键修复点说明
- 单线程读帧:用独立守护线程统一读取摄像头帧,预览和录制都从共享帧获取,避免资源竞争。
- 线程锁同步:用
threading.Lock保证多线程访问帧数据的安全性。 - 帧率优化:通过时间间隔控制帧率,替代
time.sleep(),匹配摄像头和录制的实际帧率。 - 内存回收:GIF录制后清空帧列表,页面切换时彻底释放摄像头和帧资源。
- UI线程安全:用
tkinter的after方法在主线程更新UI,避免跨线程操作UI的风险。
内容的提问来源于stack exchange,提问作者Gega Abzianidze
相关产品推荐
相关产品推荐

