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

使用Tkinter录制若干GIF后摄像头预览卡顿问题求助

问题修复:录制多次GIF后程序卡顿的解决方案

问题根源分析

  1. 摄像头资源竞争:预览线程和GIF录制线程同时调用cam.read(),两个线程抢用摄像头资源,导致帧读取混乱,多次操作后出现卡顿。
  2. 帧率控制不合理:用time.sleep()硬控帧率,和摄像头实际采集帧率不匹配,导致帧队列堆积,占用内存和CPU。
  3. 内存未及时释放:GIF录制时将所有帧存入列表,多次录制后内存占用持续升高;预览线程中每次创建新的ImageTk.PhotoImage但旧对象可能未被正确回收。
  4. 线程管理疏漏:预览线程设置为非守护线程,页面切换时如果没完全停止,会残留后台线程占用资源;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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.25 15:45:54