程序员文章、书籍推荐和程序员创业信息与资源分享平台

网站首页 > 技术文章 正文

从零实现 Python 多摄像头实时显示上位机(附完整代码)

hfteth 2025-05-26 15:54:39 技术文章 3 ℃

点赞、收藏、加关注,下次找我不迷路

话不多说,先给大家看效果


一、需求分析:明确项目目标与功能

在开始编码之前,我们需要明确项目的目标和功能。这个多摄像头实时监控系统需要具备以下核心功能:

  1. 摄像头切换功能:能够在多个摄像头之间自由切换
  2. 实时预览功能:实时显示当前摄像头的画面
  3. 清晰度检测功能:自动检测画面清晰度
  4. 最佳画面捕捉功能:自动保存最清晰的画面

有了明确的功能需求,我们就可以进入设计阶段了。


二、系统设计:架构与模块划分

2.1 整体架构设计

我们的系统采用经典的 MVC(Model-View-Controller)架构模式,将系统分为三个核心模块:

  • 模型层(Model):负责摄像头管理、视频捕获和图像处理
  • 视图层(View):负责用户界面的展示
  • 控制层(Controller):负责处理用户交互和业务逻辑

这种架构设计使代码结构清晰,各模块职责明确,便于维护和扩展。

2.2 技术选型

  • GUI 框架:选择 wxPython,因为它提供了跨平台的图形界面开发能力,且与 Python 集成良好
  • 图像处理库:OpenCV,强大的计算机视觉库,提供了丰富的图像处理功能
  • 多线程处理:使用 Python 的 threading 模块,实现视频捕获的异步处理,避免界面卡顿

2.3 类设计

我们的系统主要包含一个核心类:CameraApp。这个类将继承 wx.Frame,负责创建窗口和管理整个应用的生命周期。在这个类中,我们将实现摄像头管理、画面捕获、清晰度检测等核心功能。


三、分段代码实现:从基础框架到核心功能

3.1 基础框架搭建

首先,我们来搭建应用的基础框架,创建窗口和基本的用户界面元素。

import wx

class CameraApp(wx.Frame):
    def __init__(self, *args, **kw):
        """
        初始化 CameraApp 窗口并设置 UI 组件。

        :param args: wx.Frame 的位置参数。
        :param kw: wx.Frame 的关键字参数。
        """
        super(CameraApp, self).__init__(*args, **kw)

        self.InitUI()  # 设置 UI 元素
        self.cap = None  # 视频捕获对象
        self.current_camera_id = -1  # 初始化时未选择摄像头
        self.is_running = False  # 记录摄像头是否在采集

        self.Bind(wx.EVT_CLOSE, self.OnClose)  # 处理窗口关闭事件

        self.Show()  # 显示窗口

    def InitUI(self):
        """
        初始化用户界面组件。
        """
        panel = wx.Panel(self)

        vbox = wx.BoxSizer(wx.VERTICAL)  # 垂直布局

        # 摄像头选择下拉菜单
        self.camera_choice = wx.ComboBox(panel, choices=[str(i) for i in range(10)])
        self.camera_choice.Bind(wx.EVT_COMBOBOX, self.OnCameraChange)  # 处理摄像头切换
        vbox.Add(self.camera_choice, flag=wx.EXPAND | wx.ALL, border=10)

        # 用于显示摄像头画面的面板
        self.image_panel = wx.Panel(panel)
        vbox.Add(self.image_panel, proportion=1, flag=wx.EXPAND | wx.ALL, border=10)

        panel.SetSizer(vbox)  # 设置布局

        self.SetTitle('Camera Switcher')  # 窗口标题
        self.SetSize((800, 600))  # 初始窗口大小

    def OnClose(self, event):
        """
        处理窗口关闭事件。

        :param event: 关闭事件对象。
        """
        self.is_running = False  # 停止捕获
        if self.cap:
            self.cap.release()  # 释放摄像头
        self.Destroy()  # 销毁窗口


if __name__ == '__main__':
    app = wx.App()  # 创建 wx 应用
    CameraApp(None)  # 初始化 CameraApp
    app.MainLoop()  # 启动应用循环

这段代码创建了一个基本的窗口应用,包含一个摄像头选择下拉菜单和一个用于显示视频画面的面板。目前还没有实现视频捕获和显示功能,接下来我们将逐步添加这些功能。

3.2 摄像头管理与切换功能实现

现在,我们来实现摄像头的管理和切换功能。

def OnCameraChange(self, event):
    """
    处理选择新摄像头时的事件。

    :param event: 摄像头切换的事件对象。
    """
    camera_id = int(self.camera_choice.GetValue())
    if camera_id != self.current_camera_id:
        self.current_camera_id = camera_id
        self.switch_camera(camera_id)  # 切换到选择的摄像头

