Зона кода

А давайте немного попрограммируем...

Эффект появления изображения на холсте путём двухстороннего наплыва

C99Графические программы

Продолжаю развлекаться изготовлением роликов, демонстрирующих различные простые виды трансформации изображений. Прошлый раз был рассмотрен алгоритм заполнения холста группами хаотично разбросанных точек, которые, в итоге, образуют заранее заданное изображение. Подобный эффект мы будем рассматривать и сейчас, но принцип заполнения холста точками, из которых состоит изображение, будет другим.

Хорошо известен следующий эффект перехода между изображениями в слайд-шоу. Новая картинка начинает плавно "заезжать" на экран сбоку, например, двигаясь слева направо. Одновременно с этим, старая картинка, двигаясь в том же направлении, наоборот, "уезжает" с экрана. Картинки, при этом, плотно "прижаты" друг к другу боковыми сторонами. Таким образом, одно изображение плавно "выталкивает" в правую сторону другое. После того, как второе из них полностью исчезает с экрана, движение первого прекращается.

А теперь представим, что описанным выше образом, определённые ряды точек, из которых состоит новое изображение, выталкивают соответствующие ряды точек, из которых состоит старое. Ряды будем брать "через одного". Т. е. в выталкивании будут участвовать только ряды определённой чётности (если нумеровать их в направлении снизу вверх или сверху вниз).

А что будет происходить с рядами, не задействованными в этой операции? То же самое, но только двигаться они будут в противоположном направлении — справа налево. Движение рядов с обеих сторон будет синхронизировано. Так что выталкивание рядов одной чётности завершится одновременно с выталкиванием рядов противоположной.

Можно сказать, что друг навстречу другу будут двигаться два "наплыва", которые можно сравнить со своеобразными расчёсками. Зубчики одной расчёски будут входить в зазоры между зубчиками другой. Когда зазоры будут полностью заполнены, расчёски будут образовывать новое изображение.

Программа, реализующая описанный эффект, будет написана на языке C99, при этом будет задействована графическая библиотека pgraph. Но программа будет лишь генерировать кадры анимации в виде графических файлов. Сама анимация из полученных кадров будет построена с помощью видеоредакатора VirtualDub.

Будем, в дальнейшем, предполагать, что читатель в общих чертах знаком с библиотекой pgraph.

Структура программы

Программа состоит из трёх файлов библиотеки pgraph и файла combs.c, содержащего код, имеющий непосредственное отношение к решаемой нами задаче.

Файл combs.c начинается со следующих директив препроцессора:

#include <string.h>
#include "pgraph.h"
#define W 640            //Ширина кадра
#define H 360            //Высота кадра
#define STEP 2           //Шаг сдвига

Как мы видим, к файлу combs.c подключаются стандартный заголовочный файл <string.h> (необходим для использования функции memcpy()) и заголовочный файл графической библиотеки. Кроме этого, определяются 3 макроса. Смысл W и H, думаю, понятен из комментария. Размеры кадра мы, как обычно, выбираем, ориентируясь на Ютьюб.

Сдвигом договоримся называть количество точек произвольной строки новой картинки, уже отображённых в текущем кадре. Ясно, что при переходе от кадра к кадру это число будет увеличиваться, пока не достигнет своего максимального значения, равного ширине кадра. А шагом сдвига мы, как раз, будем называть число, на которое увеличивается сдвиг при переходе от предыдущего кадра к последующему.

Желательно, чтобы шаг сдвига был делителем ширины кадра. Отношение W / STEP даст нам количество кадров, демонстрирующих замещение одного изображения другим, причём, первый из этих кадров будет уже содержать некоторые точки нового изображения, а последний будет полностью с ним совпадать. Ясно, что при увеличении шага сдвига будет пропорционально увеличиваться скорость движения строк в итоговой анимации (при условии фиксированной частоты кадров, разумеется).

В нашем случае трансформация занимает 320 кадров, что при частоте кадров 25 кадр./с., соответствует продолжительности, равной 12,8с. Я решил, что в два раза большее значение продолжительности (т. е. примерно 25 с.), соответствующее шагу сдвига, равному 1, великовато, поэтому решил в качестве значения STEP взять 2.

В файле combs.c содержится код всего лишь двух функций — create_frame() и main(). Первая из них создаёт единственный кадр. Вторая генерирует новое и старое изображения, управляет созданием кадров посредством вызовов функции create_frame() и сохраняет их в графических файлах.

