Data Science

Если вы когда-либо задумывались о создании видео, содержащего компьютерную анимацию, эта статья для вас. Я предполагаю, что у вас уже есть код, или вы можете написать код, создающий некое изображение в виде двумерного массива пикселей. Неважно, что это за изображение, если оно помещается в массив в памяти.

Если вы сможете добраться до этой точки, в этой статье я проведу вас по оставшемуся пути. Шаг за шагом вы научитесь создавать видеофайлы в формате MP4. В итоге вы получите файл, подходящий для загрузки на YouTube, например, как этот:

План

Вот перечень шагов для создания такого видео: 

  • Написать код на C++, производящий изображение в массиве в памяти. Я использую простой генератор изображений множества Мандельброта, но, повторюсь, это может быть что угодно. 
  • Использовать LodePNG с открытым кодом для сохранения изображения на диск в формате PNG. 
  • Код повторяет первые два шага сотни раз, создавая по одному кадру за раз. Каждый PNG-файл содержит один неподвижный кадр видеоклипа. 
  • На Windows или Linux конвертируем серию PNG-изображений в MP4-видео, используя ffmpeg.

Изображения множества Мандельброта

Множество Мандельброта — один из самых знаменитых фрактальных объектов. Видео выше постепенно увеличивает приближение множества Мандельброта с 1 до 100 миллионов раз. Видео занимает 30 секунд, скорость воспроизведения 30 кадров в секунду, в общей сложности 900 быстро воспроизводимых изображений. Замечательно, что вся эта красота возникает при повторении маленькой формулы: 

Функция Mandelbrot в исходном файле mandelzoom.cpp повторяет эту формулу до тех пор, пока либо комплексное значение z не выйдет за пределы круга радиусом в 2 раза больше исходного, либо n (счетчик повторений) не достигнет максимального предела. Итоговое значение n определяет цвет определенного пикселя на экране. 

static int Mandelbrot(double cr, double ci, int limit)
{
    int count = 0;
    double zr = 0.0;
    double zi = 0.0;
    double zr2 = 0.0;
    double zi2 = 0.0;
    while ((count < limit) && (zr2 + zi2 < 4.001))
    {
        double tzi = 2.0*zr*zi + ci;
        zr = zr2 - zi2 + cr;
        zi = tzi;
        zr2 = zr*zr;
        zi2 = zi*zi;
        ++count;
    }
    return count;
}

Взгляните на функцию Palette, чтобы увидеть, как счетчик повторений конвертируется в значения красного, зеленого и синего цветов. 

Создание изображения в памяти 

Функция GenerateZoomFrames производит серию PNG-файлов. Каждый PNG-файл содержит изображение множества Мандельброта с другим увеличением. Разрешение изображений —  1280 на 720, стандартное для HD-видео.

static int GenerateZoomFrames(const char *outdir, int numframes, double xcenter, double ycenter, double zoom)
{
    try
    {
        // Создаем буфер видеокадров с разрешением 720p (1280x720).
        const int width  = 1280;
        const int height =  720;
        VideoFrame frame(width, height);

        const int limit = 16000;
        double multiplier = pow(zoom, 1.0 / (numframes - 1.0));
        double denom = 1.0;

        for (int f = 0; f < numframes; ++f)
        {
            // Рассчитываем действительный и мнимый диапазон 
            // значений для каждого кадра. 
            // Приближение экспоненциально.
            // В первом кадре масштаб таков, что меньший габарит
            // (высота) занимает 4 единицы от нижней части кадра 
            // до верхней. 
            // В последнем кадре масштаб равен количеству единиц,
            // разделенному на 'приближение'. 
            double ver_span = 4.0 / denom;
            double hor_span = ver_span * (width - 1.0) / (height - 1.0);
            double ci_top = ycenter + ver_span/2.0;
            double ci_delta = ver_span / (height - 1.0);
            double cr_left = xcenter - hor_span/2.0;
            double cr_delta = hor_span / (width - 1.0);

            for (int x=0; x < width; ++x)
            {
                double cr = cr_left + x*cr_delta;
                for (int y=0; y < height; ++y)
                {
                    double ci = ci_top - y*ci_delta;
                    int count = Mandelbrot(cr, ci, limit);
                    PixelColor color = Palette(count, limit);
                    frame.SetPixel(x, y, color);
                }
            }

            // Создаем PNG-файл для вывода, имя в формате 
            // "outdir/frame_12345.png".
            char number[20];
            snprintf(number, sizeof(number), "%05d", f);
            std::string filename = std::string(outdir) + "/frame_" + number + ".png";

            // Сохраняем видеокадр как PNG-файл.
            int error = frame.SavePng(filename.c_str());
            if (error)
                return error;

            printf("Wrote %s\n", filename.c_str());

            // Увеличиваем приближение для следующего кадра. 
            denom *= multiplier;
        }
        return 0;
    }
    catch (const char *message)
    {
        fprintf(stderr, "EXCEPTION: %s\n", message);
        return 1;
    }
}