def switch_camera(self, camera_id):
    """
    根据选择的摄像头 ID 切换摄像头。

    :param camera_id: 要切换的摄像头 ID。
    """
    if self.cap is not None:
        self.is_running = False  # 停止当前捕获
        self.cap.release()  # 释放当前摄像头
        self.cap = None

    self.cap = cv2.VideoCapture(camera_id)  # 打开新的摄像头
    if not self.cap.isOpened():
        wx.MessageBox(f"无法打开摄像头 {camera_id}", "错误", wx.OK | wx.ICON_ERROR)
        return

    self.is_running = True  # 开始捕获
    Thread(target=self.capture_frames).start()  # 在单独的线程中运行捕获

这部分代码实现了摄像头的切换功能。当用户从下拉菜单中选择一个新的摄像头时,程序会释放当前的摄像头资源,然后尝试打开新选择的摄像头。如果成功打开,就会启动一个新线程来捕获视频帧。

3.3 视频捕获与显示功能实现

接下来,我们实现视频帧的捕获和显示功能。

def capture_frames(self):
    """
    持续从摄像头捕获帧并更新 UI。
    """
    while self.is_running:
        ret, frame = self.cap.read()
        if not ret:
            continue

        # 将帧转换为 RGB
        frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)

        # 将帧转换为 wx.Image
        height, width = frame_rgb.shape[:2]
        image = wx.Image(width, height, frame_rgb.tobytes())

        # 更新图像面板显示新帧
        wx.CallAfter(self.update_image, image)

def update_image(self, image):
    """
    使用新图像帧更新图像面板。

    :param image: 要显示的 wx.Image。
    """
    bitmap = wx.Bitmap(image)  # 将图像转换为位图
    dc = wx.ClientDC(self.image_panel)  # 创建用于绘图的设备上下文
    dc.DrawBitmap(bitmap, 0, 0)  # 在面板上绘制位图

这段代码实现了视频帧的捕获和显示。在 capture_frames 方法中,我们使用一个无限循环不断从摄像头读取帧,然后将帧转换为 RGB 格式,并通过 wxPython 的 UI 线程安全调用机制更新显示。update_image 方法负责将图像绘制到界面上。

3.4 清晰度检测与最佳画面捕捉功能实现

最后,我们实现最核心的清晰度检测和最佳画面捕捉功能。

def __init__(self, *args, **kw):
    # ...原有代码...
    self.max_sharpness = 0  # 记录帧的最大清晰度
    self.best_image = None  # 根据清晰度存储最佳图像
    # ...原有代码...

def capture_frames(self):
    """
    持续从摄像头捕获帧并更新 UI。
    """
    while self.is_running:
        ret, frame = self.cap.read()
        if not ret:
            continue

        # 使用 Sobel 滤波器计算清晰度
        gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        sobelx = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=3)
        sobely = cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=3)
        sharpness = np.mean(np.sqrt(sobelx**2 + sobely**2))

        if sharpness > self.max_sharpness:
            self.max_sharpness = sharpness  # 更新最大清晰度
            self.best_image = frame.copy()  # 存储最清晰的图像

        # 将帧转换为 RGB
        frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)

        # 将帧转换为 wx.Image
        height, width = frame_rgb.shape[:2]
        image = wx.Image(width, height, frame_rgb.tobytes())

        # 更新图像面板显示新帧
        wx.CallAfter(self.update_image, image)

这里我们添加了清晰度检测的逻辑。使用 Sobel 算子计算图像的梯度,梯度越大表示图像越清晰。我们计算每一帧的清晰度,并与之前记录的最大清晰度进行比较,如果当前帧更清晰,则更新最大清晰度值并保存当前帧为最佳图像。


四、完整代码实现与解析

下面是完整的代码实现:

import wx
import cv2
import numpy as np
from threading import Thread


