Pull to refresh

Создание анимированных графиков с помощью Matlab

Level of difficultyEasy
Reading time6 min
Views6.5K

Введение

Всем здравствуйте! Иногда у меня возникает потребность в анимированных графиках. Они помогают сделать подачу информации красивее и, что самое главное, нагляднее. Matlab позволяет создавать такие графики, причём с помощью всего нескольких функций. В данной статье будут рассмотрены основные принципы создания анимации — мы сделаем парочку графиков и сохраним всё это в GIF-файлы. Вот один из примеров, чтобы вы понимали, какой можно получить результат:

Анимированный график, созданный с помощью Matlab
Анимированный график, созданный с помощью Matlab

Как получить кадр и куда-нибудь его записать?

Я думаю, что ни для кого не секрет, что анимация — это последовательность кадров. Чем больше кадров отображается за секунду, тем более плавной нам кажется картинка. Для получения кадра в Matlab есть функция getframe. На кадре может быть только сам график, а может быть и всё графическое окно. Пока что просто построим график привычным нам способом:

x = 0 : 0.01 : 3;
y = 2*x.^3 - 2*sin(tan(x)) + 25*cos(x.^-3);
fh = figure;
plot(x, y)
grid on

Получим кадр всего графического окна и запишем его в JPEG-файл:

frame = getframe(fh);
image = frame2im(frame);
[X, cmap] = rgb2ind(image, 256);
imwrite(X, cmap, 'myframe.jpeg', 'jpeg');
myframe.jpeg
myframe.jpeg

Разберёмся, что происходит в этом коде. С помощью первой строки мы получили кадр. На второй строке мы конвертировали его в картинку с помощью функции frame2im. На третьей строке мы получили индексированное изображение X и его палитру cmap с помощью функции rgb2ind. При этом можно задать, сколько максимум цветов будет в изображении (с помощью второго аргумента функции rgb2ind) — мне кажется, что 256 хватит для любого графика. На четвёртой строке мы используем полученные данные и функцию imwrite, чтобы записать наш график в файл myframe.jpeg. Поменяем первую строку таким образом, чтобы получить график без графического окна:

frame = getframe(gca);
myframe.jpeg
myframe.jpeg

Заметим, что идентичный результат можно было получить и с помощью кода ниже:

frame = getframe;

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

Попробуем создать первый анимированный график

Давайте сделаем так, чтобы синусоида вечно двигалась вправо:

x = 0 : 0.01 : 2;

for i = 0 : 0.1 : 2
    plot(x, sin(2*pi*x - pi*i))
    xlim([0 2])
    frame = getframe;
    image = frame2im(frame);
    [X, cmap] = rgb2ind(image, 256);
    if i == 0
        imwrite(X, cmap, 'myanim.gif', 'gif', 'LoopCount', Inf, ...
            'DelayTime', 1/24);
    else
        imwrite(X, cmap, 'myanim.gif', 'gif', 'WriteMode', 'append', ...
            'DelayTime', 1/24);
    end
end
myanim.gif
myanim.gif

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

В цикле for на каждой итерации мы получаем новый кадр, после чего сразу записываем его в GIF-файл. Отдельное внимание уделим строкам 9-14 листинга. По умолчанию функция imwrite записывает кадр поверх всего файла, то есть если не менять никакие параметры, на выходе мы получим GIF-файл с одним кадром (последним). При добавлении первого кадра мы ждём именно такого поведения функции, поэтому параметр WriteMode мы не трогаем. С помощью параметра LoopCount мы устанавливаем количество повторений анимации (в данном случае — бесконечное), а с помощью параметра DelayTime — длину кадра (в данном случае это \frac{1}{24}, т.к. желаемая частота кадров анимации — 24 кадра в секунду). Все остальные кадры должны записываться в конец файла, для этого мы изменяем параметр WriteMode на 'append'. Изменяя шаг для переменной i и длину кадра можно менять скорость и плавность анимации.

Создание анимированной линии

Теперь попробуем сделать так, чтобы синусоида рисовалась постепенно (слева направо). Конечно, можно сначала рассчитать вектор со всеми значениями функции, а потом в цикле for постепенно его отрисовывать — сначала только первый элемент, потом первые два элемента, потом первые три и т.д., пока не выведем весь вектор. Этот вариант, конечно, сработает, но в Matlab есть функции animatedline и addpoints, которые можно использовать в подобных ситуациях. Перейдём к примеру:

lh = animatedline;

for i = 0 : 0.01 : 2
    addpoints(lh, i, sin(2*pi*1*i));
    xlim([0 2])
    ylim([-1 1])
    frame = getframe;
    image = frame2im(frame);
    [X, cmap] = rgb2ind(image, 256);
    if i == 0
        imwrite(X, cmap, 'myanim.gif', 'gif', 'LoopCount', Inf, ...
            'DelayTime', 1/60);
    else
        imwrite(X, cmap, 'myanim.gif', 'gif', 'WriteMode', 'append', ...
            'DelayTime', 1/60);
    end
end
myanim.gif
myanim.gif

На первой строке мы создаём объект анимированной линии. В цикле мы добавляем к нашей линии по одной точке с помощью функции addpoints. Первым аргументом передаём объект анимированной линии, вторым и третьим аргументами — координаты x и y новой точки соответственно. Удалить все точки из объекта анимированной линии можно с помощью функции clearpoints. Получить все его точки можно с помощью функции getpoints.

Пример 1

В начале статьи я приводил пример анимированного графика. Попробуем его, наконец, реализовать:

%% Плавное появление синусоиды

fileName = 'habr1.gif';
figure('Position', [0, 0, 1280, 720])
grid on
sine = animatedline;
sine.Color = 'blue';

