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

如何用OpenCV与PyQt5实现摄像头流启停,解决启停时内核崩溃问题

问题根因
  • QThread不可重复启动:原代码中QThread实例调用quit()退出后,直接再次调用start()会触发未定义行为,多次启停/退出重开时直接导致内核崩溃
  • 跨线程操作硬件对象:VideoCapture对象在主线程创建、主线程释放,但实际在工作线程读写,跨线程操作OpenCV硬件对象会触发内存错误
  • 线程安全缺失:主线程直接修改工作线程正在读写的running、camera属性,无同步机制导致竞态条件
  • 窗口退出资源未释放:点击系统关闭按钮退出时,未主动停止线程、释放摄像头,导致设备占用状态异常
修复后完整代码
# -*- coding: utf-8 -*-
import sys
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
import cv2

class MainWindow(QWidget):
    def __init__(self):
        super().__init__()
        
        self.resize(300,300)
        self.globalLayout = QVBoxLayout(self)

        self.feedLabel = QLabel(self)
        self.feedLabel.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
        self.feedLabel.setStyleSheet("background-color:pink")
        
        self.start_btn=QPushButton("Start feed", self)
        self.cancel_btn = QPushButton("Cancel", self)
        self.saveImg_btn=QPushButton("Save image", self)
        
        self.select_camera=QComboBox(self)
        self.select_camera.addItems(["0","1"])
        
        buttons_widget=QWidget(self)
        buttons_layout=QHBoxLayout(buttons_widget)
        buttons_layout.addWidget(self.start_btn)
        buttons_layout.addWidget(self.cancel_btn)
        buttons_layout.addWidget(self.saveImg_btn)
        buttons_layout.addWidget(self.select_camera)
        
        self.globalLayout.addWidget(self.feedLabel)
        self.globalLayout.addWidget(buttons_widget)
        
        self.setLayout(self.globalLayout)
        
        self.start_btn.clicked.connect(self.start_feed)
        self.cancel_btn.clicked.connect(self.stop_feed)
        
        self.thread = None
        self.worker = None
        self.is_running = False

    def start_feed(self):
        if self.is_running:
            return
        # 每次启动新建线程和worker,避免复用问题
        self.thread = QThread()
        self.worker = Worker()
        self.worker.set_camera(int(self.select_camera.currentText()))
        self.worker.moveToThread(self.thread)
        self.thread.started.connect(self.worker.run)
        self.worker.imageUpdate.connect(self.set_new_img)
        self.worker.finished.connect(self.worker_done)
        self.thread.finished.connect(self.thread_done)
        
        self.is_running = True
        self.start_btn.setEnabled(False)
        self.thread.start()
        
    def stop_feed(self):
        if not self.is_running:
            return
        self.worker.stop()
        self.thread.quit()
        self.thread.wait()
        self.is_running = False
        self.start_btn.setEnabled(True)
        print("feed was asked to stop")
        
    def worker_done(self):
        print("worker finished")
        
    def thread_done(self):
        print("thread finished")
        
    def set_new_img(self,Image):
        self.feedLabel.setPixmap(QPixmap.fromImage(Image))
    
    def closeEvent(self, event):
        # 窗口关闭前主动释放资源
        self.stop_feed()
        event.accept()
     
        
class Worker(QObject):
    finished = pyqtSignal()
    imageUpdate = pyqtSignal(QImage)
    
    def __init__(self):
        super().__init__()
        self.camera = None
        self.running = False
    
    def set_camera(self, cam_id):
        self.camera = cv2.VideoCapture(cam_id, cv2.CAP_DSHOW)
    
    def stop(self):
        self.running = False
    
    def run(self):
        self.running = True
        while self.running:
            ret, frame = self.camera.read()
            if ret:
                Image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
                FlippedImage = cv2.flip(Image, 1)
                ConvertToQtFormat = QImage(FlippedImage.data, FlippedImage.shape[1], FlippedImage.shape[0], QImage.Format_RGB888)
                Pic = ConvertToQtFormat.scaled(640, 480, Qt.KeepAspectRatio)
                self.imageUpdate.emit(Pic)
        # 工作线程内释放摄像头,避免跨线程操作
        self.camera.release()
        self.finished.emit()
        
if __name__ == "__main__":
    App = QApplication(sys.argv)
    Root = MainWindow()
    Root.show()
    sys.exit(App.exec())
关键修改说明
  • 每次启动流都新建独立的QThread和Worker实例,规避QThread无法复用的问题
  • 所有VideoCapture的创建、释放操作都放在Worker内部执行,避免跨线程操作硬件对象
  • 增加is_running状态锁和按钮置灰逻辑,防止重复点击启停触发异常
  • 重写closeEvent方法,窗口关闭时主动停止线程、释放资源,避免设备残留占用
  • 增加thread.wait()逻辑,保证线程完全退出后再执行后续操作,避免线程僵尸状态

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.24 22:45:04