慢的帧速率和在C#中使用FFmpeg提取帧时的高资源使用率。

huangapple go评论83阅读模式
英文:

Slow frame rate and high resource usage when extracting frames with ffmpeg in C#

问题

我目前正在进行一个项目,需要在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的输出流并为每一帧创建一个Bitmap可能导致性能问题。

我会感激任何关于如何优化帧提取过程以获得更好性能和更低资源使用的见解或建议。是否有一种更有效的方法来使用C#从视频中提取帧?是否有任何可以帮助提高帧提取速度的替代方法或优化措施?

提前感谢您的帮助和建议!

英文:

I'm currently working on a project where I need to extract frames from a video using ffmpeg in C#. However, I'm facing issues with a slow frame rate and high resource usage. The code I'm using is as follows:

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++;
        }
    });
}

The issue is that the frame rate is quite slow, and the resource usage is higher than expected. I suspect that reading the output stream of ffmpeg and creating a Bitmap from a MemoryStream for each frame might be causing the performance issues.

I would appreciate any insights or suggestions on how to optimize the frame extraction process for better performance and lower resource usage. Is there a more efficient way to extract frames from a video using ffmpeg in C#? Are there any alternative approaches or optimizations that could help improve the frame extraction speed?

Thank you in advance for your help and suggestions!

答案1

得分: 2

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

我在你的另一篇文章中的回答("如何在C#中使用FFmpeg在PictureBox中播放视频?")基本上与这篇文章相同,但显示了如何更快地完成,尽管仍然不能达到每秒60帧

性能测量

使用Stopwatch,我已经测量了您代码中每个步骤所需的时间。

注意:我已将格式从bmp更改为png以获得较小的单个帧大小

代码

以下是我的测量代码:

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");
            }));
        }
    }
}

测量结果

以下是我的结果,对于一部480p 30fps的1分36秒长视频,在我的计算机上,我平均有:

  • 读取ffmpeg发送的字节:110ms,峰值约为300ms
  • 处理以前的帧:<1ms
  • 将流转换为位图:1ms
  • 将转换后的位图分配给PictureBox:<1ms
  • 将PictureBox渲染到屏幕上:2ms

作为提醒,要实现60 FPS,您需要在16.67ms内渲染每个帧,或者在30 FPS时为33.33ms

从中我们能得出什么

很明显,问题的罪魁祸首是读取ffmpeg发送的流所需的时间。

顺便说一句,我也尝试了@Charlieface的建议,但是,使用process.StandardOutput.BaseStream.CopyTo(outputStream);并没有节省太多时间。

事实上,您可以直接使用new Bitmap(process.StandardOutput.BaseStream);,但这并没有使整个过程更快。

真正的问题

起初,显然可以责怪ffmpeg将我们要求的帧转换为所需格式的速度较慢。

正如@ChristophRackwitz指出的那样,这确实是您的代码运行速度慢的一个因素。

当要求ffmpeg获取特定帧时,实际上需要解码直到该帧的整个视频流,这导致随着时间的推移,每个帧都需要更多的时间。

尽管在我的情况下这并不是一个问题,因为我使用的视频质量和帧速率非常低。因此,我的测量是最简单的,实际上,渲染每个帧可能需要的时间要比我测量的要长得多。您可以想象渲染1小时长的4K 144fps视频的最后一帧需要多长时间。

还有另一个因素,与进程创建有关。

问题在于,当您启动一个新的ffmpeg进程实例时,会发生以下情况:

  • Windows需要分配空间来启动新的进程
  • ffmpeg需要请求Windows打开您的视频文件
  • Windows为ffmpeg提供文件句柄
  • ffmpeg需要请求Windows提供输出数据的流
  • Windows为ffmpeg提供发送数据的流
  • ffmpeg最终可以执行其工作并将数据发送到流
  • ffmpeg需要告诉Windows关闭视频文件,因为它已经用完了
  • Windows关闭文件
  • ffmpeg需要告诉Windows关闭它打开的流,因为不再需要
  • Windows关闭流
  • ffmpeg需要告诉Windows它已经完成并可以关闭
  • Windows可以释放ffmpeg进程

这是相当多的步骤,对吧?

问题在于,在所有这些步骤中,只有一个步骤对每个帧都是唯一的,那就是“ffmpeg最终可以执行其工作并将数据发送到流”。

