如何用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
相关产品推荐
相关产品推荐