f = 1; %Частота синусоиды
for i = 0 : 0.01 : 1
    addpoints(sine, i, sin(2*pi*f*i - pi/12));
    xlim([0, 1])
    ylim([-1.1, 1.1])
    [A, map] = GetIndImage;
    if i == 0
        imwrite(A, map, fileName, 'gif', 'LoopCount', Inf, ...
            'DelayTime', 1/60);
    else
        imwrite(A, map, fileName, 'gif', 'WriteMode', 'append', ...
            'DelayTime', 1/60);
    end
end

%% Раздвоение и сдвиг синусоиды

[sine_x, sine_y] = getpoints(sine);
x = 0 : 0.01 : 1;

for phi = 0 : 0.02 : pi/2
    sine_offset_y = sin(2*pi*f*x - pi/12 - phi);
    hAx = plot(sine_x, sine_y, 'r', ...
        x, sine_offset_y, 'b');
    grid on
    box off
    xlim([0, 1])
    ylim([-1.1 1.1])
    [A, map] = GetIndImage;
    imwrite(A, map, fileName, 'gif', 'WriteMode', 'append', 'DelayTime', 1/60);
end

%% Плавное появление вертикальных линий

left_line = animatedline;
right_line = animatedline;

for i = 1.1 : -0.05 : -1.1
    addpoints(left_line, 1/24, i);
    addpoints(right_line, 0.4166, i);
    xlim([0, 1])
    ylim([-1.1, 1.1])
    [A, map] = GetIndImage;
    imwrite(A, map, fileName, 'gif', 'WriteMode', 'append', 'DelayTime', 1/60);
end

%% Плавное закрашивание области

rh = rectangle('Position', [1/24 -1.2 0.4166-1/24 2.4], 'FaceColor', [0 0 0 0]);

for i = 0 : 0.01 : 0.3
    rh.FaceColor = [0 0 0 i];
    [A, map] = GetIndImage;
    imwrite(A, map, fileName, 'gif', 'WriteMode', 'append', 'DelayTime', 1/60);
end

imwrite(A, map, fileName, 'gif', 'WriteMode', 'append', 'DelayTime', 1.5);

%% Функция получения индексированного изображения

function [A, map] = GetIndImage
    frame = getframe;
    image = frame2im(frame);
    [A, map] = rgb2ind(image, 256);
end

Сразу обращу внимание, что получение индексированного изображения из кадра я вынес в отдельную функцию, её определение на строках 69-73. До 22 строки всё, как мне кажется, должно быть понятно — это плавное появление синусоиды, которое мы делали и в предыдущем разделе. На строке 26 происходит преобразование получившейся синусоиды в набор точек, который потом используется для построения этой же синусоиды с помощью функции plot в следующем цикле. С плавным появлением вертикальных линий, как мне кажется, тоже всё должно быть понятно. Плавное закрашивание области реализовано с помощью плавного понижения прозрачности прямоугольника. Делать так можно, кстати, далеко не со всеми объектами. К сожалению, менять прозрачность текста нельзя. Строка 65 нужна для задержки кадра в конце анимации. То есть на протяжении анимации частота обновления кадров может быть разной.

Пример 2

Напоследок приведу анимацию, которая реально помогает нагляднее представить информацию. Она показывает, что такое эффект "растекания спектра", который возникает при применении дискретного преобразования Фурье.

fileName = 'habr2.gif';
fh = figure('Position', [0 0 1280 720]);
x = -0.1 : 0.01 : 1.5;
y = sin(2*pi*1*x);
counter = 0;

%% Движение вправо

for i = 0.8 : 0.01 : 1.2
    subplot(2, 1, 1)
    plot(x, y, 'LineWidth', 1.5)
    grid on
    rectangle('Position', [0 -1.2 i 2.4], ...
        'FaceColor', [204/255 153/255 255/255 0.3], ...
        'LineWidth', 0.01)
    xline(1, 'r--', 'LineWidth', 1.3)
    xlim([-0.01 1.21])
    ylim([-1.1 1.1])

    subplot(2, 1, 2)
    stem(abs(fft(y(11 : 90 + counter))))
    xlim([0, 121])
    ylim([0, 60])
    counter = counter + 1;
    
    frame = getframe(fh);
    image = frame2im(frame);
    [A, map] = rgb2ind(image, 256);
    if i == 0.8
        imwrite(A, map, fileName, 'gif', 'LoopCount', Inf, 'DelayTime', 1/24);
    else
        imwrite(A, map, fileName, 'gif', 'WriteMode', 'append', 'DelayTime', 1/15);
    end
end

%% Движение влево

counter = counter - 1;
for i = 1.2 : -0.01 : 0.8
    subplot(2, 1, 1)
    plot(x, y, 'LineWidth', 1.5)
    grid on
    rectangle('Position', [0 -1.2 i 2.4], ...
        'FaceColor', [204/255 153/255 255/255 0.3], ...
        'LineWidth', 0.01)
    xline(1, 'r--', 'LineWidth', 1.3)
    xlim([-0.01 1.21])
    ylim([-1.1 1.1])
    
    subplot(2, 1, 2)
    stem(abs(fft(y(11 : 90 + counter))))
    xlim([0, 121])
    ylim([0, 60])
    counter = counter - 1;
    
    frame = getframe(fh);
    image = frame2im(frame);
    [A, map] = rgb2ind(image, 256);
    imwrite(A, map, fileName, 'gif', 'WriteMode', 'append', 'DelayTime', 1/15);
end
habr2.gif
habr2.gif

Все приёмы, что есть в приведённом выше коде, мы с вами уже обсудили. Надеюсь, что статья была для вас полезной!

Tags:
Hubs:
Total votes 25: ↑25 and ↓0+25
Comments9

Articles