基于视频生成的新帧率计算帧索引

Calculating frame index based on new frame rate for video generation

我的 .NET 应用程序作为图像的顺序列表,表示以每秒 30 帧的速度录制的视频的每一帧。

00000001.png
00000002.png
00000003.png
...
99999999.png

现在我想重新排序此列表,以便它可以根据以下参数生成视频:

Start Frame Index: 100
Direction:         Forward
Output Speed:      100 FPS
Duration:          10 seconds

到目前为止我有这样的东西:

var originalFrameRate = 30D;
var originalFrameTime = 1D / originalFrameRate;
var originalStartFrameIndex = 100; // 00000100.png.
// Assume [originalFrames] will be filled with image file names from above.
var originalFrames = new List<string>
(new string [] { "0000003.png", "0000002.png", ..., "99999999.png", });

var targetFrameRate 100; // FPS.
var targetDuration = TimeSpan.FromSeconds(10);
var targetFrameCount = speed * targetDuration.Seconds;
var targetFrames = new List<string>();

for (int i = 0; i < targetFrameCount; i++)
{
    // How to map the original list from 30 FPS to 100 FPS?
    targetFrames.Add(originalFrames [originalStartFrameIndex + ???]);
}

在上面的示例中,输出将是根据变量名称 targetXXX.

填充了适当文件名的 targetFrames

如有任何关于如何映射的建议,我们将不胜感激。

编辑: 我忘了说输出视频将始终以原始帧速率生成。目标视频的长度当然会发生变化。如果原始 FPS 低于目标,我们将重复帧。否则我们将跳过它们。

targetFrames.Add(originalFrames [originalStartFrameIndex + (int)(i * targetFrameRate / originalFrameRate) ]

应该可以解决问题。添加一些错误验证(检查除以零和超出数组边界):)

我开始扩展 Vincent 的回答以解决我注意到的一个问题:当从 30fps 缩放到 100fps 时,第 0 帧重复第四次(0000 111 222 3333 的帧模式),而我期待 000 111 2222。没什么大不了的,因为这可能只是一个偏好问题(无论你想让小数 "adjustment" 在偶数帧还是奇数帧上发生),但后来我钻进了兔子洞并构建了一个迭代器 class几乎可以处理任何情况,包括分数帧率。

(使用通用迭代器的额外好处是不需要帧为 string - 如果您想将每个帧表示为 class,您也可以这样做。)

public sealed class FramerateScaler<T> : IEnumerable<T>
{
    private IEnumerable<T> _source;
    private readonly double _inputRate;
    private readonly double _outputRate;
    private readonly int _startIndex;

    public double InputRate { get { return _inputRate; } }
    public double OutputRate { get { return _outputRate; } }
    public int StartIndex { get { return _startIndex; } }

    public TimeSpan InputDuration {
        get { return TimeSpan.FromSeconds((1 / _inputRate) * (_source.Count() - StartIndex)); }
    }

    public TimeSpan OutputDuration {
        get { return TimeSpan.FromSeconds((1 / _outputRate) * this.Count()); }
    }

    public FramerateScaler(
        double inputRate, double outputRate, 
        IEnumerable<T> source, int startIndex = 0)
    {
        _source = source;
        _inputRate = inputRate;
        _outputRate = outputRate;
        _startIndex = startIndex;
    }

    public IEnumerator<T> GetEnumerator()
    {
        return new ScalingFrameEnumerator<T>(_inputRate, _outputRate, _source, _startIndex);
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return (IEnumerator)GetEnumerator();
    }

    private sealed class ScalingFrameEnumerator<T> : IEnumerator<T>
    {
        internal readonly double _inputRate;
        internal readonly double _outputRate;
        internal readonly int _startIndex;

        private readonly List<T> _source;

        private readonly double _rateScaleFactor;
        private readonly int _totalOutputFrames;
        private int _currentOutputFrame = 0;

        public ScalingFrameEnumerator(
            double inputRate, double outputRate, 
            IEnumerable<T> source, int startIndex)
        {
            _inputRate = inputRate;
            _outputRate = outputRate;
            _source = source.ToList();
            _startIndex = startIndex;

            _rateScaleFactor = _outputRate / _inputRate;
            // Calculate total output frames from input duration
            _totalOutputFrames = (int)Math.Round(
                (_source.Count - startIndex) * _rateScaleFactor, 0);
        }

        public T Current
        {
            get
            {
                return _source[_startIndex + 
                    (int)Math.Ceiling(_currentOutputFrame / _rateScaleFactor) - 1];
            }
        }

        public void Dispose()
        {
            // Nothing unmanaged to dispose
        }

        object IEnumerator.Current
        {
            get { return Current; }
        }

        public bool MoveNext()
        {
            _currentOutputFrame++;
            return ((_currentOutputFrame - 1) < _totalOutputFrames);
        }

        public void Reset()
        {
            _currentOutputFrame = 0;
        }
    }
}

以及一组涵盖幂等性、放大、缩小和分数帧率的测试:

[TestClass]
public class Test
{
    private readonly List<string> _originalFrames = new List<string>();

    public Test()
    {
        // 30 FPS for 10 seconds
        for (int f = 0; f < 300; f++)
        {
            _originalFrames.Add(string.Format("{0:0000000}.png", f));
        }
    }

