【Python】PyQt5实现多相机(USB)同时打开

硬件与软件支持解析

硬件支持

对于需要同时操作多个USB相机的应用,标准计算机或工控机的USB端口数量和带宽可能成为瓶颈。通常情况下,一个USB控制器(如集成在主板上的)可以管理两个USB相机的实时数据流,但如果要扩展到六个或更多USB相机,则需要额外的硬件支持。

解决方案:

  • PCIE USB 扩展卡:使用带有多个USB端口的PCIE扩展卡来增加可用的USB通道数。这种扩展卡插入主板的PCIE插槽中,提供独立工作的USB端口,确保每个相机都有足够的带宽进行数据传输。

  • 推荐配置:选择一款6通道独立工作的USB PCIE扩展卡。这样的卡能够为每个连接的USB相机提供专用的数据通道,避免了多设备共用同一USB控制器时可能出现的性能问题。
  • 软件支持

    为了有效地管理和操作多个USB相机,不仅需要合适的硬件,还需要相应的软件库来简化开发过程并确保稳定性和效率。

    安装依赖库:

    pip install pycameralist opencv-python PyQt5
  • pycameralist 是一个用于列出系统中所有视频设备的Python库,它可以帮助开发者轻松获取当前连接的所有相机信息。
  • opencv-python 提供了强大的图像处理功能,并且包含了一个易于使用的接口来捕获和显示视频流。
  • PyQt5 是构建图形用户界面的库,这里用来创建窗口并在界面上显示视频流。
  • OpenVideos

    定义了一个名为open_videos的函数,用于管理和打开视频设备(如摄像头)。它使用了OpenCV库(cv2)来处理视频捕捉,并通过日志记录函数log_message输出操作信息。

    初始化

    camera_dict = {}
  • camera_dict是一个字典,用来存储每个视频设备的名字及其对应的VideoCapture对象。
  • 函数定义

    def open_videos():
  • 定义了一个名为open_videos的函数,负责释放当前已打开的视频设备并尝试重新打开所有可用的视频设备。
  • 释放现有视频设备

        for cap in camera_dict.values():
            if cap is not None and cap.isOpened():
                cap.release()
  • 遍历camera_dict中的所有值(即之前打开的VideoCapture对象)。
  • 如果某个VideoCapture对象存在且是打开状态,则调用release()方法关闭它,以确保在重新打开前没有残留的打开设备。
  • 获取视频设备列表

        video_devices = dict(list_video_devices())
        log_message(f'视频设备列表:{video_devices}')
  • 调用list_video_devices()函数获取系统中所有可用的视频设备,并将结果转换为字典格式。
  • 使用log_message函数记录下这些视频设备的信息,方便调试和监控。
  • 打开视频设备

        for (index, name) in video_devices.items():
            camera_dict[name] = None
            # 打开视频设备
            cap = cv2.VideoCapture(index, cv2.CAP_DSHOW)
            if cap is not None and cap.isOpened():
                camera_dict[name] = cap
                log_message(f'视频设备[{name}]打开成功!')
  • 遍历从list_video_devices()获得的视频设备列表。
  • 对于每一个视频设备,首先将其对应的camera_dict条目设置为None
  • 尝试使用OpenCV的VideoCapture类打开该视频设备,指定设备索引index和捕获API后端标识符cv2.CAP_DSHOW(DirectShow,Windows平台上的一个视频捕捉API)。
  • 如果VideoCapture对象创建成功并且设备确实被打开(isOpened()返回True),则更新camera_dict,将设备名字映射到这个新的VideoCapture对象,并记录一条日志表示设备打开成功。
  • 总结

    open_videos函数的主要功能包括:

  • 清理:确保在重新打开视频设备之前,任何现有的打开设备都被正确关闭。
  • 枚举:获取系统中所有可用的视频设备,并记录它们的信息。
  • 初始化:尝试根据获得的设备列表打开每个视频设备,并在成功时保存相应的VideoCapture对象,以便后续使用。
  • 这种方法可以有效地管理多个视频输入源,并保证每次启动或重新配置视频输入时,程序能够正确地识别和初始化所有的视频设备。此外,通过日志记录,开发人员可以更容易地跟踪视频设备的状态变化,便于排查问题和维护代码。

    camera_dict = {}
    
    def open_videos():
        for cap in camera_dict.values():
            if cap is not None and cap.isOpened():
                cap.release()
    
        video_devices = dict(list_video_devices())
        log_message(f'视频设备列表:{video_devices}')
        for (index, name) in video_devices.items():
            camera_dict[name] = None
            # 打开视频设备
            cap = cv2.VideoCapture(index, cv2.CAP_DSHOW)
            if cap is not None and cap.isOpened():
                camera_dict[name] = cap
                log_message(f'视频设备[{name}]打开成功!')

    实时视频刷新

    为了实现实时视频流的更新,代码中设置了两个定时器 (QTimer),分别负责刷新不同的相机组。这样做的目的是减轻CPU负载,防止因为频繁调用导致性能下降。

    self.video_timer1 = QTimer(self)
    self.video_timer1.timeout.connect(self.updateFrames13)
    self.video_timer1.start(50)
    
    self.video_timer2 = QTimer(self)
    self.video_timer2.timeout.connect(self.updateFrames46)
    self.video_timer2.start(50)

    每个定时器触发时都会调用相应的更新函数(如 updateFrames13updateFrames46),这些函数会遍历指定的相机名称列表,并调用通用的 updateFrames 函数来读取最新的帧数据、添加文本标签,并将图像转换为适合GUI显示的格式。

    updateFrames

    定义了一个名为updateFrames的方法,用于更新与一系列摄像头名称关联的视频帧,并将这些帧显示在相应的GUI标签(labels)上。它使用了OpenCV库来处理视频捕捉和图像处理,并且转换图像格式以便在PyQt或PySide GUI框架中显示

    方法签名

    def updateFrames(self, names):
  • self:指向实例对象本身,表明这是一个类方法。
  • names:一个包含摄像头名称的列表,每个名称对应于self.cameras字典中的一个VideoCapture对象。
  • 尝试更新每个摄像头的帧

    try:
        for name in names:
            cap = self.cameras[name]
            if cap is not None and cap.isOpened():
                _, frame = cap.read()
                if not _:
                    self.cameras[name] = None
                    log_message(f'视频设备[{name}]断开,已关闭!')
                cv2.putText(frame, name, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
            else:
                frame = np.zeros((480, 640, 3), np.uint8)
                cv2.putText(frame, name, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)
  • 遍历names列表:对于每一个摄像头名称,尝试从对应的VideoCapture对象中读取一帧图像。
  • 如果摄像头是打开状态并且成功读取了一帧,则:
  • 在图像左上角添加摄像头名称的文字标签,颜色为绿色。
  • 如果读取失败(即返回的第一个值_False),则认为该摄像头已经断开连接,设置其对应的VideoCapture对象为None并记录日志。
  • 如果摄像头未打开或者不存在,则创建一个黑色背景的空白图像,并在上面用红色文字标记摄像头名称,表示当前无法获取到有效的视频流。
  • 将图像转换为适合GUI显示的格式

            h, w, ch = frame.shape
            bytesPerLine = ch * w
            convertToQtFormat = QImage(frame.data, w, h, bytesPerLine, QImage.Format_RGB888).rgbSwapped()
            pixmap = QPixmap.fromImage(convertToQtFormat)
            self.labels[name].setPixmap(pixmap.scaled(630, 480, Qt.KeepAspectRatio))
  • 获取图像的高度、宽度和通道数。
  • 计算每行的字节数,用于构建QImage对象时指定图像数据的排列方式。
  • 使用frame.data直接从NumPy数组创建QImage对象,注意这里调用了.rgbSwapped()方法,因为OpenCV默认使用的BGR格式需要转换为RGB以正确显示。
  • QImage对象转换为QPixmap对象,这是为了能够在PyQt或PySide的QLabel控件中显示图像。
  • 设置QLabel控件的pixmap属性,同时保持图像的宽高比缩放到合适的尺寸(630×480)。
  • 异常处理

    except:
        pass
  • 捕获所有异常,并简单地跳过任何发生的错误而不做进一步处理。这可能不是最佳实践,因为它会掩盖潜在的问题。通常建议至少记录下异常信息以便调试。
  • 总结

    updateFrames方法的主要目的是周期性地从多个视频源获取最新的帧,并将这些帧更新到GUI界面上相应的标签控件中。通过这种方式,用户可以实时查看多个摄像头的视频流。此外,该方法还处理了摄像头断开的情况,当检测到某个摄像头不再可用时,会显示一条提示信息而不是空内容。这种方法适用于开发视频监控系统或其他需要多摄像头输入的应用程序。

    def updateFrames(self, names):
        try:
            for name in names:
                cap = self.cameras[name]
                if cap is not None and cap.isOpened():
                    _, frame = cap.read()
                    if not _:
                        self.cameras[name] = None
                        log_message(f'视频设备[{name}]断开,已关闭!')
                    cv2.putText(frame, name, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
                else:
                    frame = np.zeros((480, 640, 3), np.uint8)
                    cv2.putText(frame, name, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)
    
                h, w, ch = frame.shape
                bytesPerLine = ch * w
                convertToQtFormat = QImage(frame.data, w, h, bytesPerLine, QImage.Format_RGB888).rgbSwapped()
                pixmap = QPixmap.fromImage(convertToQtFormat)
                self.labels[name].setPixmap(pixmap.scaled(630, 480, Qt.KeepAspectRatio))
        except:
            pass

    刷新视频方法

    refreshVideos 方法提供了手动重新扫描和打开视频设备的功能,这对于应对设备热插拔场景非常有用。该方法会释放所有现有的视频捕获对象,重新枚举视频设备,并尝试重新建立连接。

    def refreshVideos(self):
        for cap in camera_dict.values():
            if cap is not None and cap.isOpened():
                cap.release()
    
        video_devices = dict(list_video_devices())
        log_message(f'视频设备列表:{video_devices}')
        for (index, name) in video_devices.items():
            camera_dict[name] = None
            # 打开视频设备
            cap = cv2.VideoCapture(index, cv2.CAP_DSHOW)
            if cap is not None and cap.isOpened():
                camera_dict[name] = cap
                log_message(f'视频设备[{name}]打开成功!')
    
        self.cameras = camera_dict

    总结

    本文展示了如何结合硬件扩展和适当的软件库来管理多个USB相机,并在一个图形用户界面中实时显示其视频流。通过合理地分配任务给不同的线程以及对视频设备的有效管理,实现了高效稳定的多相机视频采集和展示系统。

    作者:道友老李

    物联沃分享整理
    物联沃-IOTWORD物联网 » 【Python】PyQt5实现多相机(USB)同时打开

    发表回复