class CameraApp(wx.Frame):
    def __init__(self, *args, **kw):
        """
        初始化 CameraApp 窗口并设置 UI 组件。

        :param args: wx.Frame 的位置参数。
        :param kw: wx.Frame 的关键字参数。
        """
        super(CameraApp, self).__init__(*args, **kw)

        self.InitUI()  # 设置 UI 元素
        self.cap = None  # 视频捕获对象
        self.current_camera_id = -1  # 初始化时未选择摄像头
        self.max_sharpness = 0  # 记录帧的最大清晰度
        self.best_image = None  # 根据清晰度存储最佳图像
        self.is_running = False  # 记录摄像头是否在采集

        self.Bind(wx.EVT_CLOSE, self.OnClose)  # 处理窗口关闭事件

        self.Show()  # 显示窗口

    def InitUI(self):
        """
        初始化用户界面组件。
        """
        panel = wx.Panel(self)

        vbox = wx.BoxSizer(wx.VERTICAL)  # 垂直布局

        # 摄像头选择下拉菜单
        self.camera_choice = wx.ComboBox(panel, choices=[str(i) for i in range(10)])
        self.camera_choice.Bind(wx.EVT_COMBOBOX, self.OnCameraChange)  # 处理摄像头切换
        vbox.Add(self.camera_choice, flag=wx.EXPAND | wx.ALL, border=10)

        # 用于显示摄像头画面的面板
        self.image_panel = wx.Panel(panel)
        vbox.Add(self.image_panel, proportion=1, flag=wx.EXPAND | wx.ALL, border=10)

        panel.SetSizer(vbox)  # 设置布局

        self.SetTitle('Camera Switcher')  # 窗口标题
        self.SetSize((800, 600))  # 初始窗口大小

    def OnCameraChange(self, event):
        """
        处理选择新摄像头时的事件。

        :param event: 摄像头切换的事件对象。
        """
        camera_id = int(self.camera_choice.GetValue())
        if camera_id != self.current_camera_id:
            self.current_camera_id = camera_id
            self.switch_camera(camera_id)  # 切换到选择的摄像头

    def switch_camera(self, camera_id):
        """
        根据选择的摄像头 ID 切换摄像头。

        :param camera_id: 要切换的摄像头 ID。
        """
        if self.cap is not None:
            self.is_running = False  # 停止当前捕获
            self.cap.release()  # 释放当前摄像头
            self.cap = None

        self.cap = cv2.VideoCapture(camera_id)  # 打开新的摄像头
        if not self.cap.isOpened():
            wx.MessageBox(f"无法打开摄像头 {camera_id}", "错误", wx.OK | wx.ICON_ERROR)
            return

        self.is_running = True  # 开始捕获
        Thread(target=self.capture_frames).start()  # 在单独的线程中运行捕获

    def capture_frames(self):
        """
        持续从摄像头捕获帧并更新 UI。
        """
        while self.is_running:
            ret, frame = self.cap.read()
            if not ret:
                continue

            # 使用 Sobel 滤波器计算清晰度
            gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
            sobelx = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=3)
            sobely = cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=3)
            sharpness = np.mean(np.sqrt(sobelx**2 + sobely**2))

            if sharpness > self.max_sharpness:
                self.max_sharpness = sharpness  # 更新最大清晰度
                self.best_image = frame.copy()  # 存储最清晰的图像

            # 将帧转换为 RGB
            frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)

            # 将帧转换为 wx.Image
            height, width = frame_rgb.shape[:2]
            image = wx.Image(width, height, frame_rgb.tobytes())

            # 更新图像面板显示新帧
            wx.CallAfter(self.update_image, image)

    def update_image(self, image):
        """
        使用新图像帧更新图像面板。

        :param image: 要显示的 wx.Image。
        """
        bitmap = wx.Bitmap(image)  # 将图像转换为位图
        dc = wx.ClientDC(self.image_panel)  # 创建用于绘图的设备上下文
        dc.DrawBitmap(bitmap, 0, 0)  # 在面板上绘制位图

    def OnClose(self, event):
        """
        处理窗口关闭事件。

        :param event: 关闭事件对象。
        """
        self.is_running = False  # 停止捕获
        if self.cap:
            self.cap.release()  # 释放摄像头
        self.Destroy()  # 销毁窗口


if __name__ == '__main__':
    app = wx.App()  # 创建 wx 应用
    CameraApp(None)  # 初始化 CameraApp
    app.MainLoop()  # 启动应用循环

代码解析

整个程序的工作流程如下:

  1. 初始化阶段:创建窗口和用户界面元素,初始化摄像头和状态变量
  2. 摄像头切换:当用户选择新摄像头时,释放当前摄像头并打开新摄像头
  3. 视频捕获:在单独线程中持续捕获视频帧
  4. 清晰度检测:对每一帧计算清晰度评分
  5. 最佳画面保存:保存清晰度最高的画面
  6. 界面更新:将当前帧显示在界面上
  7. 资源释放:窗口关闭时释放摄像头资源

这个程序采用了多线程设计,将视频捕获和处理放在单独的线程中,避免阻塞 UI 线程,确保界面响应流畅。同时,使用了面向对象的设计方法,将功能封装在类中,使代码结构清晰,易于维护和扩展。

摄像头选型

通过这个小项目,我们实现了使用 Python 和相关库实现一个多摄像头实时显示上位机。从需求分析、系统设计到代码实现,我们完整地经历了一个项目的开发过程。后续经过优化扩展,这个系统不仅可以用于监控,还可以作为计算机视觉应用的基础平台,用于图像分析、物体识别等更高级的应用。

完整代码供大家参考、练手,有问题欢迎评论区交流!

最近发表
标签列表