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

如何为PyQt6的QVideoFrame添加或编辑文本?

如何在PyQt6的QVideoFrame中添加文本并在预览和录制视频中显示?

问题描述

我用PyQt6的QtMultimedia实现了摄像头实时采集与视频录制功能,想给QVideoFrame添加文本,让文本同时显示在QVideoWidget预览和导出的视频文件中。尝试过以下方法但都失败:

  • 使用map()、QVideoFrame.toImage()和unmap(),但toImage()始终返回Null QImage,换过多种摄像头格式(YUYV、JPEG、NV12等)都没用;
  • 使用QVideoFrame.addSubtitleText(),但该方法不会将文本嵌入视频帧,导出的视频里看不到文本。

环境:Python3.10、PyQt6.7.1

解决方案

核心思路

  1. 解决toImage()返回Null的问题:需要以读写模式映射帧,且手动处理像素格式转换(部分格式无法直接通过toImage()转换);
  2. 确保预览和录制都能拿到处理后的帧:不能直接让QMediaCaptureSession同时连接QVideoWidget和QMediaRecorder,需要通过自定义中间视频接收器,处理完帧后再分发给预览组件和录制器;
  3. 在QImage上绘制文本:使用QPainter在转换后的QImage上添加文本,再转回QVideoFrame。

修改后的完整代码

import os
from PyQt6.QtCore import QUrl, pyqtSlot, Qt
from PyQt6.QtGui import QImage, QPainter, QFont
from PyQt6.QtMultimedia import (
    QCamera,
    QCameraDevice,
    QMediaCaptureSession,
    QMediaDevices,
    QMediaRecorder,
    QVideoFrame,
    QVideoSink
)
from PyQt6.QtMultimediaWidgets import QVideoWidget
from PyQt6.QtWidgets import (
    QApplication,
    QComboBox,
    QLabel,
    QMainWindow,
    QPushButton,
    QVBoxLayout,
    QWidget,
)

video_path = os.path.join(os.getcwd(), "my_video.mp4")

class FrameProcessorSink(QVideoSink):
    def __init__(self, target_sink: QVideoSink, recorder_sink: QVideoSink, parent=None):
        super().__init__(parent)
        self.target_sink = target_sink
        self.recorder_sink = recorder_sink

    def videoFrameReceived(self, frame: QVideoFrame) -> None:
        if not frame.isValid():
            return
        
        # 以读写模式映射帧
        if not frame.map(QVideoFrame.MapMode.ReadWrite):
            return
        
        # 转换为可编辑的QImage(处理部分格式无法直接toImage的问题)
        # 先尝试直接转换,失败则手动创建QImage
        img = frame.toImage()
        if img.isNull():
            # 根据帧的像素格式选择对应的QImage格式,这里以RGB32为例,可根据实际格式调整
            img = QImage(
                frame.bits(),
                frame.width(),
                frame.height(),
                frame.bytesPerLine(),
                QImage.Format.Format_RGB32
            ).copy()
        
        # 在QImage上绘制文本
        painter = QPainter(img)
        painter.setPen(Qt.GlobalColor.red)
        painter.setFont(QFont("Arial", 24))
        # 在右下角绘制文本
        painter.drawText(
            img.rect(),
            Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignRight,
            "录制中..."
        )
        painter.end()
        
        # 将修改后的QImage转回QVideoFrame
        processed_frame = QVideoFrame(img)
        processed_frame.setTimestamp(frame.timestamp())
        
        # 分发给预览组件和录制器
        self.target_sink.setVideoFrame(processed_frame)
        self.recorder_sink.setVideoFrame(processed_frame)
        
        frame.unmap()

class Main(QMainWindow):
    def __init__(self):
        super().__init__()
        widget = QWidget()
        self.setCentralWidget(widget)
        self.main_layout = QVBoxLayout(widget)

        self.m_devices = QMediaDevices()
        self.devices_list = QComboBox()
        [
            self.devices_list.addItem(str(d.description()), d)
            for d in self.m_devices.videoInputs()
        ]
        self.devices_list.setCurrentIndex(-1)
        self.devices_list.activated.connect(
            lambda idx: self.set_camera(self.devices_list.itemData(idx))
        )
        self.main_layout.addWidget(QLabel("选择摄像头"))
        self.main_layout.addWidget(self.devices_list)

        self.start_btn = QPushButton("开始录制")
        self.start_btn.clicked.connect(self.start_recording)
        self.main_layout.addWidget(self.start_btn)

        self.stop_btn = QPushButton("停止录制")
        self.stop_btn.clicked.connect(self.stop_recording)
        self.main_layout.addWidget(self.stop_btn)

        self.video_widget = QVideoWidget()
        self.video_widget_sink = self.video_widget.videoSink()
        self.main_layout.addWidget(self.video_widget, 1)

        self.m_recorder = QMediaRecorder()
        self.m_recorder.recorderStateChanged.connect(lambda state: print(state))
        self.m_recorder.setOutputLocation(QUrl.fromLocalFile(video_path))
        self.m_recorder.errorOccurred.connect(lambda error, error_str: print(error_str))
        # 获取录制器的视频接收器
        self.recorder_sink = self.m_recorder.videoSink()

        # 创建自定义帧处理器,连接预览和录制器的接收器
        self.frame_processor = FrameProcessorSink(self.video_widget_sink, self.recorder_sink)

        self.m_capture_session = QMediaCaptureSession()
        # 将捕获会话的视频输出指向自定义处理器
        self.m_capture_session.setVideoSink(self.frame_processor)
        self.m_capture_session.setRecorder(self.m_recorder)

        self.camera = QCamera(QCameraDevice())

        self.show()
        self.resize(800, 600)

    def set_camera(self, camera: QCameraDevice):
        self.camera = QCamera(camera)
        self.m_capture_session.setCamera(self.camera)
        self.camera.start()

    def start_recording(self):
        self.m_recorder.record()

    def stop_recording(self):
        self.m_recorder.stop()

if __name__ == "__main__":
    app = QApplication([])
    main = Main()
    app.exec()

关键代码解释

  1. 自定义FrameProcessorSink:继承QVideoSink,重写videoFrameReceived方法,负责接收原始帧、处理帧、转发处理后的帧到预览组件和录制器;
  2. 帧转换与文本绘制:
    • 以ReadWrite模式映射帧,确保能修改帧数据;
    • 先尝试toImage(),失败则手动通过帧的原始数据创建QImage,避免格式不兼容问题;
    • 使用QPainter在QImage上绘制文本,支持自定义字体、颜色和位置;
  3. 帧分发:将处理后的帧同时发送给QVideoWidget的视频接收器和QMediaRecorder的视频接收器,确保预览和录制的内容一致;
  4. 保持时间戳:设置processed_frame.setTimestamp(frame.timestamp()),避免视频播放时出现时序问题。

注意事项

  • 若摄像头输出的像素格式为YUV类格式(如YUYV、NV12),手动创建QImage时需注意像素格式的对应关系,可参考Qt文档中QVideoFrame像素格式与QImage格式的映射表;
  • 文本绘制的性能会影响实时预览的流畅度,可根据需要调整字体大小、绘制复杂度,或采用异步处理方式。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 14:40:13