You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多
文档控制台
免费开始使用

基于PyQt与Python线程的树莓派3摄像头流卡顿问题求助

解决树莓派PyQt摄像头流卡顿问题并适配多线程扩展(GPIO/串口)

你已经尝试用线程处理摄像头流,但卡顿的核心问题其实出在主线程阻塞线程同步缺失上,加上树莓派3本身性能有限,原代码的一些写法会放大这些问题。下面我一步步帮你梳理修复方案,同时兼顾后续GPIO和串口的多线程需求。

原代码的核心问题

  • 主线程被阻塞play()方法里的while self.Stopp循环会完全占用Qt的主线程,导致UI事件(比如按钮点击、界面刷新)无法及时处理,这是卡顿最主要的原因。Qt的UI更新必须在主线程完成,但不能用死循环阻塞它,应该用QTimer定时触发更新。
  • 无线程同步机制WebcamStream里的currentFramereadFrame是线程共享变量,但没有加锁保护,读取和写入时可能出现数据不完整的情况,导致画面撕裂或错误。
  • 无帧率控制:摄像头线程一直在不间断读取帧,树莓派3的CPU资源有限,帧堆积会导致处理不过来,反而降低流畅度。
  • 线程停止逻辑不安全self.stopped变量没有用线程安全的方式访问,可能出现线程无法及时停止的情况。

修复后的完整代码

我基于你的代码做了针对性修改,解决了上述问题,同时保留了原有的功能结构:

import sys
import cv2
import numpy as np
from threading import Thread, Lock
from PyQt4 import QtGui, QtCore, uic
from PyQt4.QtCore import pyqtSlot

class WebcamStream():
    def __init__(self, src=0, width=640, height=480, fps=20):
        self.capture = cv2.VideoCapture(src)
        # 设置摄像头分辨率和帧率,适配树莓派性能
        self.capture.set(cv2.CAP_PROP_FRAME_WIDTH, width)
        self.capture.set(cv2.CAP_PROP_FRAME_HEIGHT, height)
        self.capture.set(cv2.CAP_PROP_FPS, fps)
        
        self.ret, self.currentFrame = self.capture.read()
        self.stopped = False
        # 添加线程锁保护共享数据
        self.frame_lock = Lock()

    def start(self):
        print("Stream_Start")
        t = Thread(target=self.update, args=())
        t.daemon = True
        t.start()
        return self

    def update(self):
        """在子线程中持续读取摄像头帧,加锁保护共享数据"""
        while not self.stopped:
            ret, frame = self.capture.read()
            if ret:
                # 转换颜色空间后再存入共享变量
                rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
                # 加锁写入,避免主线程读取时数据不完整
                with self.frame_lock:
                    self.currentFrame = rgb_frame
                    self.ret = ret
            # 控制循环频率,避免占用过多CPU
            cv2.waitKey(1)

    def read(self):
        """主线程读取帧时加锁,确保数据完整"""
        with self.frame_lock:
            frame = self.currentFrame.copy()
            ret = self.ret
        return ret, frame

    def stop(self):
        self.stopped = True
        # 释放摄像头资源
        self.capture.release()

class Gui(QtGui.QMainWindow):
    def __init__(self, parent=None):
        QtGui.QWidget.__init__(self, parent)
        uic.loadUi('mainWindow.ui', self)
        self.show()
        
        # 初始化摄像头流
        self.video = WebcamStream(src=0, width=640, height=480, fps=20).start()
        # 用QTimer定时更新UI,避免阻塞主线程
        self.update_timer = QtCore.QTimer(self)
        self.update_timer.timeout.connect(self.update_video_frame)
        self.is_streaming = False

    def update_video_frame(self):
        """定时从摄像头线程读取帧并更新UI"""
        ret, frame = self.video.read()
        if ret:
            # 转换为Qt可用的格式
            height, width = frame.shape[:2]
            img = QtGui.QImage(frame, width, height, QtGui.QImage.Format_RGB888)
            pixmap = QtGui.QPixmap.fromImage(img)
            self.videoFrame.setPixmap(pixmap)
            self.videoFrame.setScaledContents(True)

    @pyqtSlot()
    def on_ButtonBegin_clicked(self):
        if not self.is_streaming:
            self.is_streaming = True
            self.update_timer.start(30)  # 约33fps,根据摄像头帧率调整

    @pyqtSlot()
    def on_ButtonStop_clicked(self):
        if self.is_streaming:
            self.is_streaming = False
            self.update_timer.stop()
            self.video.stop()
            # 清空显示区域
            self.videoFrame.clear()

def main():
    app = QtGui.QApplication(sys.argv)
    ex = Gui()
    sys.exit(app.exec_())

if __name__ == '__main__':
    main()

关键修改点说明

  1. 替换while循环为QTimer:用QTimer每隔30ms触发一次UI更新,这样主线程可以正常处理其他事件(比如按钮点击、后续的串口/GPIO事件),不会被阻塞。
  2. 添加线程锁:用threading.Lock保护共享的帧数据,确保子线程写入和主线程读取时不会出现数据竞争,避免画面撕裂或错误。
  3. 优化摄像头参数:设置合适的分辨率(640x480)和帧率(20fps),树莓派3的CPU性能有限,过高的参数会导致处理不过来。
  4. 安全停止线程:确保摄像头资源被正确释放,线程可以干净退出。
  5. 分离帧读取和转换逻辑:子线程只负责读取和转换颜色空间,主线程负责转换为Qt格式并更新UI,职责更清晰。

后续GPIO和串口的多线程扩展建议

因为你后续还要用到GPIO和串口,这些操作都不应该阻塞主线程,建议采用同样的多线程思路:

  • 串口通信:单独创建一个串口线程,负责持续读取串口数据,用线程锁或QtCore.QSignal将数据发送到主线程更新UI。
  • GPIO操作:如果是GPIO输入(比如按键),可以用线程监听或者用RPi.GPIO的回调机制;如果是GPIO输出,直接在主线程或单独的控制线程处理即可,注意线程安全。
  • 线程间通信:Qt中推荐用pyqtSignal来实现子线程和主线程的通信,避免直接操作UI控件(UI控件只能在主线程修改)。

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

火山引擎 最新活动