winforms 在C#中使用ffmpeg提取帧时,帧速率慢,资源占用高

zujrkrfu  于 2023-08-07  发布在  C#
关注(0)|答案(1)|浏览(153)

我目前正在做一个项目,我需要在C#中使用ffmpeg从视频中提取帧。然而,我面临着帧速率慢和资源使用率高的问题。我使用的代码如下:

private bool move = false;
private int master_frame = 0;

private void pic()
{
    using (Process process = new Process())
    {
        process.StartInfo.FileName = "C:/Users/lenovo/Desktop/ffmpeg.exe";
        process.StartInfo.Arguments = $"-i \"C:/Users/lenovo/Desktop/New folder/video.mp4\" -vf \"select=gte(n\\,{master_frame})\" -vframes 1 -q:v 2 -f image2pipe -c:v bmp -";
        process.StartInfo.UseShellExecute = false;
        process.StartInfo.CreateNoWindow = true;
        process.StartInfo.RedirectStandardOutput = true;
        process.StartInfo.RedirectStandardError = true;
        process.Start();

        using (MemoryStream outputStream = new MemoryStream())
        {
            byte[] buffer = new byte[1024];
            int bytesRead;
            while ((bytesRead = process.StandardOutput.BaseStream.Read(buffer, 0, buffer.Length)) > 0)
            {
                outputStream.Write(buffer, 0, bytesRead);
            }

            pictureBox1.Invoke((MethodInvoker)(() =>
            {
                pictureBox1.Image?.Dispose();
                pictureBox1.Image = new Bitmap(outputStream);
            }));
        }
    }
}

private async void panel1_MouseUp(object sender, MouseEventArgs e)
{
    move = true;
    await Task.Run(() =>
    {
        while (move)
        {
            pic();
            master_frame++;
        }
    });
}

字符串
问题是帧速率相当慢,并且资源使用率高于预期。我怀疑阅读ffmpeg的输出流并从MemoryStream为每个帧创建位图可能会导致性能问题。
我将感谢任何关于如何优化帧提取过程以获得更好的性能和更低的资源使用的见解或建议。有没有更有效的方法在C#中使用ffmpeg从视频中提取帧?是否有其他方法或优化可以帮助提高帧提取速度?
提前感谢您的帮助和建议!

shstlldc

shstlldc1#

**TLDR:**非常低的帧率是由于启动ffmpeg进程和ffmpeg开始发送实际数据之间存在巨大的延迟。

My answer to your other post ("How to Play a Video in a PictureBox Using FFmpeg in C#?"), which is essentially the same as this post, shows how to do it much faster, although still not at 60 frames per second

性能测量

使用Stopwatch es,我测量了代码中每一步所花费的时间。

  • 注意:我已经将格式从bmp更改为png,用于较小的单个帧大小 *

代码

下面是我的measuments代码:

private bool move = false;
private int master_frame = 0;

Stopwatch sw = new Stopwatch();
private void pic()
{
    using (Process process = new Process())
    {
        process.StartInfo.FileName = "ffmpeg.exe";
        process.StartInfo.Arguments = $"-i \"video.mp4\" -vf \"select=gte(n\\,{master_frame})\" -vframes 1 -q:v 2 -f image2pipe -c:v png -";
        process.StartInfo.UseShellExecute = false;
        process.StartInfo.CreateNoWindow = true;
        process.StartInfo.RedirectStandardOutput = true;
        process.StartInfo.RedirectStandardError = true;
        process.Start();

        using (MemoryStream outputStream = new MemoryStream())
        {
            Console.WriteLine("-");

            sw.Restart();
            byte[] buffer = new byte[1024];
            int bytesRead;
            while ((bytesRead = process.StandardOutput.BaseStream.Read(buffer, 0, buffer.Length)) > 0)
            {
                outputStream.Write(buffer, 0, bytesRead);
            }
            sw.Stop();
            Console.WriteLine("Reading bytes: " + sw.ElapsedMilliseconds + "ms");

            framePictureBox.Invoke((MethodInvoker)(() =>
            {
                sw.Restart();
                framePictureBox.Image?.Dispose();
                sw.Stop();
                Console.WriteLine("Disposing of old picture: " + sw.ElapsedMilliseconds + "ms");

                sw.Restart();
                Bitmap bmp = new Bitmap(outputStream);
                sw.Stop();
                Console.WriteLine("Converting stream to bitmap: " + sw.ElapsedMilliseconds + "ms");

                sw.Restart();
                framePictureBox.Image = bmp;
                sw.Stop();
                Console.WriteLine("Assigning to PictureBox: " + sw.ElapsedMilliseconds + "ms");

                sw.Restart();
                framePictureBox.Refresh();
                sw.Stop();
                Console.WriteLine("Rendering: " + sw.ElapsedMilliseconds + "ms");
            }));
        }
    }
}