Сохранение изображения в PNG-файл

Класс VideoFrame может быть полезен для приложения генератора видео. Он отображает один кадр и знает, как сохранить его в PNG-файл. Функция GenerateZoomFrames использует VideoFrame для создания каждого кадра видео фрактального приближения. 

class VideoFrame
{
private:
    int width;
    int height;
    std::vector<unsigned char> buffer;

public:
    VideoFrame(int _width, int _height)
        : width(_width)
        , height(_height)
        , buffer(4 * _width * _height, 255)
        {}

    void SetPixel(int x, int y, PixelColor color)
    {
        int index = 4 * (y*width + x);
        buffer[index]   = color.red;
        buffer[index+1] = color.green;
        buffer[index+2] = color.blue;
        buffer[index+3] = color.alpha;
    }

    int SavePng(const char *outFileName)
    {
        unsigned error = lodepng::encode(outFileName, buffer, width, height);
        if (error)
        {
            fprintf(stderr, "ERROR: lodepng::encode returned %u\n", error);
            return 1;
        }
        return 0;
    }
};

Функция-член VideoFrame::SetPixel должна вызываться приложением для каждого пикселя каждого кадра. Вы передаете структуру PixelColor, которая определяет красный, зеленый и синий компоненты пикселя. Эти значение — целые числа в диапазоне от 0 до 255.

PixelColor также содержит альфа-значение в диапазоне от 0 до 255, отображающее прозрачность пикселя. Для видео приложений следует всегда устанавливать это значение 255, что означает, что пиксель полностью непрозрачен. Я включил альфа-значение для универсальности, в случае, если вам захочется сгенерировать PNG-файлы с прозрачными областями.

Использование ffmpeg для создания видеоклипа

Я включил баш-скрипт run, автоматизирующий весь процесс создания программы mandelzoom из исходного кода, ее запуск и конвертирование полученных 900 PNG-файлов в файл видеоклипа с именем zoom.mp4. Последний шаг выполняется запуском программы ffmpeg. Вот скрипт run. он содержит несколько полезных комментариев с пояснениями к аргументам командной строки для ffmpeg.

#!/bin/bash
Fail()
{
    echo "FATAL($0): $1"
    exit 1
}

# Компилируем исходный код для mandelzoom. Оптимизируем для 
# скорости.
echo "Building C++ code..."
g++ -Wall -Werror -Ofast -o mandelzoom mandelzoom.cpp lodepng.cpp || Fail "Error building C++ code."

# Уничтожаем папку "movie" и ее содержимое, если она существует. 
rm -rf movie || Fail "Error deleting movie directory."

# Создаем новую пустую папку "movie" для хранения файлов вывода. 
mkdir movie || Fail "Error creating movie directory."

# Запускаем генератор mandelbrot. Он создает все PNG-файлы вывода.
./mandelzoom movie $((30*30)) -0.74498410019 -0.13523854817 1.0e8 ||
    Fail "Error running Mandelbrot Zoom program."

# Конвертируем PNG-файлы в видеофайл zoom.mp4.
# Пояснение к аргументам командной строки ffmpeg:
# "-r 30" означает 30 кадров в секунду.
# "-f image2" : конвертирует серию кадров в видео.
# "-s 1280x720" указывает размеры итогового видео в пикселях. 
# "-i movie/frame_%05d.png" задает имена png-файлов, которые будут 
# использоваться в качестве входных данных.
# "-vcodec libx264": библиотека кодеков, источник: 
# https://www.videolan.org/developers/x264.html
# "-crf 15" : коэффициент постоянной скорости, определяющий степень # сжатия с потерями. https://trac.ffmpeg.org/wiki/Encode/H.264
# "-pix_fmt yuv420p" определяет, как цвета кодируются в mp4-файле.  
# https://en.wikipedia.org/wiki/YUV
# "zoom.mp4" - имя итогового файла.
ffmpeg -r 30 -f image2 -s 1280x720 -i movie/frame_%05d.png -vcodec libx264 -crf 15 -pix_fmt yuv420p zoom.mp4 ||
    Fail "Error in ffmpeg."

echo "Created movie zoom.mp4"
exit 0

И это все! Репозиторий mandelzoom с исходным кодом можно найти здесь

Еще один пример

Я упоминал, что этот метод можно использовать для создания любого видео, при условии, что вы сможете написать код, создающий каждый кадр. Я закончу статью другим моим видео. Оно основано на простой трассировке лучей, которую я создал для моей книги Основы трассировки лучей. Код в этом видео работает так же, как и код выше: он создает серии PNG-файлов, которые затем ffmpeg конвертирует в MP4-файл.

Читайте также:


Перевод статьи Don Cross: Create a YouTube Video from Code

Предыдущая статьяИнтерфейсы против реализаций
Следующая статьяНа собеседование в Google через челлендж Python #1