Создание кадров — функция create_frame()

Перед тем, как привести код функции create_frame(), сделаем замечание. Как мы знаем, для хранения информации в объектах типа image (этот тип описан в заголовочном файле pgraph.h) используются одномерные массивы. При этом информация о цветах точек каждой строки изображения содержится в элементах массива, идущих подряд.

Таким образом, создание кадра сводится, по сути, к копированию информации из двух массивов в третий, причём копирование можно производить не поэлементно, а блоками. Например, чтобы создать одну строку кадра достаточно произвести лишь 2 операции копирования посредством двукратного вызова функции memcpy().

Однако непросто сходу получить формулы для вычисления адресов участков памяти, участвующих в обмене информацией. Поэтому я решил сначала написать версию функции create_frame(), в которой каждый пиксель кадра обрабатывается отдельно. При этом не будет осуществляться доступ к массивам, хранящим цвета пикселей "напрямую", а будут задействованы функции из библиотеки pgraph get_color() (получение цвета пикселя) и set_color() (установление цвета пикселя).

В  дальнейшем, на основе этой версии будет написана вторая версия, в которой каждый внутренний цикл, содержащий вызовы pgraph get_color() и set_color(), будет заменён единственным вызовом функции memcpy(). Но вторую версию мы тоже усовершенствуем, и окончательной версией будет третья.

Ниже приведён код первой версии функции create_frame().

 1.image* create_frame(image *img1, image *img2, image *img3)
 2.{
 3.    static int disp = 0; //Сдвиг
 4.    disp += STEP;
 5.    //Левая расчёска
 6.    for (int j = 0; j < H; j += 2)
 7.    {
 8.        for (int i = 0; i < disp; i++)
 9.            set_color(img3, i, j, get_color(img2, i + W - disp, j));
10.        for (int i = disp; i < W; i++)
11.            set_color(img3, i, j, get_color(img1, i - disp, j));
12.    }
13.    //Правая расчёска
14.    for (int j = 1; j < H; j += 2)
15.    {
16.        for (int i = 0; i < W - disp; i++)
17.            set_color(img3, i, j, get_color(img1, i + disp, j));
18.        for (int i = W - disp; i < W; i++)
19.            set_color(img3, i, j, get_color(img2, i - W + disp, j));
20.    }
21.    return img3;
22.}

Формальные параметры img1, img2 и img3 должны содержать соответственно адреса нового и старого изображений, а также формируемого кадра. Функция раскрашивает кадр, используя информацию из двух изображений, и возвращает его адрес (т. е. значение переменной img3) в вызывающую функцию. Таким образом, сама функция create_frame() не выделяет динамически память для кадра, а использует память, уже выделенную ранее, адресуемую указателем img3.

Итак, в строке 3 объявляем статическую переменную disp, предназначенную для хранения сдвига (её значение сохраняется между вызовами функции). В строке 4 содержимое disp увеличивается на шаг сдвига и становится равным значению сдвига, актуальному для обрабатываемого кадра.

Далее "рисуем" в кадре левый наплыв (расчёску) (см. стр. 6-12). Каждой строке кадра можно поставить в соответствие её номер — ординату точек, входящих в её состав. В цикле for (стр. 6) перебираем всевозможные чётные номера строк. На каждой итерации цикла раскрашиваем строку кадра, номер которой равен значению переменной цикла j.

Делаем это следующим образом. Сначала в одном внутреннем цикле (стр. 8-9) обрабатываем первые disp пикселей строки кадра, присваивая им цвета последних disp пикселей соответствующей строки нового изображения. Затем в другом внутреннем цикле for (стр. 10-11) обрабатываем оставшиеся W - disp пикселей текущей строки кадра, присваивая им цвета первых W - disp пикселей соответствующей строки старого изображения.

Затем рисуем в кадре правую расчёску (стр. 14-20). Принцип такой же. Перебираем теперь во внешнем цикле всевозможные нечётные номера строк (стр. 14). Сначала в одном внутреннем цикле (стр. 16-17) присваиваем первым W - disp пикселям текущей строки кадра цвета такого же количества пикселей, расположенных в конце соответствующей строки старого изображения. Затем в другом внутреннем цикле (стр. 18-19) присваиваем оставшимся disp пикселям текущей строки кадра цвета такого же количества пикселей нового изображения, расположенных в начале соответствующей строки.