字符串

测量

这是我的结果,对于一个480 p 30 fps的视频,长度为1分36秒,在我的电脑上,我平均有:

  • 阅读ffmpeg发送的字节:110ms,峰值在300ms附近
  • 处理前一帧:<1ms
  • 将流转换为位图:1ms
  • 将转换后的位图分配给PictureBox:<1ms
  • 将PictureBox呈现到屏幕:2ms

提醒一下,要达到60 FPS,您需要在16.67ms33.33ms中渲染每一帧以获得30 FPS。

从中我们能理解什么

这里的罪魁祸首是读取ffmpeg发送的流所花费的时间,这是相当明显的。
顺便说一下,我也试过@Charlieface的建议,不,使用process.StandardOutput.BaseStream.CopyTo(outputStream);并没有保存多少时间。
事实上,你可以直接使用new Bitmap(process.StandardOutput.BaseStream);,但这并不能使整个事情变得更快。

真实的的问题

起初,指责ffmpeg将我们要求的帧转换为所需格式的速度太慢似乎是显而易见的。
正如@ChristophRackwitz所指出的,这确实是代码缓慢的一个因素。
当请求ffmpeg获取特定帧时,它实际上需要解码直到该帧的整个视频流,从而导致随着时间的推移,每帧所花费的时间越来越多。
这不是一个问题,虽然在我身边,因为我用了一个非常低的质量和帧率的视频。因此,我的测量是极简主义的,在实践中,渲染每一帧所需的时间可能比我测量的要长得多。我让你想象一下,获得一个1小时长的4K 144 fps视频的最后一帧所需的时间。
还有另一个因素,它是关于流程创建的。
事情是这样的,当你启动一个新的ffmpeg进程示例时,会发生以下情况:

  • Windows需要分配空间来 Boot 新进程
  • ffmpeg需要要求Windows打开您的视频文件
  • Windows给ffmpeg一个文件句柄
  • ffmpeg需要在完成转换时向Windows请求一个流来输出其数据
  • Windows给ffmpeg一个流来发送它的数据
  • ffmpeg终于可以完成它的工作并将其发送到流中
  • ffmpeg需要告诉Windows关闭视频文件,因为它已经完成了
  • Windows关闭文件
  • ffmpeg需要告诉Windows关闭它打开的流,因为它不再需要它
  • Windows关闭流
  • ffmpeg需要告诉Windows它已经完成并且可以关闭
  • Windows可以释放ffmpeg进程

这是相当多的,对不对?
这里的问题是,在所有这些步骤中,只有一个步骤对每个帧都是唯一的,那就是“ffmpeg终于可以完成它的工作并将其发送到流”。
所有其他步骤,这需要大量的时间,重复每帧。
因此,my answer to your other post,其中不是为每个帧打开一个流,而是为所有帧打开一个流并真实的渲染它们。这避免了为每个帧创建新的处理示例,并且还避免了必须从每个帧的开始解码整个视频流。

最终解决方案

你不能用你的代码目前的工作方式来提高它的速度,你需要从每帧一个进程切换到每帧一个进程,这就是my answer to your other post所做的。
感谢阅读。

相关问题