图像序列转视频流?

69

像许多其他人一样(在这个主题上有几个线程),我正在寻找将一系列图像转换为视频的方法。

我想要用C#实现我的功能。

这是我想做的事情:

/*Pseudo code*/
void CreateVideo(List<Image> imageSequence, long durationOfEachImageMs, string outputVideoFileName, string outputFormat)
{
    // Info: imageSequence.Count will be > 30 000 images
    // Info: durationOfEachImageMs will be < 300 ms

    if (outputFormat = "mpeg")
    {
    }
    else if (outputFormat = "avi")
    {      
    }
    else
    {
    }

    //Save video file do disk
}

我知道有一个名为Splicer的项目(http://splicer.codeplex.com/),但我找不到合适的文档或清晰的示例可以跟随(这些是我找到的示例)。

最接近我想要做的,我在 CodePlex 上发现了这个: How can I create a video from a directory of images in C#?

我也阅读了一些关于ffmpeg的帖子(例如这个:C# and FFmpeg preferably without shell commands?以及这个:convert image sequence using ffmpeg),但我找不到任何人能够帮助我解决我的问题,并且我认为ffmpeg命令行方式不是我最好的解决方案(因为图片数量很多)。

我相信我可以以某种方式使用Splicer项目(?)。

在我的情况下,大约有30,000张图片,每张图片应该在大约200毫秒内显示(在我要创建的视频流中)。

(视频是关于什么的?植物生长...)

有人可以帮我完成我的函数吗?


有一个很好的第三方工具包叫做leadtools,可能会对您有所帮助。这个工具包可以让您从一系列图像中生成视频文件。详情请参见此链接:http://support.leadtools.com/CS/forums/16880/ShowPost.aspx - user1659908
1
没有免费的LeadTools。有没有带完整源代码的最终解决方案? - Kiquenet
你需要在什么方面寻求@Kiquenet的帮助? - Hauns TM
顺便说一句,不要使用字符串来表示图像格式,应该使用枚举。这就是它们的用途... - Basic
7个回答

71

好的,这个答案有点晚了,但我最近注意到我的原始问题有一些活动(并且没有提供有效的解决方案),所以我想给你最终对我有用的东西。

我将我的答案分为三个部分:

  • 背景
  • 问题
  • 解决方案

背景

(此部分对解决方案不重要)

我的原始问题是我有很多图片(即巨量),这些图片被单独存储在数据库中,以字节数组的形式存在。我想用所有这些图片制作一个视频序列。

我的设备设置大致如下图所示: enter image description here

这些图片描绘了处于不同状态下的生长中的番茄植物。所有图片都是在白天每隔1分钟拍摄的。

/*pseudo code for taking and storing images*/
while (true)
{
    if (daylight)
    {
        //get an image from the camera
        //store the image as byte array to db
    }
    //wait 1 min
}

我有一个非常简单的数据库来存储图像,里面只有一个表(ImageSet表): 在这里输入图片描述

问题

我已经阅读了许多关于ffmpeg的文章(请参见我的原始问题),但我找不到任何关于如何从一组图像转换为视频的文章。


解决方案

最终,我找到了一个可行的解决方案!其中主要部分来自于开源项目AForge.NET。简单来说,你可以说AForge.NET是一个用C#编写的计算机视觉和人工智能库。(如果你想要这个框架的副本,只需从http://www.aforgenet.com/获取即可)

在AForge.NET中,有一个名为VideoFileWriter类(使用ffmpeg编写视频文件的类)。这几乎完成了所有的工作。(这里还有一个非常好的例子here

这是我用来从我的图像数据库中获取和转换图像数据为视频的最终类(精简版):

public class MovieMaker
{

    public void Start()
    {
        var startDate = DateTime.Parse("12 Mar 2012");
        var endDate = DateTime.Parse("13 Aug 2012");

        CreateMovie(startDate, endDate);
    }    
    

    /*THIS CODE BLOCK IS COPIED*/

    public Bitmap ToBitmap(byte[] byteArrayIn)
    {
        var ms = new System.IO.MemoryStream(byteArrayIn);
        var returnImage = System.Drawing.Image.FromStream(ms);
        var bitmap = new System.Drawing.Bitmap(returnImage);

        return bitmap;
    }

    public Bitmap ReduceBitmap(Bitmap original, int reducedWidth, int reducedHeight)
    {
        var reduced = new Bitmap(reducedWidth, reducedHeight);
        using (var dc = Graphics.FromImage(reduced))
        {
            // you might want to change properties like
            dc.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
            dc.DrawImage(original, new Rectangle(0, 0, reducedWidth, reducedHeight), new Rectangle(0, 0, original.Width, original.Height), GraphicsUnit.Pixel);
        }

        return reduced;
    }

    /*END OF COPIED CODE BLOCK*/


    private void CreateMovie(DateTime startDate, DateTime endDate)
    {
        int width = 320;
        int height = 240;
        var framRate = 200;

        using (var container = new ImageEntitiesContainer())
        {
            //a LINQ-query for getting the desired images
            var query = from d in container.ImageSet
                        where d.Date >= startDate && d.Date <= endDate
                        select d;

            // create instance of video writer
            using (var vFWriter = new VideoFileWriter())
            {
                // create new video file
                vFWriter.Open("nameOfMyVideoFile.avi", width, height, framRate, VideoCodec.Raw);

                var imageEntities = query.ToList();

                //loop throught all images in the collection
                foreach (var imageEntity in imageEntities)
                {
                    //what's the current image data?
                    var imageByteArray = imageEntity.Data;
                    var bmp = ToBitmap(imageByteArray);
                    var bmpReduced = ReduceBitmap(bmp, width, height);

                    vFWriter.WriteVideoFrame(bmpReduced);
                }
                vFWriter.Close();
            }
        }

    }
}

更新于2013年11月29日(如何)(希望这是您所要求的,@Kiquenet?)

  1. 下载页面下载AForge.NET Framework(下载完整的ZIP压缩包,您将在AForge.NET Framework-2.2.5\Samples文件夹中找到许多有趣的Visual Studio解决方案和项目,例如视频...)
  2. 命名空间:AForge.Video.FFMPEG(来自文档
  3. 程序集:AForge.Video.FFMPEG(在AForge.Video.FFMPEG.dll中)(来自文档)(您可以在AForge.NET Framework-2.2.5\Release文件夹中找到此AForge.Video.FFMPEG.dll

如果你想创建自己的解决方案,请确保在项目中引用了AForge.Video.FFMPEG.dll。然后,使用VideoFileWriter类应该很容易。如果您按照link到类的链接,您将找到一个非常好(而简单)的示例。在代码中,他们使用for循环将Bitmap image提供给VideoFileWriter。



5
如果有人感兴趣,可以在 Youtube 上找到最终的视频,其中包括“音效”。请将此评论仅视为解决方案的演示。:o) - Hauns TM
1
为什么你昨天没有回答呢?我只是需要它 :) 但还是非常感谢你。 - Ivan Kochurkin
1
说实话,我还没有尝试过那个。当我创建我的Youtube视频剪辑(见上面的链接)时,我使用了一个朋友的视频编辑软件+我自己制作的avi文件和一些不同的音频剪辑文件。如果您没有其他视频编辑软件,我认为您可以使用[VideoLAN Movie Creator-VLMC] (http://trac.videolan.org/vlmc/)。请告诉我它是否有效。 :-) - Hauns TM
有没有完整的源代码示例?使用命名空间吗?引用了哪些库? - Kiquenet
1
非常感谢你的答案!只是一个提示:如果从 AForge.Video.FFMPEG.dll DLL 中出现未找到引用的运行时错误,你需要将 C:\Program Files (x86)\AForge.NET\Framework\Externals\ffmpeg\bin 文件夹中的所有 DLL 复制到输出目录。 - Jack
显示剩余9条评论

11

我在切片机的示例中找到了这段代码,它看起来非常接近您想要的:

string outputFile = "FadeBetweenImages.wmv";
using (ITimeline timeline = new DefaultTimeline())
{
    IGroup group = timeline.AddVideoGroup(32, 160, 100);
    ITrack videoTrack = group.AddTrack();
    IClip clip1 = videoTrack.AddImage("image1.jpg", 0, 2); // play first image for a little while
    IClip clip2 = videoTrack.AddImage("image2.jpg", 0, 2); // and the next
    IClip clip3 = videoTrack.AddImage("image3.jpg", 0, 2); // and finally the last
    IClip clip4 = videoTrack.AddImage("image4.jpg", 0, 2); // and finally the last
}

  double halfDuration = 0.5;

  // fade out and back in
  group.AddTransition(clip2.Offset - halfDuration, halfDuration, StandardTransitions.CreateFade(), true);
  group.AddTransition(clip2.Offset, halfDuration, StandardTransitions.CreateFade(), false);

  // again
  group.AddTransition(clip3.Offset - halfDuration, halfDuration, StandardTransitions.CreateFade(), true);
  group.AddTransition(clip3.Offset, halfDuration, StandardTransitions.CreateFade(), false);

  // and again
  group.AddTransition(clip4.Offset - halfDuration, halfDuration, StandardTransitions.CreateFade(), true);
  group.AddTransition(clip4.Offset, halfDuration, StandardTransitions.CreateFade(), false);

  // add some audio
  ITrack audioTrack = timeline.AddAudioGroup().AddTrack();

  IClip audio =
     audioTrack.AddAudio("testinput.wav", 0, videoTrack.Duration);

  // create an audio envelope effect, this will:
  // fade the audio from 0% to 100% in 1 second.
  // play at full volume until 1 second before the end of the track
  // fade back out to 0% volume
  audioTrack.AddEffect(0, audio.Duration,
                 StandardEffects.CreateAudioEnvelope(1.0, 1.0, 1.0, audio.Duration));

  // render our slideshow out to a windows media file
  using (
     IRenderer renderer =
        new WindowsMediaRenderer(timeline, outputFile, WindowsMediaProfiles.HighQualityVideo))
  {
     renderer.Render();
  }
}

非常感谢!我已经看过了,但有一件事我无法理解。我有一个重要的前提条件,即每个图像的持续时间durationOfEachImageMs < 300毫秒。根据上面的示例,我应该在哪里设置这个非常短的时间? - Hauns TM
AddImage函数的最后一个参数是double类型的,你试过用0.3而不是2吗? - Adam
嗯,那可能行得通,但现在我遇到了另一个问题。 “COMException was caught”详情: `System.Runtime.InteropServices.COMException was caught Message=Access is denied. Source=DirectShowLib-2005 ErrorCode=-2147024891 StackTrace: at DirectShowLib.DES.DESError.ThrowExceptionForHR(Int32 hr) at Splicer.Renderer.AbstractRenderer.StartRender() at Splicer.Renderer.AbstractRenderer.BeginRender(AsyncCallback callback, Object state) at Splicer.Renderer.AbstractRenderer.Render()`你有任何想法该如何解决吗? - Hauns TM
你确定Splicer框架可以支持创建3万个IClips吗? - Daniel Mošmondor
Windows Media Player是先决条件。需要在安装之前安装它。 - Kiquenet

11

我无法让上述示例正常工作。不过,我找到了另一个库,它曾经表现出色。尝试通过NuGet下载"accord.extensions.imaging.io",然后我编写了以下小函数:

    private void makeAvi(string imageInputfolderName, string outVideoFileName, float fps = 12.0f, string imgSearchPattern = "*.png")
    {   // reads all images in folder 
        VideoWriter w = new VideoWriter(outVideoFileName, 
            new Accord.Extensions.Size(480, 640), fps, true);
        Accord.Extensions.Imaging.ImageDirectoryReader ir = 
            new ImageDirectoryReader(imageInputfolderName, imgSearchPattern);
        while (ir.Position < ir.Length)
        {
            IImage i = ir.Read();
            w.Write(i);
        }
        w.Close();
    }

它从文件夹中读取所有图像并将它们组合成视频。

如果你想要让它更好看一点,你可能可以读取图像的尺寸而不是硬编码,但你明白了重点。


4
图书馆现在被称为DotImaging。 - dajuric
@dajuric,是否有选项可以使用上述库将音频(文本转语音)并行集成到视频中? - Pranay
@Pranay 由于该库依赖于没有此类选项的OpenCV视频API,因此答案是否定的。但是,您可以生成一个视频,然后调用ffmpeg将视频与音频交错。请注意,如果您想在构建视频时流式传输视频,则需要将其切成块,然后使用DASH进行流式传输。 - dajuric
3
我已经通过NuGet安装了Accord,但我无法使这段代码正常工作。它没有识别VideoWriter类。 - malt_man

8
FFMediaToolkit 是一个不错的解决方案,适用于2020年,并支持 .NET Core。 https://github.com/radek-k/FFMediaToolkit

FFMediaToolkit 是一个跨平台的 .NET Standard 库,用于创建和读取视频文件。它使用由 FFmpeg.Autogen 绑定提供的本地 FFmpeg 库。

该库的 README 包含了一个关于问答的好例子。
// You can set there codec, bitrate, frame rate and many other options.
var settings = new VideoEncoderSettings(width: 1920, height: 1080, framerate: 30, codec: VideoCodec.H264);
settings.EncoderPreset = EncoderPreset.Fast;
settings.CRF = 17;
var file = MediaBuilder.CreateContainer(@"C:\videos\example.mp4").WithVideo(settings).Create();
while(file.Video.FramesCount < 300)
{
    file.Video.AddFrame(/*Your code*/);
}
file.Dispose(); // MediaOutput ("file" variable) must be disposed when encoding is completed. You can use `using() { }` block instead.

3
重要声明 - 本库不应用于生产环境。 - jjxtra

5

这是一个使用Visual Studio和C#从图像序列创建视频的解决方案。

我的起点是下面“Hauns TM”给出的答案,但我的要求比他们更基础,因此该解决方案可能更适用于不那么高级的用户(比如我自己)

库:

using System;
using System.IO;
using System.Drawing;
using Accord.Video.FFMPEG;

您可以通过在“工具 -> NuGet包管理器 -> 管理解决方案的NuGet包...”中搜索FFMPEG来获取FFMPEG库。
我传递给该函数的变量是:
输出文件名 = "C://outputFolder//outputMovie.avi" 输入图像序列 = ["C://inputFolder//image_001.avi", "C://inputFolder//image_002.avi", "C://inputFolder//image_003.avi", "C://inputFolder//image_004.avi"] 函数:
private void videoMaker( string outputFileName , string[] inputImageSequence)
{
  int width = 1920;
  int height = 1080;
  var framRate = 25;

  using (var vFWriter = new VideoFileWriter())
  {
    // create new video file
    vFWriter.Open(outputFileName, width, height, framRate, VideoCodec.Raw);

    foreach (var imageLocation in inputImageSequence)
    {
      Bitmap imageFrame = System.Drawing.Image.FromFile(imageLocation) as Bitmap;
      vFWriter.WriteVideoFrame(imageFrame);
    }
    vFWriter.Close();
  }
}

3
看起来许多这些答案在2020年已经过时了,所以我会补充我的想法。
我一直在解决同样的问题,并在GitHub上发布了.NET Core项目"Time Lapse Creator":https://github.com/pekspro/TimeLapseCreator 它展示了如何在额外帧上添加信息(例如时间戳),背景音频,标题屏幕,淡入淡出等等。然后使用ffmpeg进行渲染,这是在该函数中完成的:
// Render video from a list of images, add background audio and a thumbnail image.
private async Task RenderVideoAsync(int framesPerSecond, List<string> images, string ffmpgPath,
        string audioPath, string thumbnailImagePath, string outPath,
        double videoFadeInDuration = 0, double videoFadeOutDuration = 0,
        double audioFadeInDuration = 0, double audioFadeOutDuration = 0)
{
    string fileListName = Path.Combine(OutputPath, "framelist.txt");
    var fileListContent = images.Select(a => $"file '{a}'{Environment.NewLine}duration 1");

    await File.WriteAllLinesAsync(fileListName, fileListContent);

    TimeSpan vidLengthCalc = TimeSpan.FromSeconds(images.Count / ((double)framesPerSecond));
    int coverId = -1;
    int audioId = -1;
    int framesId = 0;
    int nextId = 1;

    StringBuilder inputParameters = new StringBuilder();
    StringBuilder outputParameters = new StringBuilder();

    inputParameters.Append($"-r {framesPerSecond} -f concat -safe 0 -i {fileListName} ");

    outputParameters.Append($"-map {framesId} ");

    if(videoFadeInDuration > 0 || videoFadeOutDuration > 0)
    {
        List<string> videoFilterList = new List<string>();
        if (videoFadeInDuration > 0)
        {
            //Assume we fade in from first second.
            videoFilterList.Add($"fade=in:start_time={0}s:duration={videoFadeInDuration.ToString("0", NumberFormatInfo.InvariantInfo)}s");
        }

        if (videoFadeOutDuration > 0)
        {
            //Assume we fade out to last second.
            videoFilterList.Add($"fade=out:start_time={(vidLengthCalc.TotalSeconds - videoFadeOutDuration).ToString("0.000", NumberFormatInfo.InvariantInfo)}s:duration={videoFadeOutDuration.ToString("0.000", NumberFormatInfo.InvariantInfo)}s");
        }

        string videoFilterString = string.Join(',', videoFilterList);

        outputParameters.Append($"-filter:v:{framesId} \"{videoFilterString}\" ");
    }

    if (thumbnailImagePath != null)
    {
        coverId = nextId;
        nextId++;

        inputParameters.Append($"-i {thumbnailImagePath} ");

        outputParameters.Append($"-map {coverId} ");
        outputParameters.Append($"-c:v:{coverId} copy -disposition:v:{coverId} attached_pic ");
    }

    if (audioPath != null)
    {
        audioId = nextId;
        nextId++;

        inputParameters.Append($"-i {audioPath} ");
        outputParameters.Append($"-map {audioId} ");

        if(audioFadeInDuration <= 0 && audioFadeOutDuration <= 0)
        {
            // If no audio fading, just copy as it is.
            outputParameters.Append($"-c:a copy ");
        }
        else
        {
            List<string> audioEffectList = new List<string>();
            if(audioFadeInDuration > 0)
            {
                //Assume we fade in from first second.
                audioEffectList.Add($"afade=in:start_time={0}s:duration={audioFadeInDuration.ToString("0", NumberFormatInfo.InvariantInfo)}s");
            }

            if (audioFadeOutDuration > 0)
            {
                //Assume we fade out to last second.
                audioEffectList.Add($"afade=out:start_time={(vidLengthCalc.TotalSeconds - audioFadeOutDuration).ToString("0.000", NumberFormatInfo.InvariantInfo)}s:duration={audioFadeOutDuration.ToString("0.000", NumberFormatInfo.InvariantInfo)}s");
            }

            string audioFilterString = string.Join(',', audioEffectList);

            outputParameters.Append($"-filter:a \"{audioFilterString}\" ");
        }
    }

    int milliseconds = vidLengthCalc.Milliseconds;
    int seconds = vidLengthCalc.Seconds;
    int minutes = vidLengthCalc.Minutes;
    var hours = (int)vidLengthCalc.TotalHours;

    string durationString = $"{hours:D}:{minutes:D2}:{seconds:D2}.{milliseconds:D3}";

    outputParameters.Append($"-c:v:{framesId} libx264 -pix_fmt yuv420p -to {durationString} {outPath} -y ");
        
    string parameters = inputParameters.ToString() + outputParameters.ToString();

    try
    {
        await Task.Factory.StartNew(() =>
        {
            var outputLog = new List<string>();

            using (var process = new Process
            {
                StartInfo =
                {
                FileName = ffmpgPath,
                Arguments = parameters,
                UseShellExecute = false,
                CreateNoWindow = true,
                // ffmpeg send everything to the error output, standard output is not used.
                RedirectStandardError = true
                },
                EnableRaisingEvents = true
            })
            {
                process.ErrorDataReceived += (sender, e) =>
                {
                    if (string.IsNullOrEmpty(e.Data))
                    {
                        return;
                    }

                    outputLog.Add(e.Data.ToString());
                    Console.WriteLine(e.Data.ToString());
                };

                process.Start();

                process.BeginErrorReadLine();

                process.WaitForExit();

                if (process.ExitCode != 0)
                {
                    throw new Exception($"ffmpeg failed error exit code {process.ExitCode}. Log: {string.Join(Environment.NewLine, outputLog)}");
                }
                Console.WriteLine($"Exit code: {process.ExitCode}");
            }
        });
    }
    catch(Win32Exception )
    {
        Console.WriteLine("Oh no, failed to start ffmpeg. Have you downloaded and copied ffmpeg.exe to the output folder?");
    }

    Console.WriteLine();
    Console.WriteLine("Video was successfully created. It is availible at: " + Path.GetFullPath(outPath));
}

1
这个函数基于Splicer.Net库。我花费了很长时间才理解该库的工作原理。 确保fps(每秒帧数)正确。顺便说一下,标准为24 f/s。
在我的情况下,我有15张图片,我现在知道我需要7秒钟的视频->所以fps=2。 fps可能会根据平台或开发者使用而有所不同。
public bool CreateVideo(List<Bitmap> bitmaps, string outputFile, double fps)
        {
            int width = 640;
            int height = 480;
            if (bitmaps == null || bitmaps.Count == 0) return false;
            try
            {
                using (ITimeline timeline = new DefaultTimeline(fps))
                {
                    IGroup group = timeline.AddVideoGroup(32, width, height);
                    ITrack videoTrack = group.AddTrack();

                    int i = 0;
                    double miniDuration = 1.0 / fps;
                    foreach (var bmp in bitmaps)
                    {
                        IClip clip = videoTrack.AddImage(bmp, 0, i * miniDuration, (i + 1) * miniDuration);
                        System.Diagnostics.Debug.WriteLine(++i);

                    }
                    timeline.AddAudioGroup();
                    IRenderer renderer = new WindowsMediaRenderer(timeline, outputFile, WindowsMediaProfiles.HighQualityVideo);
                    renderer.Render();
                }
            }
            catch { return false; }
            return true;
        }

希望这有所帮助。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接