По завершению работы функции возвращаем из неё адрес кадра (стр. 21).

Вторая версия функции create_frame()

Переходим ко второй версии функции, в которой вместо четырёх внутренних циклов будут использованы вызовы функции memcpy().

Теперь мы уже будем напрямую обращаться к массивам, в которых хранятся цвета изображений. Будем делать это через указатели, которые инициализируем в начале функции create_frame():

color *pixels1 = img1->pixels;
color *pixels2 = img2->pixels;
color *pixels3 = img3->pixels;

Для работы с данными массивами нам пригодится умение вычислять индекс k элемента, в котором хранится цвет пикселя с координатами i и j. Формула весьма простая: k = j w + i. Здесь w — это ширина изображения в пикселях.

Используя эту формулу, из каждого внутреннего цикла будем конструировать вызов функции memcpy().

Обратимся, например, к первому внутреннему циклу (см. стр. 8-9 первой версии функции create_frame()). На каждой его итерации пиксель формируемого кадра, имеющий координаты i и j, окрашивается в цвет пикселя нового изображения с координатами i + W - disp и j. Несложно найти адреса элементов массивов, в которых хранятся цвета этих пикселей: они равны, соответственно, значениям выражений

pixels3 + j * W + i
и
pixels2 + (j + 1) * W - disp + i.

Но нам не нужны адреса всех элементов этих двух массивов. Ведь в результате выполнения внутреннего цикла содержимое одного участка памяти копируется в другой. Нас интересуют адреса именно этих участков памяти. Чтобы их получить, нужно заменить переменную цикла i в приведённых выше выражениях, её начальным значением; оно равно 0 (см. стр. 8 первой версии функции create_frame()). В итоге получаем: адрес участка, в который записывается информация и адрес, из которого она берётся, равны соответственно, значениям выражений

pixels3 + j * W
и
pixels2 + (j + 1) * W - disp.

Эти выражения и будут первым и вторым фактическими аргументами функции memcpy() соответственно. А третий аргумент — это количество копируемых элементов, умноженное на размер элемента. Это количество совпадает с числом итераций внутреннего цикла. В нашем случае оно равно disp.

Теперь мы уже можем записать вызов функции memcpy() и заменить им первый внутренний цикл:

memcpy(pixels3 + j * W, pixels2 + (j + 1) * W - disp, disp * sizeof(color));

Действуя по описанной выше схеме, заменяем оставшиеся три внутренних цикла тремя вызовами memcpy(). В результате получим следующий код второй версии функции create_frame():

image* create_frame(image *img1, image *img2, image *img3)
{
    static int disp = 0; //Сдвиг
    disp += STEP;
    color *pixels1 = img1->pixels;
    color *pixels2 = img2->pixels;
    color *pixels3 = img3->pixels;
    //Левая расчёска
    for (int j = 0; j < H; j += 2)
    {
        memcpy(pixels3 + j * W, pixels2 + (j + 1) * W - disp, disp * sizeof(color));
        memcpy(pixels3 + j * W + disp, pixels1 + j * W, (W - disp) * sizeof(color));
    }
    //Правая расчёска
    for (int j = 1; j < H; j += 2)
    {
        memcpy(pixels3 + j * W, pixels1 + j * W + disp, (W - disp) * sizeof(color));
        memcpy(pixels3 + (j + 1) * W - disp, pixels2 + j * W, disp * sizeof(color));
    }
    return img3;
}

Третья версия функции create_frame()

Оптимизируем код второй версии. Для начала, заметим, что внутри циклов используется выражение W - disp, значение которого постоянно в ходе всех итераций. Вычислим его один раз и сохраним в отдельной переменной:

int disp2 = W - disp;

В дальнейшем вместо упомянутого выражения будем использовать переменную disp2.

То же самое можно сказать и о выражениях disp * sizeof(color) и (W - disp) * sizeof(color). С той же целью введём отдельные переменные и поместим в них значения этих выражений:

int disp_size = disp * sizeof(color);
int disp2_size = disp2 * sizeof(color);

