PyQt QGraphicsScene 将场景渲染为视频格式

问题描述 投票:0回答:1

我试图允许用户将场景导出为mp4(视频格式),场景的项目由QGraphicsVideoItem和多个QGraphicsTextItem组成,我需要导出场景,因为它将允许用户保存视频文本项目。我找到了一种方法来做到这一点,但问题是,一个简单的 5 秒视频需要几个小时,因为它将每个图像保存到一个字节来创建视频,每个图像都是一毫秒。如果我从毫秒更改为秒,它可以加快速度,但视频看起来不会那么流畅,是否有更有效的方法来做到这一点,而不需要这么长时间?

from PySide6.QtCore import *
from PySide6.QtGui import *
from PySide6.QtWidgets import *
from PySide6.QtSvgWidgets import *
from PySide6.QtMultimediaWidgets import QGraphicsVideoItem
from PySide6.QtMultimedia import QMediaPlayer, QAudioOutput, QMediaMetaData 

import subprocess

import sys

class ExportVideo(QThread):
    def __init__(self, video_item, video_player, graphics_scene, graphics_view):
        super().__init__()
        self.video_item = video_item
        self.video_player = video_player
        self.graphics_scene = graphics_scene
        self.graphics_view = graphics_view
    
    def run(self):
        self.video_player.pause()
        duration = self.video_player.duration()
        meta = self.video_player.metaData()


        # Prepare a pipe for ffmpeg to write to
        ffmpeg_process = subprocess.Popen(['ffmpeg', '-y', '-f', 'image2pipe', '-r', '1000', '-i', '-', '-c:v', 'libx265', '-pix_fmt', 'yuv420p', 'output.mp4'], stdin=subprocess.PIPE)

        for duration in range(0, duration):
            self.video_player.setPosition(duration)

            # Add logic to render the frame here
            print("Exporting frame:", duration) 

            image = QImage(self.graphics_scene.sceneRect().size().toSize(), QImage.Format_ARGB32)
            painter = QPainter(image)
            self.graphics_scene.render(painter)
            painter.end()

            # Convert QImage to bytes
            byte_array = QByteArray()
            buffer = QBuffer(byte_array)
            buffer.open(QIODevice.WriteOnly)
            image.save(buffer, 'JPEG')

            # Write image bytes to ffmpeg process
            ffmpeg_process.stdin.write(byte_array.data())

        # Close the pipe to signal ffmpeg that all frames have been processed
        ffmpeg_process.stdin.close()
        ffmpeg_process.wait()


class PyVideoPlayer(QWidget):
    
    def __init__(self):
        super().__init__()

        self.text_data = []

        self.mediaPlayer = QMediaPlayer()
        self.audioOutput = QAudioOutput()

        self.graphics_view = QGraphicsView()
        self.graphic_scene = QGraphicsScene()

        self.graphics_view.setScene(self.graphic_scene)
        self.graphic_scene.setBackgroundBrush(Qt.black)
        self.graphics_view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.graphics_view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)

        self.video_item = QGraphicsVideoItem()
        self.graphic_scene.addItem(self.video_item)
        self.save_video = QPushButton()
        
        layout = QVBoxLayout()
        layout.addWidget(self.graphics_view, stretch=1)
        layout.addWidget(self.save_video)
        self.setLayout(layout)

        # Slots Section
        self.mediaPlayer.setVideoOutput(self.video_item)
        self.mediaPlayer.positionChanged.connect(self.changeVideoPosition)
        self.save_video.clicked.connect(self.saveVideo)

    def setMedia(self, fileName):
        self.mediaPlayer.setSource(QUrl.fromLocalFile(fileName))
        self.mediaPlayer.setAudioOutput(self.audioOutput)
        self.play()
        self.video_item.setSize(self.mediaPlayer.videoSink().videoSize())

        self.text_item = QGraphicsTextItem()
        self.text_item.setPlainText("Test Dummy")
        self.text_item.setDefaultTextColor(Qt.white)
        font = QFont()
        font.setPointSize(90)  
        self.text_item.setFont(font)
        self.text_item.setPos(self.graphic_scene.sceneRect().x() + self.text_item.boundingRect().width(), self.graphic_scene.sceneRect().center().y() - self.text_item.boundingRect().height())
        self.graphic_scene.addItem(self.text_item)
        self.text_data.append("Test Dummy")

    def play(self):
        if self.mediaPlayer.playbackState() == QMediaPlayer.PlaybackState.PlayingState:
            self.mediaPlayer.pause()
        else:
            self.mediaPlayer.play()

    def changeVideoPosition(self, duration):
        if duration > 1000 and self.text_item.isVisible():
            print("Hide Text")
            self.text_item.hide()

    def resize_graphic_scene(self):
        self.graphics_view.fitInView(self.graphic_scene.sceneRect(), Qt.KeepAspectRatio)

    def showEvent(self, event):
        self.resize_graphic_scene()

    def resizeEvent(self, event):
        self.resize_graphic_scene()

    def saveVideo(self):
        self.videoExport = ExportVideo(self.video_item, self.mediaPlayer, self.graphic_scene, self.graphics_view)
        self.videoExport.start()

    