    [TestMethod]
    public void Should_set_default_values()
    {
        var scaler = new FramerateScaler<string>(30, 30, _originalFrames, 10);

        Assert.AreEqual(30, scaler.InputRate);
        Assert.AreEqual(30, scaler.OutputRate);
        Assert.AreEqual(10, scaler.StartIndex);
        Assert.AreEqual(_originalFrames.ElementAt(10), scaler.First());
    }

    [TestMethod]
    public void Scale_from_same_is_idempotent()
    {
        var scaler = new FramerateScaler<string>(30, 30, _originalFrames);

        Assert.AreEqual(scaler.InputDuration, scaler.OutputDuration);
        Assert.AreEqual(_originalFrames.Count, scaler.Count());
        Assert.IsTrue(_originalFrames.SequenceEqual(scaler));
    }

    [TestMethod]
    public void Scale_from_same_offset_by_half_is_idempotent()
    {
        var scaler = new FramerateScaler<string>(
            30, 30, _originalFrames, _originalFrames.Count / 2);

        Assert.AreEqual(150, scaler.Count());
        Assert.AreEqual(scaler.OutputDuration, scaler.InputDuration);
        Assert.IsTrue(_originalFrames
            .Skip(150)
            .SequenceEqual(scaler));
    }

    [TestMethod]
    public void Scale_from_30_to_60()
    {
        var scaler = new FramerateScaler<string>(30, 60, _originalFrames);

        Assert.AreEqual(600, scaler.Count());
        Assert.AreEqual(scaler.OutputDuration, scaler.InputDuration);
        var result = scaler.ToList();
        Assert.IsTrue(_originalFrames
            .Concat(_originalFrames)
            .OrderBy(x => x)
            .SequenceEqual(scaler));
    }

    [TestMethod]
    public void Scale_from_30_to_60_offset_by_half()
    {
        var scaler = new FramerateScaler<string>(
            30, 60, _originalFrames, _originalFrames.Count / 2);

        Assert.AreEqual(300, scaler.Count());
        Assert.AreEqual(scaler.OutputDuration, scaler.InputDuration);
        Assert.IsTrue(_originalFrames
            .Skip(150)
            .Concat(_originalFrames.Skip(150))
            .OrderBy(x => x)
            .SequenceEqual(scaler));
    }

    [TestMethod]
    public void Scale_from_30_to_100()
    {
        var scaler = new FramerateScaler<string>(30, 100, _originalFrames);

        Assert.AreEqual(1000, scaler.Count());
        Assert.AreEqual(scaler.OutputDuration, scaler.InputDuration);
        // 000 - 111 - 2222 ...
        Assert.IsTrue(scaler.PatternIs(0, 0, 0, 1, 1, 1, 2, 2, 2, 2));
    }

    [TestMethod]
    public void Scale_from_30_to_100_offset_by_half()
    {
        var scaler = new FramerateScaler<string>(
            30, 100, _originalFrames, _originalFrames.Count / 2);

        Assert.AreEqual(500, scaler.Count());
        Assert.AreEqual(scaler.OutputDuration, scaler.InputDuration);
        // 000 - 111 - 2222 ...
        Assert.IsTrue(scaler.PatternIs(0, 0, 0, 1, 1, 1, 2, 2, 2, 2));
    }

    [TestMethod]
    public void Scale_from_24p_to_ntsc()
    {
        var scaler = new FramerateScaler<string>(23.967, 29.97, _originalFrames);

        Assert.AreEqual(375, scaler.Count());
        Assert.AreEqual(
            scaler.OutputDuration.TotalMilliseconds, 
            scaler.InputDuration.TotalMilliseconds, delta: 4);
        // 0 - 1 - 2 - 33 ...
        Assert.IsTrue(scaler.PatternIs(0, 1, 2, 3, 3));
    }

    [TestMethod]
    public void Scale_from_30_to_15()
    {
        var scaler = new FramerateScaler<string>(30, 15, _originalFrames);

        Assert.AreEqual(150, scaler.Count());
        Assert.AreEqual(scaler.OutputDuration, scaler.InputDuration);
        Assert.IsTrue(_originalFrames
            .Where((item, index) => index % 2 == 1)
            .SequenceEqual(scaler));
    }

    [TestMethod]
    public void Scale_from_30_to_15_offset_by_half()
    {
        var scaler = new FramerateScaler<string>(30, 15, _originalFrames, 150);

        Assert.AreEqual(75, scaler.Count());
        Assert.AreEqual(scaler.OutputDuration, scaler.InputDuration);
        Assert.IsTrue(_originalFrames
            .Skip(150)
            .Where((item, index) => index % 2 == 1)
            .SequenceEqual(scaler));
    }
}

static class Extensions
{
    public static bool PatternIs<T>(this IEnumerable<T> source, params int[] pattern)
    {
        foreach (var chunk in source.Chunkify(pattern.Length))
        {
            for (var i = 0; i < chunk.Length; i++)
                if (!chunk.ElementAt(i).Equals(
                    chunk.Distinct().ElementAt(pattern[i])))
                    return false;
        }

        return true;
    }

    // 
    public static IEnumerable<T[]> Chunkify<T>(this IEnumerable<T> source, int size)
    {
        if (source == null) throw new ArgumentNullException("source");
        if (size < 1) throw new ArgumentOutOfRangeException("size");
        using (var iter = source.GetEnumerator())
        {
            while (iter.MoveNext())
            {
                var chunk = new T[size];
                chunk[0] = iter.Current;
                for (int i = 1; i < size && iter.MoveNext(); i++)
                {
                    chunk[i] = iter.Current;
                }
                yield return chunk;
            }
        }
    }
}