Ещё я решил соединить два цикла в один и на каждой итерации, в зависимости от чётности или нечётности её номера (итерации нумеруем, начиная с единицы), выполнять тот или иной фрагмент кода. Для этого я ввёл переменную oddnes. Она будет принимать значение 1, если номер текущей итерации нечётнен, и 0 — если чётен.

int oddness = 1;     //1, если номер текущей итерации нечётен

В конце каждой итерации будем присваивать oddnes новое значение, соответствующее нечётности следующей итерации.

Но переход от двух циклов к одному на производительность особо не повлияет. Использование одного цикла — дело вкуса (в данном случае, моего). А вот следующая наша цель — избавиться от оставшихся умножений, выполняемых внутри циклов (от некоторых мы уже избавились, введя переменные disp_size и disp_size2), — весьма важна.

Достичь этой цели можно, если сделать так, чтобы в начале каждой итерации цикла переменные pixels1, pixels2, pixels3 указывали на элементы массивов, соответствующие первым пикселям строк, обрабатываемых на данной итерации. Добиться этого легко: нужно в конце каждой итерации увеличивать значения этих переменных на W * sizeof(color) (здесь подразумевается обычная операция сложения; если говорить о сложении в рамках адресной арифметики, то увеличивать значения нужно на W).

Теперь часть тела цикла, отвечающая за вызовы функции memcpy(), будет выглядеть так:

if (oddnes)
{
    memcpy(pixels3, pixels2 + disp2, disp_size);
    memcpy(pixels3 + disp, pixels1, disp2_size);
}
else
{
    memcpy(pixels3, pixels1 + disp, disp2_size);
    memcpy(pixels3 + disp2, pixels2, disp_size);
}

Поскольку переменная цикла j в теле цикла уже не используется, я решил вместо j использовать переменную h, объявляемую следующим образом:

int h = H;

А вместо цикла for решил задействовать while. В качестве условия продолжения цикла я взял значение выражения h--.

Ниже приведён код третьей версии функции create_frame().

image* create_frame(image *img1, image *img2, image *img3)
{
    static int disp = 0; //Сдвиг
    disp += STEP;
    int disp2 = W - disp;
    int disp_size = disp * sizeof(color);
    int disp2_size = disp2 * sizeof(color);
    int h = H;
    int oddness = 1;     //1, если номер текущей итерации нечётен
    color *pixels1 = img1->pixels;
    color *pixels2 = img2->pixels;
    color *pixels3 = img3->pixels;
    while (h--)
    {
        if (oddness)
        {
            memcpy(pixels3, pixels2 + disp2, disp_size);
            memcpy(pixels3 + disp, pixels1, disp2_size);
        }
        else
        {
            memcpy(pixels3, pixels1 + disp, disp2_size);
            memcpy(pixels3 + disp2, pixels2, disp_size);
        }
        pixels1 += W;
        pixels2 += W;
        pixels3 += W;
        oddness = 1 - oddness;
    }
    return img3;
}

Итак, окончательная версия функции create_frame() получена.

Функция main()

А вот и код функции main(), управляющей всем процессом создания и сохранения кадров:

 1.int main()
 2.{
 3.    image *img1 = create_image(W, H);
 4.    image *img2 = load_from_file("image2.bmp");
 5.    image *img3 = create_image(W, H);
 6.    char filename[13];
 7.    int i;
 8.    for(i = 1; i <= 25; i++)
 9.    {   
10.       sprintf(filename, "results\\%03d.bmp", i);
11.       save_to_file(img1, filename);
12.    }
13.    int j = i + W / STEP;
14.    for(; i < j; i++)
15.    {   
16.       sprintf(filename, "results\\%03d.bmp", i);
17.       save_to_file(create_frame(img1, img2, img3), filename);
18.    }
19.    j = i + 23;
20.    for(; i <= j; i++)
21.    {
22.       sprintf(filename, "results\\%03d.bmp", i);
23.       save_to_file(img2, filename);
24.    }
25.    free(img1);
26.    free(img2);
27.    free(img3);
28.    return 0;
29.}

В строках 3-5 динамически создаём 3 объекта типа image. Первый содержит первоначальное изображение (белый прямоугольник). Второй — финальное изображение. Оно загружается из файла image2.bmp и к моменту выполнения программы должно находиться в той же директории, что и исполняемый файл. Третий объект предназначен для хранения текущих кадров, генерируемых функцией create_frame(). Изначально он содержит белый прямоугольник, но это не играет никакой роли.

