树莓派摄像头GUI开发:PySide6与QML实战指南
1. 项目缘起:为什么要在树莓派上折腾GUI和摄像头?
最近手头有个小项目,需要在一块树莓派4B上实现一个带图形界面的摄像头应用。需求听起来很简单:开机启动,屏幕上显示摄像头的实时画面,最好还能有几个按钮控制拍照、录像和调整参数。但真动起手来,发现这里面门道不少。直接用OpenCV的imshow()?在树莓派上跑起来窗口响应慢,而且界面太简陋,想加个好看的按钮都费劲。用Tkinter?功能是够了,但做出来的界面总感觉是上个世纪的产物,想实现流畅的动画或者复杂的布局比较吃力。
这时候,Qt框架就进入了视野。PySide6/PyQt6作为Qt在Python上的绑定,能让我们用Python的简洁语法调用Qt强大的GUI库,而QML则是Qt专门为声明式、动画丰富的用户界面设计的一门语言。把它们和树莓派的摄像头(通常是CSI接口的官方摄像头或USB摄像头)结合起来,就能打造出既专业又流畅的嵌入式图形应用。这个组合非常适合需要本地化显示、低延迟预览的场合,比如智能门禁、工业检测、移动机器人视觉终端等。
本文将基于树莓派OS(原Raspbian),手把手带你搭建环境,并分别通过PySide6和QML两种主流方式,实现一个功能完整的摄像头GUI应用。我会重点分享在资源受限的树莓派上如何保证性能、如何处理常见的摄像头兼容性问题,以及如何打包成开机自启的应用。如果你也在为嵌入式Linux设备开发视觉交互界面,这篇内容应该能帮你避开不少坑。
2. 环境准备与核心工具选型
在树莓派上玩转GUI和摄像头,第一步就是把基础环境打牢。树莓派OS默认已经包含了很多软件包,但我们还需要针对性地安装一些库。
2.1 系统更新与摄像头启用
首先,确保系统是最新的,并启用摄像头接口。
sudo apt update sudo apt full-upgrade -y更新完成后,需要启用摄像头硬件支持。通过树莓派的配置工具进行操作:
sudo raspi-config在配置界面中,依次选择Interface Options->Camera,然后选择<Yes>来启用摄像头接口。完成后会提示重启,选择确认重启树莓派。
重启后,可以通过一个简单的命令测试摄像头是否工作正常。对于CSI摄像头,可以使用libcamera套件提供的工具:
libcamera-hello -t 0这条命令会打开一个预览窗口并持续显示。如果看到画面,说明摄像头硬件和基础驱动没问题。按Ctrl+C退出。
注意:从树莓派OS Bullseye开始,默认的摄像头栈从旧的
raspistill/raspicam切换到了新的libcamera。新的架构更现代,支持更多摄像头,但一些旧的代码和库可能需要调整。我们后续的代码将主要基于libcamera或通用的OpenCV接口,以保证兼容性。
2.2 Python环境与Qt绑定安装
接下来是安装Python环境和Qt的Python绑定。树莓派OS通常预装了Python3,我们直接安装必要的包。
安装PySide6 (Qt for Python):这是Qt官方提供的Python绑定,许可更宽松(LGPL)。
sudo apt install python3-pip pip3 install pyside6安装OpenCV-Python:OpenCV是我们处理摄像头帧数据的关键库。在树莓派上,推荐使用apt安装预编译版本,虽然版本可能稍旧,但免去了漫长的编译过程,且更稳定。
sudo apt install python3-opencv安装后,可以在Python中测试一下:
import cv2 print(cv2.__version__)关于PyQt6的选型说明:你可能会看到PyQt6。PyQt6是Riverbank Computing公司开发的绑定,功能同样强大且成熟。但它的许可(GPL/商业许可)对于某些商业项目可能不如PySide6的LGPL友好。两者在API层面高度相似,绝大多数代码可以互相替换。本文选择PySide6,但如果你熟悉PyQt6,迁移过来几乎无成本。安装PyQt6的命令是pip3 install pyqt6。
安装QML运行环境:如果我们想用QML来设计界面,还需要安装Qt Declarative模块以及QML运行时。对于PySide6,相关模块已经包含在pyside6包中。但我们可能还需要一些额外的QML插件,可以通过apt安装:
sudo apt install qml6-module-qtquick qml6-module-qtquick-controls qml6-module-qtquick-layouts qml6-module-qtquick-window至此,核心的软件环境就准备好了。简单来说,我们搭建了一个由PySide6 (GUI框架)+OpenCV (视觉处理)+libcamera/Qt Multimedia (图像采集)组成的工具链。
3. 方案一:使用PySide6与OpenCV实现摄像头GUI
这是最直接、最受Python开发者欢迎的方案。其核心思路是:使用OpenCV (cv2.VideoCapture) 从摄像头抓取每一帧图像,然后将OpenCV的图像格式(BGR NumPy数组)转换为Qt能显示的格式(QImage/QPixmap),最后在PySide6的窗口部件(如QLabel)中实时更新显示。
3.1 核心架构与线程模型
首先必须理解一个关键点:GUI主线程不能阻塞。OpenCV的cv2.VideoCapture.read()是一个阻塞调用,如果它在主线程中执行,窗口就会在等待下一帧时“卡住”,无法响应用户的点击等操作。
因此,正确的架构是使用多线程:
- 主线程 (GUI线程):负责创建和管理窗口、按钮等所有UI元素,并处理用户交互事件。
- 摄像头采集线程:在一个独立的工作线程中,循环调用
read()方法抓取帧,然后通过信号(Signal)将帧数据发送回主线程进行显示。
PySide6的QThread和Signal/Slot机制完美支持这种跨线程通信。下面我们一步步实现。
3.2 代码实现:从零搭建应用
我们先创建一个完整的Python脚本,比如camera_app_pyside.py。
第一步:导入必要的库
import sys import cv2 import numpy as np from PySide6.QtCore import Qt, QThread, Signal, Slot from PySide6.QtGui import QImage, QPixmap from PySide6.QtWidgets import QApplication, QMainWindow, QLabel, QPushButton, QVBoxLayout, QWidget, QHBoxLayout第二步:创建摄像头工作线程
这个线程类负责所有与摄像头硬件的交互。
class CameraThread(QThread): # 定义一个信号,用于将捕获到的帧(NumPy数组)发送出去 change_pixmap_signal = Signal(np.ndarray) def __init__(self, camera_index=0): super().__init__() self.camera_index = camera_index self._run_flag = True # 线程运行标志 def run(self): # 线程的主循环 cap = cv2.VideoCapture(self.camera_index) # 尝试设置一个合理的分辨率,例如640x480,以减轻树莓派的负担 cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) if not cap.isOpened(): print(f"错误:无法打开摄像头索引 {self.camera_index}") return while self._run_flag: ret, frame = cap.read() if ret: # 成功捕获一帧,通过信号发射出去 self.change_pixmap_signal.emit(frame) else: print("从摄像头读取帧失败") break # 可以在这里加入一个小的延时来控制帧率,例如 time.sleep(0.03) 对应约30FPS # 循环结束,释放摄像头 cap.release() print("摄像头线程退出") def stop(self): """优雅地停止线程""" self._run_flag = False self.wait() # 等待线程真正结束实操心得:摄像头索引与格式:在树莓派上,
cv2.VideoCapture(0)通常指向系统默认的摄像头(USB摄像头)。对于CSI摄像头,你可能需要使用cv2.VideoCapture(-1)或cv2.VideoCapture('/dev/video0'),具体取决于驱动和libcamera的兼容层libcamera-vid。一个更可靠的方法是使用v4l2-ctl --list-devices命令列出所有视频设备。如果OpenCV无法直接打开CSI摄像头,可以考虑使用picamera2库(专为libcamera设计),其获取的帧也可以转换为OpenCV格式。
第三步:创建主窗口类
主窗口负责布局UI,并连接摄像头线程的信号。
class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("树莓派摄像头 (PySide6+OpenCV)") self.setGeometry(100, 100, 800, 600) # 创建中央部件和布局 central_widget = QWidget() self.setCentralWidget(central_widget) layout = QVBoxLayout(central_widget) # 用于显示图像的标签 self.image_label = QLabel() self.image_label.setAlignment(Qt.AlignCenter) self.image_label.setMinimumSize(640, 480) layout.addWidget(self.image_label) # 创建按钮布局 button_layout = QHBoxLayout() self.start_button = QPushButton("开始") self.stop_button = QPushButton("停止") self.capture_button = QPushButton("拍照") self.stop_button.setEnabled(False) self.capture_button.setEnabled(False) button_layout.addWidget(self.start_button) button_layout.addWidget(self.stop_button) button_layout.addWidget(self.capture_button) layout.addLayout(button_layout) # 连接按钮信号 self.start_button.clicked.connect(self.start_camera) self.stop_button.clicked.connect(self.stop_camera) self.capture_button.clicked.connect(self.capture_image) # 初始化摄像头线程 self.camera_thread = CameraThread() # 连接摄像头线程的信号到主线程的槽函数 self.camera_thread.change_pixmap_signal.connect(self.update_image) # 存储当前帧,用于拍照 self.current_frame = None @Slot(np.ndarray) def update_image(self, cv_img): """更新显示的图像。这个槽函数在主线程中被调用。""" self.current_frame = cv_img.copy() # 保存一份用于拍照 # 将OpenCV的BGR图像转换为RGB rgb_image = cv2.cvtColor(cv_img, cv2.COLOR_BGR2RGB) # 获取图像尺寸和通道数 h, w, ch = rgb_image.shape bytes_per_line = ch * w # 将NumPy数组转换为QImage qt_image = QImage(rgb_image.data, w, h, bytes_per_line, QImage.Format_RGB888) # 缩放图像以适应QLabel,同时保持宽高比 scaled_pixmap = QPixmap.fromImage(qt_image).scaled( self.image_label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation ) # 在QLabel上显示 self.image_label.setPixmap(scaled_pixmap) def start_camera(self): if not self.camera_thread.isRunning(): self.camera_thread.start() self.start_button.setEnabled(False) self.stop_button.setEnabled(True) self.capture_button.setEnabled(True) def stop_camera(self): if self.camera_thread.isRunning(): self.camera_thread.stop() self.start_button.setEnabled(True) self.stop_button.setEnabled(False) self.capture_button.setEnabled(False) # 清空显示 self.image_label.clear() def capture_image(self): if self.current_frame is not None: # 生成一个文件名,包含时间戳 from datetime import datetime filename = datetime.now().strftime("capture_%Y%m%d_%H%M%S.jpg") # OpenCV保存的是BGR格式,直接保存即可 cv2.imwrite(filename, self.current_frame) print(f"图像已保存为: {filename}")第四步:启动应用
if __name__ == "__main__": app = QApplication(sys.argv) window = MainWindow() window.show() sys.exit(app.exec())将以上代码保存并运行python3 camera_app_pyside.py,你应该能看到一个带开始、停止、拍照按钮的窗口,点击开始后摄像头画面会实时显示。
3.3 性能优化与常见问题排查
在树莓派上运行这个程序,你可能会遇到性能问题或奇怪的现象,以下是几个关键优化点和排查思路:
分辨率与帧率:默认的
cv2.VideoCapture可能以最高分辨率捕获,这对树莓派的CPU和内存是巨大负担。务必在run方法中设置一个适中的分辨率(如640x480或320x240)。你还可以尝试设置cv2.CAP_PROP_FPS来限制帧率。图像转换开销:
cv2.cvtColor和QImage的创建在每一帧都会发生,这是性能瓶颈。一个优化技巧是:如果不需要彩色,可以捕获灰度图(cv2.COLOR_BGR2GRAY),数据量减少三分之二。更激进的做法是使用内存映射或共享内存,但这会大大增加代码复杂度。CSI摄像头的兼容性:这是最大的坑。如前所述,OpenCV +
libcamera可能需要桥接。一个更现代的方案是使用Picamera2库。它是libcamera的Python封装,性能更好,与CSI摄像头原生兼容。下面是使用Picamera2替换OpenCV采集线程的简要示例:from picamera2 import Picamera2 from libcamera import Transform class CameraThread(QThread): change_pixmap_signal = Signal(np.ndarray) def __init__(self): super().__init__() self._run_flag = True self.picam2 = Picamera2() # 配置预览模式,分辨率可调 preview_config = self.picam2.create_preview_configuration(main={"size": (640, 480)}) self.picam2.configure(preview_config) def run(self): self.picam2.start() while self._run_flag: # 获取一帧,格式为BGR frame = self.picam2.capture_array() self.change_pixmap_signal.emit(frame) self.picam2.stop() def stop(self): self._run_flag = False self.wait()使用Picamera2通常能获得更稳定、帧率更高的CSI摄像头支持。
线程安全与资源释放:确保
stop方法被正确调用,特别是在窗口关闭时。你需要重写主窗口的closeEvent,确保先停止摄像头线程再退出。class MainWindow(QMainWindow): # ... 其他代码 ... def closeEvent(self, event): if self.camera_thread.isRunning(): self.camera_thread.stop() event.accept()
4. 方案二:使用QML与Qt Multimedia实现摄像头GUI
如果你对界面美观度、动画效果有更高要求,或者希望界面逻辑(UI)与业务逻辑(C++/Python)更清晰地分离,那么QML是更好的选择。QML是一种声明式语言,写界面像写JSON一样直观,并且原生支持各种炫酷的动画和状态切换。
Qt Multimedia模块提供了直接访问摄像头和多媒体功能的QML类型,我们可以完全不用OpenCV,直接在QML里调用摄像头并显示。
4.1 QML方案的优势与工作流程
优势:
- 界面与逻辑分离:
.qml文件定义界面,Python/C++文件处理业务逻辑,耦合度低。 - 开发效率高:声明式语法使得构建复杂、动态的UI非常快速。
- 性能优异:Qt Multimedia底层可能使用硬件加速(如树莓派的GPU),视频渲染效率高。
- 动画支持强大:内置丰富的动画类型,轻松实现平滑过渡。
工作流程:
- Python端(使用PySide6)创建一个
QQuickView或QQmlApplicationEngine来加载并运行QML文件。 - QML文件中,使用
MediaDevices、Camera、VideoOutput等类型直接捕获和显示摄像头画面。 - 通过PySide6将Python对象暴露给QML环境,从而实现QML界面与Python后端逻辑的交互(例如,点击QML按钮触发Python函数保存图片)。
4.2 代码实现:纯QML界面与Python后端
这个例子我们将创建一个更现代、更“Qt范儿”的应用。
第一步:创建QML界面文件 (main.qml)
import QtQuick import QtQuick.Controls import QtMultimedia ApplicationWindow { id: window width: 800 height: 600 visible: true title: qsTr("树莓派摄像头 (QML)") // 媒体设备管理器,用于获取摄像头列表 MediaDevices { id: mediaDevices } // 摄像头对象 Camera { id: camera // 可以在这里选择特定的摄像头,例如:cameraDevice: mediaDevices.videoInputs[0] // 对于树莓派,通常只有一个摄像头,使用默认即可。 } // 视频输出,用于显示摄像头画面 VideoOutput { id: videoOutput anchors.fill: parent source: camera // 将摄像头作为视频源 fillMode: VideoOutput.PreserveAspectFit // 保持宽高比适应 } // 底部控制栏 footer: ToolBar { Row { anchors.centerIn: parent spacing: 20 Button { text: camera.playbackState === Camera.ActiveState ? qsTr("停止") : qsTr("开始") onClicked: { if (camera.playbackState === Camera.ActiveState) { camera.stop() } else { camera.start() } } } Button { text: qsTr("拍照") enabled: camera.playbackState === Camera.ActiveState onClicked: { // 调用Python后端提供的拍照函数 pythonBackend.captureImage() } } ComboBox { id: cameraSelector model: mediaDevices.videoInputs // 摄像头列表模型 textRole: "description" onActivated: { // 切换摄像头 camera.cameraDevice = mediaDevices.videoInputs[currentIndex] } Component.onCompleted: { // 如果有摄像头,默认选择第一个 if (model.length > 0) { currentIndex = 0 camera.cameraDevice = mediaDevices.videoInputs[0] } } } } } }这个QML文件定义了一个带有全屏视频预览、底部控制栏(开始/停止、拍照、摄像头选择)的窗口。界面逻辑非常清晰。
第二步:创建Python后端与QML引擎集成 (main_qml.py)
Python端主要负责启动QML引擎,并将一个Python对象(作为后端逻辑)注入到QML上下文中。
import sys import os from datetime import datetime from PySide6.QtCore import QObject, Slot, Property, Signal from PySide6.QtGui import QGuiApplication from PySide6.QtQml import QQmlApplicationEngine from PySide6.QtMultimedia import QMediaDevices # 定义一个Python对象,它将暴露给QML class Backend(QObject): # 可以定义一个信号,用于通知QML拍照完成 captureFinished = Signal(str) def __init__(self): super().__init__() @Slot() def captureImage(self): """被QML调用的拍照函数""" # 注意:在纯QML+Qt Multimedia方案中,我们无法直接在这里获取到摄像头帧的像素数据。 # 因为帧数据在QML的VideoOutput中渲染,没有直接暴露给Python。 # 因此,这个方案更适合“控制”而非“图像处理”。 # 如果需要处理图像,通常需要: # 1. 使用方案一(OpenCV线程)获取帧并处理,然后将处理后的帧用某种方式传给QML显示(复杂)。 # 2. 或者,在QML端使用`Camera.imageCapture`进行拍照,然后将图片路径传给Python后端。 print("拍照指令已收到(纯QML方案,需结合其他方式获取图像数据)") # 模拟一个完成信号 filename = datetime.now().strftime("qml_capture_%Y%m%d_%H%M%S.jpg") self.captureFinished.emit(filename) if __name__ == "__main__": app = QGuiApplication(sys.argv) # 创建后端对象 backend = Backend() # 创建QML引擎 engine = QQmlApplicationEngine() # 将后端对象暴露给QML,在QML中可以通过 `pythonBackend` 访问 engine.rootContext().setContextProperty("pythonBackend", backend) # 加载QML文件 qml_file = os.path.join(os.path.dirname(__file__), "main.qml") engine.load(qml_file) if not engine.rootObjects(): sys.exit(-1) sys.exit(app.exec())运行python3 main_qml.py,你会看到一个界面更现代、带有摄像头选择下拉框的应用。点击“开始”按钮,摄像头画面会直接通过Qt Multimedia的管道显示在窗口中,通常比OpenCV方案更流畅。
4.3 QML方案的局限性与混合架构
从上面的代码可以看到,纯QML+Qt Multimedia方案的一个主要局限是:图像数据被封装在Qt Multimedia内部,Python后端难以直接访问原始的图像帧进行OpenCV等库的处理。
解决方案:混合架构。这是更强大的模式,结合了方案一和方案二的优点:
- 图像采集与处理仍在Python端:使用方案一的
CameraThread(基于OpenCV或Picamera2)在后台抓取并处理帧。 - 界面使用QML:用QML构建炫酷的UI。
- 通过信号桥接:将Python端处理后的帧(例如,转换为Base64字符串或保存为临时文件)通过信号发送给QML,QML端用
Image元素加载显示。或者,更高效的方式是,Python端将帧数据写入一个共享内存或纹理,QML端直接读取渲染(这需要更底层的C++扩展,复杂度高)。
一个简单的混合架构示例是,Python后端不断将帧保存为JPEG文件,QML端的Image元素定时加载这个文件来更新显示。但这效率较低,仅适用于低帧率场景。对于高性能需求,建议研究如何使用QQuickImageProvider或自定义QML项来直接传递图像数据。
5. 部署实战:打包与开机自启动
开发完成后,我们需要让这个应用能在树莓派上稳定运行,最好是能开机自动启动,像一个真正的嵌入式产品一样。
5.1 将Python脚本打包为可执行文件
使用PyInstaller可以将Python脚本及其所有依赖打包成一个独立的可执行文件,方便分发和运行。
首先安装PyInstaller:
pip3 install pyinstaller然后针对我们的PySide6应用进行打包。由于PySide6应用需要Qt的运行时文件,打包命令相对复杂:
cd /path/to/your/project pyinstaller --onefile --windowed --name CameraApp \ --add-data=".:." \ # 如果需要包含QML文件等资源,这样添加 --hidden-import PySide6.QtCore \ --hidden-import PySide6.QtGui \ --hidden-import PySide6.QtWidgets \ --hidden-import PySide6.QtMultimedia \ --hidden-import PySide6.QtQml \ camera_app_pyside.py--onefile打包成单个文件,--windowed表示是GUI应用(不显示控制台窗口)。打包过程可能会比较慢,完成后会在dist/目录下生成CameraApp可执行文件。你可以直接运行它测试。
踩坑记录:PyInstaller与树莓派:在树莓派ARM架构上使用PyInstaller有时会遇到奇怪的动态库链接问题。一个更稳妥的方法是使用
--paths参数显式指定PySide6库的路径,例如--paths /usr/lib/python3.9/site-packages。如果打包后运行报错缺少模块,通常需要手动--hidden-import添加。
5.2 配置系统服务实现开机自启
我们希望应用在树莓派启动后自动运行,并且如果崩溃了能自动重启。最好的方式是创建一个systemd服务。
创建服务文件:
sudo nano /etc/systemd/system/camera-app.service编辑服务内容:
[Unit] Description=Camera GUI Application After=graphical.target # 如果是桌面环境,等图形界面启动后 # 或者使用 After=multi-user.target 用于非桌面环境(需要自己设置DISPLAY) [Service] Type=simple User=pi # 以pi用户运行 Environment="DISPLAY=:0" # 非常重要!告诉服务图形显示在哪里 Environment="XAUTHORITY=/home/pi/.Xauthority" # X授权文件路径 WorkingDirectory=/home/pi/camera_app # 你的应用所在目录 ExecStart=/usr/bin/python3 /home/pi/camera_app/camera_app_pyside.py # 如果使用打包后的可执行文件:ExecStart=/home/pi/camera_app/dist/CameraApp Restart=on-failure # 失败时重启 RestartSec=5s [Install] WantedBy=multi-user.target关键点解释:
After=graphical.target: 确保桌面环境完全启动后再运行我们的应用。Environment="DISPLAY=:0": 这是最关键的设置。systemd服务默认没有图形显示环境,必须手动设置DISPLAY变量指向正确的X Server显示(通常是:0)。Environment="XAUTHORITY=...": X Server的认证文件路径,通常位于用户家目录下。需要确保运行服务的用户(这里是pi)有权限读取这个文件。Restart=on-failure: 增加 robustness,应用意外退出会自动重启。
启用并启动服务:
sudo systemctl daemon-reload sudo systemctl enable camera-app.service # 启用开机自启 sudo systemctl start camera-app.service # 立即启动服务 sudo systemctl status camera-app.service # 查看服务状态调试服务: 如果服务启动失败,首先检查状态信息
sudo systemctl status camera-app.service -l。 更详细的日志可以查看journal:sudo journalctl -u camera-app.service -f。 最常见的错误就是DISPLAY或XAUTHORITY环境变量设置不对,导致应用无法创建窗口。
5.3 针对无显示器(Headless)模式的优化
如果你的树莓派最终不会连接物理显示器,而是通过VNC或直接远程桌面访问,或者应用只需要在后台处理图像而不需要显示,那么需要调整策略。
虚拟显示:可以使用
xvfb(X Virtual Framebuffer) 创建一个虚拟的显示环境。这样,即使没有物理显示器,GUI应用也能正常运行。sudo apt install xvfb然后修改systemd服务的
ExecStart为:ExecStart=/usr/bin/xvfb-run -a --server-args="-screen 0 1024x768x24" /usr/bin/python3 /home/pi/camera_app/camera_app_pyside.pyxvfb-run会自动分配一个虚拟的DISPLAY给应用。移除GUI依赖:如果应用根本不需要显示窗口(例如,只是一个图像处理服务器),那么最好重构代码,移除PySide6/QML的GUI部分,只保留图像采集和处理逻辑,通过网络接口或文件来提供结果。这能大幅减少资源占用。
6. 进阶功能与扩展思路
一个基础的摄像头GUI应用已经完成。但在实际项目中,我们往往需要更多功能。这里提供几个扩展方向:
1. 增加图像处理流水线:在CameraThread的循环中,在emit(frame)之前,可以对frame进行各种OpenCV处理,如灰度化、边缘检测(Canny)、人脸识别、颜色追踪等。然后将处理后的帧发送给GUI显示。这就能实现一个实时的视觉处理终端。
2. 视频录制功能:利用OpenCV的VideoWriter类,在开始录制时创建写入器,在每一帧循环中将帧写入文件,停止时释放。注意树莓派的写入速度,建议使用MJPG或H264编码(如果硬件支持),并选择较低的分辨率和帧率以保证流畅。
3. 网络流媒体:使用Flask或FastAPI搭建一个简单的HTTP服务器,在摄像头线程中,将最新的帧编码为JPEG,并通过HTTP接口提供。这样,你就可以在同一个网络的任何设备的浏览器上查看实时视频流。这常用于监控场景。
4. 参数控制界面:在GUI上增加滑块(QSlider)或旋钮,用于实时调整摄像头的曝光、对比度、饱和度等参数。对于OpenCV的VideoCapture,可以使用cap.set(cv2.CAP_PROP_EXPOSURE, value)来设置。对于Picamera2,则有更丰富的API。
5. 多摄像头支持:如果你的项目需要多个USB摄像头,可以创建多个CameraThread实例,每个对应一个摄像头索引(0, 1, 2...)。在GUI中用多个QLabel或QML的VideoOutput来同时显示。需要特别注意树莓派USB总线的带宽和CPU负载,同时处理多路高清视频流可能力不从心。
我个人在树莓派上部署这类应用时,最大的体会是“平衡”。树莓派的算力有限,必须在功能、分辨率和帧率之间做出取舍。例如,一个640x480@15FPS的灰度视频处理流水线可能很流畅,但一旦换成彩色并加上复杂算法,延迟就会明显增加。因此,在开发初期就要用实际硬件进行性能测试,确定可接受的参数范围。另外,散热也很关键,长时间高负载运行务必考虑加装散热片或风扇。