如何为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
解决方案
核心思路
- 解决
toImage()返回Null的问题:需要以读写模式映射帧,且手动处理像素格式转换(部分格式无法直接通过toImage()转换); - 确保预览和录制都能拿到处理后的帧:不能直接让QMediaCaptureSession同时连接QVideoWidget和QMediaRecorder,需要通过自定义中间视频接收器,处理完帧后再分发给预览组件和录制器;
- 在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()
关键代码解释
- 自定义FrameProcessorSink:继承QVideoSink,重写
videoFrameReceived方法,负责接收原始帧、处理帧、转发处理后的帧到预览组件和录制器; - 帧转换与文本绘制:
- 以
ReadWrite模式映射帧,确保能修改帧数据; - 先尝试
toImage(),失败则手动通过帧的原始数据创建QImage,避免格式不兼容问题; - 使用QPainter在QImage上绘制文本,支持自定义字体、颜色和位置;
- 以
- 帧分发:将处理后的帧同时发送给QVideoWidget的视频接收器和QMediaRecorder的视频接收器,确保预览和录制的内容一致;
- 保持时间戳:设置
processed_frame.setTimestamp(frame.timestamp()),避免视频播放时出现时序问题。
注意事项
- 若摄像头输出的像素格式为YUV类格式(如YUYV、NV12),手动创建QImage时需注意像素格式的对应关系,可参考Qt文档中QVideoFrame像素格式与QImage格式的映射表;
- 文本绘制的性能会影响实时预览的流畅度,可根据需要调整字体大小、绘制复杂度,或采用异步处理方式。
内容的提问来源于stack exchange,提问作者Ben Griffiths
相关产品推荐
相关产品推荐