其他所有步骤,需要大量时间,都会为每个帧重复执行。

因此,我的回答中,我们不是为每个帧打开一个流,而是为所有帧打开一个单独的流,并实时渲染它们。这避免了为每个帧创建新的进程实例,也避免了为每个帧从开始解码整个视频流。

最终解决方案

英文:

TLDR: The very low framerate is due to the enormous delay there is between starting the ffmpeg process and the actual data starting to be sent by 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

Measuring performances

Using Stopwatches, I have measured the time it takes for each steps of your code.

Note: I have changed the format from bmp to png for smaller individual frame sizes

Code

Here's my measuments code:

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

Stopwatch sw = new Stopwatch();
private void pic()
{
    using (Process process = new Process())
    {
        process.StartInfo.FileName = &quot;ffmpeg.exe&quot;;
        process.StartInfo.Arguments = $&quot;-i \&quot;video.mp4\&quot; -vf \&quot;select=gte(n\\,{master_frame})\&quot; -vframes 1 -q:v 2 -f image2pipe -c:v png -&quot;;
        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(&quot;-&quot;);

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

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

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

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

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

Measurements

And here are my results, for a 480p 30fps video that is 1 minute and 36 seconds long, on my computer, I have on average:

  • Reading the bytes sent by ffmpeg: 110ms, with peaks to around 300ms
  • Disposing of the previous frame: &lt;1ms
  • Converting the stream to a bitmap: 1ms
  • Assigning the converted bitmap to the PictureBox: &lt;1ms
  • Rendering the PictureBox to the screen: 2ms

As a reminder, to achieve 60 FPS you'd need to render each frame in 16.67ms, or 33.33msfor 30 FPS.

What can we understand from that

It is quite blatant that the culprit here is the time it takes to read the stream that ffmpeg sends.

By the way, I have also tried @Charlieface's suggestion, and no, using process.StandardOutput.BaseStream.CopyTo(outputStream); doesn't save much time.

In fact, you could just use new Bitmap(process.StandardOutput.BaseStream); directly, but it doesn't make the whole thing that must faster.

The real issue

At first, it might seem obvious to blame ffmpeg for being slow to convert the frame we asked for in the desired format.

And as pointed out by @ChristophRackwitz, it is indeed a factor of your code's slowness.

When asking ffmpeg for a specific frame, it actually needs to decode the entire video stream up to that frame, resulting in more and more time being taken for each frame as time goes on.

This wasn't an issue on my side though since I have used a video with a very low quality and framerate. My measurements are therefore minimalistic, and in practice, it could take much longer to render each frame than what I have measured. I'll let you imagine the time it'd take to get the last frame of a 1 hour long 4K 144fps video.

There is another factor as well, and it's about process creation.

Here's the thing, when you start a new ffmpeg process instance, here's what happens:

  • Windows needs to allocate space to boot a new process
  • ffmpeg needs to ask Windows to open your video file
  • Windows gives a file handle to ffmpeg
  • ffmpeg needs to ask Windows for a stream to output its data when it'll be done converting
  • Windows gives ffmpeg a stream to send its data to
  • ffmpeg can finally do its job and send it to the stream
  • ffmpeg needs to tell Windows to close the video file since it's done with it
  • Windows closes the file
  • ffmpeg needs to tell Windows to close the stream it opened since it no longer needs it
  • Windows closes the stream
  • ffmpeg needs to tell Windows that it has finished and can close
  • Windows can free the ffmpeg process

That's quite a lot, right?

The issue here is that out of all these steps, only one of them is unique to every frame, and that is the "ffmpeg can finally do its job and send it to the stream" one.

All of the other steps, which takes a massive amount of time, are repeated for each frame.

Hence my answer to your other post, in which instead of opening a stream for each frame, we open a single stream for all of them and render them in real time. This avoids creating a new process instance for each frame, and also avoids having to decode the entire video stream from the beginning for each frame.

The end solution

You can't improve the speed of your code with the way it currently works, you need to switch from one process per frame to one process for each of them, which is what my answer to your other post does.

Thanks for reading.

huangapple
  • 本文由 发表于 2023年6月29日 06:25:24
  • 转载请务必保留本文链接:https://go.coder-hub.com/76577060.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定