В строках 6 и 7 объявляем символьный массив для хранения имён файлов и счётчик кадров i, который будет использоваться для формирования этих имён.

Далее идут 3 цикла for. Первый из них (8-12) генерирует 25 одинаковых кадров, совпадающих с первоначальным изображением, и сохраняет их в графических файлах. Второй (стр. 14-18) посредством вызовов функции create_frame() занимается созданием кадров, непосредственно участвующих в трансформации. Таких кадров всего W / STEP, т .е. 320. Все они также сохраняются в файлах. Наконец, третий цикл (стр. 20-24) генерирует и сохраняет 24 одинаковых кадра, совпадающих с финальным изображением.

Таким образом, общее количество кадров — 349. Все они сохраняются в каталоге results, который к моменту выполнения программы должен существовать и располагаться в той же директории, что и исполняемый файл. Графические файлы будут иметь имена 001.bmp, 002.bmp, и т. д., вплоть до 349.bmp.

После успешного сохранения графических файлов остаётся лишь удалить уже не нужные объекты типа image (стр. 25-27) и завершить выполнение программы (стр. 28).

Тестирование программы

В качестве финального изображения возьмём фотографию Кунсткамеры, которую я сделал пару недель назад. Погода стояла пасмурная, поэтому фотография оказалась мрачноватой.

Фотография с изображением Кунсткамеры

Кунсткамера в облачную погоду

Файл image2.bmp с данным изображением был помещён в директорию исполняемого файла. Этот файл можно скачать по ссылке.

После выполнения программы в каталоге results появились 349 графических файлов, содержащих кадры анимации. С помощью видеоредактора VirtualDub из них был сформирован видеоролик. При этом был использован кодек Huffyuv, сжимающий без потерь, а частота кадров была установлена равной 25кадр./с. (подробности использования VirtualDub'а см. в этой и в этой статьях). Продолжительность ролика составила 13,96с.

Ролик был загружен на Ютьюб. Вы можете посмотреть его прямо сейчас.

Теперь давайте в качестве исходного изображения вместо белого прямоугольника возьмём ещё одну фотографию. На этот раз это будет подводная лодка, всплывшая в акватории Невы, которую я сфотографировал несколько лет назад. Тогда было лето, и погода была солнечной.

Фотография с изображением подводной лодки

Подводная лодка на Неве в солнечную погоду

Чтобы в качестве исходного изображения использовалась данная фотография, нам нужно, во-первых, в функции main() заменить 3-ю строку строкой

image *img1 = load_from_file("image1.bmp");

Во-вторых, необходимо файл image1.bmp, содержащий изображение подводной лодки, поместить в директорию исполняемого файла. Файл image1.bmp можно скачать по ссылке.

Вот видеоролик, получившийся в итоге:

Слайд-шоу

Реализованный эффект трансформации изображений я решил применить в слайд-шоу. К двум фотографиям, размещённым выше, добавил мрачное изображение Исаакиевского собора, сфотографированного в один день с Кунсткамерой. К сожалению, достаточно долгое время Исаакий не может избавиться от дурацкого "скотча", которым обёрнута его верхняя часть.

Ещё я погулял на днях по городу, благо, погода выдалась солнечной, и поснимал различные виды Санкт-Петербурга. В итоге, к 3-м изображениям добавились ещё 8, и всего получилось 11. 

Для создания слайд-шоу потребовалось 10 переходов между изображениями. С помощью описанной в этой статье программы и видеоредактора VirtualDub было создано 10 соответствующих видеороликов, которые, затем, были объединены в один посредством того же VirtualDub'а.

В качестве шага сдвига я взял значение 16, чтобы увеличить скорость перехода между слайдами по сравнению с той, которая использовалась в двух приведённых в предыдущем разделе видеороликах. Таким образом, каждая трансформация состоит из 40 кадров (если считать частью трансформации последний кадр, идентичный финальному изображению) и длится 1,6с (использовалась частота 25кадр./c.). Каждый слайд демонстрируется зрителю в течение 4с. Суммарное количество кадров в итоговом видеоролике — 1490, что соответствует длительности видео, равной 59,6с.

Ролик загружен на Ютюб и доступен для просмотра:

Ну что ж, по-моему, получилось неплохо. Эх, ещё бы Исаакий без скотча!..

Скачать исходный код