if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = PyVideoPlayer()
    window.setMedia("example.mp4")
    window.setGeometry(100, 100, 400, 300)  # Set the window size
    window.setWindowTitle("QGraphicsView Example")
    window.show()
    sys.exit(app.exec())

  • 编辑:从PNG更改为JPEG,它加快了图像的保存速度,我也遇到了这个问题:
    Assertion fctx->async_lock failed at C:/ffmpeg-n6.0/libavcodec/pthread_frame.c:155.
    我认为这一定与设置视频播放器位置有关。
qt ffmpeg pyqt pyside6 qgraphicsscene
1个回答
0
投票

我认为您使用当前方法(UI 屏幕捕获来生成带注释的视频)将面临的问题是您可能会丢失原始视频分辨率(假设它是 1080p 但显示在 300x400 UI 帧中,那么您将最终得到 300x400 视频而不是 1080p)。

IMO 更好的方法是在 FFmpeg(或任何其他方式)中生成具有匹配帧分辨率(在我的示例中为 1080p)的具有透明背景的文本图像(PNG),并将每个此类图像作为 QGraphicItem 加载到 QGraphicScene 上(我认为可以拖动它来重新定位)。

这将为您提供背景视频和一堆文本图像及其显示时间和位置偏移。然后,FFmpeg 可以处理将这些文件合并在一起。

要生成文本PNG,您可以运行

import subprocess as sp

video_size = [1920, 1080]
text = "hello world"
color = "black"
fontsize = 30
fontfile = "Freeserif.ttf"

textfile = "temp_01.png" # place it in a temp folder & increment

sp.run(
    [
        "ffmpeg",
        "-f", "lavfi",
        "-i", f"color=c={color}@0:size={video_size[0]}x{video_size[1]},format=rgba",
        "-vf", f'drawtext=fontsize={fontsize}:fontfile={fontfile}:text=\'{text}\':x=(w-text_w)/2:y=(h-text_h)/2',
        "-update", "1", "-vframes", "1",
        "-y", # if needed to overwrite old
        textfile,
    ]
)

此脚本创建一个在屏幕中间带有“hello world”的 PNG。假设我们想将此文本放在视频的时间戳 1 和 2 之间,用户定义的偏移量为 [-200px,100px](朝向左下角)。


text_start = 1
text_end = 2
text_xoff = -200
text_yoff = 100

videofile = "example.mp4"
outfile = "annotated.mp4"

sp.run(
    [
        "ffmpeg",
        "-i", videofile,  # [0:v]
        "-i", textfile,  # [1:v]
        "-filter_complex", f"[0:v][1:v]overlay=x={text_xoff}:y={text_yoff}:enable='between(t,{text_start},{text_end})'[out]",
        "-map", "[out]",
        '-y', # again, if need to overwrite previous output
        outfile,
    ]
)

如果要叠加多个文本图像,则需要列出所有文本文件作为附加输入 (

-i ...
) 并级联运行叠加过滤器:

[0:v][1:v]overlay=...[out1];
[out1][2:v]overlay=...[out2];
...
[outN][N:v]overlay=...[out]

显然,您希望以编程方式生成过滤器图表达式。

这里是所用过滤器的链接

color
format
drawtext
overlay
。熟悉这些过滤器和生成过程中的过滤器图结构,尤其要注意字符转义。 (提示:将文本放在单引号中,并在覆盖文本中转义单引号)

注释

  • 如果文本太长,它会被视频帧截断
  • 一旦你了解了这个机制,你就可以看看 Qt 是否可以使用管道传输的 PNG 数据。 FFmpeg 支持使用数据协议的base64 编码。
  • overlay
    过滤器支持文本图像随时间移动,因此可以设置动画,但在 Qt 上显示动画会很痛苦

我对 Qt 业务端不熟悉。所以,我就把这个留给你了。

欢迎在评论区提问。

© www.soinside.com 2019 - 2024. All rights reserved.