はじめに
EEIC Advent Calendar 2020 に参加者があまりいなかった1ようなので、記事を書くことにしました。
さて何について書こうかと思いましたが、以前に研究発表で「伸びる折れ線グラフ」を用いた時にどのように作ったかを尋ねられたことがあり、知らない人もいそうなので「 MATLAB によるアニメーション作成 」について説明していきたいと思います。
アニメーションとは言ってもテレビアニメのようにキャラクターを動かしたりするのではなくて、本稿では論文に貼るような図に動きを持たせて印象的なプレゼンをする2ことを第一目的と考えていますのでご注意ください。
参加しているアドベントカレンダーの EEIC との関連を述べておきますが、東京大学では2019年度から学生の費用負担なしで MATLAB を使えるようになったため、 EEIC でも学部の授業や実験で活用されています(そのはずです)。研究で MATLAB を使っている方のお役に立てれば嬉しいです。
ただ、MATLAB で出来ることは大抵 Python でも出来るので、無料の Python の方が利用人口が多そうに思います3。逆に MATLAB の方が日本語情報は少ないので、こんな記事でも書いたっていいですよね……。
基本編
基本の流れは以下の通りです。
- グラフデータを生成する
- 各フレームの画像データを生成する
- 動画として出力する
動画の出力方法は普通の動画とアニメーションGIFとで異なります。普通の動画は VideoWriter オブジェクト を使い、アニメーションGIFは imwrite 関数 を使います。
PowerPoint に貼るなら一時停止やシークが可能な普通の動画をオススメしますが、両者は適材適所で使い分けるのが良いでしょう。そうなると同じコードで出力方式を切り替えたいですよね……。ということで以下のサンプルコードをご利用ください。
%% step 1
% グラフデータを生成するコードをここに書く
%% step 2
fig = figure; % Figure オブジェクトの生成
% 図を描画するコード(軸ラベルや範囲など)をここに書く
frames(100) = struct('cdata', [], 'colormap', []); % 各フレームの画像データを格納する配列
for i = 1:100 % 動画の長さは100フレームとする
% 図を更新するコードをここに書く
drawnow; % 描画を確実に実行させる
frames(i) = getframe(fig); % 図を画像データとして得る
end
%% step 3
switch exporttype % 今回は出力方法を exporttype 変数で指定することにする
case 'mp4' % 普通の動画の場合
video = VideoWriter('filename.mp4', 'MPEG-4'); % ファイル名や出力形式などを設定
open(video); % 書き込むファイルを開く
writeVideo(video, frames); % ファイルに書き込む
close(video); % 書き込むファイルを閉じる
case 'gif'
filename = 'filename.gif'; % ファイル名
for i = 1:100
[A, map] = rgb2ind(frame2im(frames(i)), 256); % 画像形式変換
if i == 1
imwrite(A, map, filename, 'gif', 'DelayTime', 1/30); % 出力形式(30FPS)を設定
else
imwrite(A, map, filename, 'gif', 'DelayTime', 1/30, 'WriteMode', 'append'); % 2フレーム目以降は"追記"の設定も必要
end
end
end
注意: 既定の動画圧縮方法は Motion JPEG なので、背景がまっさらだと特にブロックノイズが目立ってしまいます。忘れずに H.264 (MPEG-4) へ変えましょう。
実践編
環境: Windows 10, MATLAB R2020a
伸びる折れ線グラフ (plot -> animatedline)
普通の2次元プロットは plot 関数を用いますが、頻繁に使われることが想定されているのか、アニメーション専用に animatedline 関数 と addpoints 関数 が用意されています。
animatedline 関数で線の色や種類を指定し、 addpoints 関数で点を追加して線を伸ばします。
%% step 1
x = 0:0.1:10;
y = [sin(x); cos(x)];
%% step 2
fig = figure; % Figure オブジェクトの生成
lines = [animatedline('Color', 'red'); animatedline('Color', 'blue')]; % AnimatedLine オブジェクトの生成
xlim([0 10]); ylim([-1 1]); % 描画範囲の固定
frames(100) = struct('cdata', [], 'colormap', []); % 各フレームの画像データを格納する配列
for i = 1:100 % 動画の長さは100フレームとする
addpoints(lines(1), x(i), y(1, i)); % 点を追加(sin)
addpoints(lines(2), x(i), y(2, i)); % 点を追加(cos)
drawnow; % 描画を確実に実行させる
frames(i) = getframe(fig); % 図を画像データとして得る
end
塗りつぶし等高線図 (contourf)
フレームごとに全体を描画し直すのでそれほど複雑ではないでしょう。
%% step 1
t = 0:0.1:10; % 時間軸
[x, y] = meshgrid(-1:0.1:1); % X, Y軸
z = cell(size(t));
for i = 1:length(t)
z{i} = (x - cos(2 * pi * t(i) / 10)) .^ 2 + (y - sin(2 * pi * t(i) / 10)) .^ 2; % 各時刻のデータ
end
%% step 2
fig = figure; % Figure オブジェクトの生成
xlim([-1 1]); ylim([-1 1]); % 描画範囲の固定
frames(100) = struct('cdata', [], 'colormap', []); % 各フレームの画像データを格納する配列
for i = 1:100 % 動画の長さは100フレームとする
contourf(x, y, z{i}, 0:0.5:4); % 等高線図の描画
colorbar(); % カラーバーの描画(毎回必要)
drawnow; % 描画を確実に実行させる
frames(i) = getframe(fig); % 図を画像データとして得る
end
点が増える散布図 (scatter)
hold on して scatter 関数を繰り返し呼び出してもいいですが、既に描画したグラフィックオブジェクトのプロパティをいじって、1点ずつ表示させていく方法を紹介します。 NaN 値が描画されないことを利用しています。
%% step 1
x = randn(100, 1); % X軸データ
y = randn(100, 1); % Y軸データ
y_dummy = nan(100, 1); % Y軸ダミーデータ
%% step 2
fig = figure; % Figure オブジェクトの生成
plt = scatter(x, y_dummy); % ダミーデータで Scatter オブジェクトを生成(NaNなので何も表示されない)
xlim([-3 3]); ylim([-3 3]); % 描画範囲の固定
frames(100) = struct('cdata', [], 'colormap', []); % 各フレームの画像データを格納する配列
for i = 1:100 % 動画の長さは100フレームとする
plt.YData(i) = y(i); % ダミーデータを実データへ差し替え
drawnow; % 描画を確実に実行させる
frames(i) = getframe(fig); % 図を画像データとして得る
end
伸びる棒グラフ (bar)
こんなのが役立つような場面は想像できませんが、完成形を描画してから時間軸を逆に戻すこともできる例として紹介します。
%% step 1
y = randi(100, 10, 1); % Y軸データ
%% step 2
fig = figure; % Figure オブジェクトの生成
plt = bar(y); % 完全な棒グラフの描画
ylim([0 100]); % 描画範囲の固定
frames(100) = struct('cdata', [], 'colormap', []); % 各フレームの画像データを格納する配列
for i = 1:100 % 動画の長さは100フレームとする
plt.YData = y * ((100 - i) / 99); % Y軸データを縮める
drawnow; % 描画を確実に実行させる
frames(i) = getframe(fig); % 図を画像データとして得る
end
frames = flip(frames); % 時間軸を逆転する
動く図形 (patch)
変化してほしい部分だけプロパティを書き換えるのがオススメです。
patch 関数で表示させた図形は XData, YData プロパティです。関数の引数とプロパティ名の対応関係は、グラフィックオブジェクトのヘルプページで確認します。
%% step 1
t = 0:0.1:10; % 時間軸
c = 3 * t / 10 - 1.5; % 中心点X軸
x = c + cos(2 * pi * ([0; 0.25; 0.5; 0.75] + t / 5)) / 2; % 頂点X軸
y = sin(2 * pi * ([0; 0.25; 0.5; 0.75] + t / 5)) / 2; % 頂点Y軸
%% step 2
fig = figure; % Figure オブジェクトの生成
plt = patch(x(:, 1), y(:, 1), 'red'); % Patch オブジェクトの生成
xlim([-1 1]); ylim([-1 1]); % 描画範囲の固定
frames(100) = struct('cdata', [], 'colormap', []); % 各フレームの画像データを格納する配列
for i = 1:100 % 動画の長さは100フレームとする
plt.XData = x(:, i); % X軸データをセット
plt.YData = y(:, i); % Y軸データをセット
drawnow; % 描画を確実に実行させる
frames(i) = getframe(fig); % 図を画像データとして得る
end
時刻等を文字で入れる
くどいようですが、変化してほしい部分だけプロパティを書き換えるのがオススメです。
text 関数で表示させた文字は String プロパティです。
%% step 2
fig = figure; % Figure オブジェクトの生成
txt = text(0, 0, '', 'FontSize', 32, 'HorizontalAlignment', 'center'); % Text オブジェクトの生成
xlim([-1 1]); ylim([-1 1]); % 描画範囲の固定
frames(100) = struct('cdata', [], 'colormap', []); % 各フレームの画像データを格納する配列
for i = 1:100 % 動画の長さは100フレームとする
txt.String = sprintf('Frame: %d', i); % 表示する文字列を書き換え
drawnow; % 描画を確実に実行させる
frames(i) = getframe(fig); % 図を画像データとして得る
end
(appendix) 知見
ToolBar を表示させない
図にマウスカーソルを乗せると描画中でもツールバーが表示されてしまって、動画にも映り込んでしまいます。
Figure オブジェクトの ToolBar プロパティを 'none' に設定することをオススメします。
fig = figure;
fig.ToolBar = 'none';
背景色を白にする
静止画として保存するときは背景色が白になっていても、動画にすると背景色が灰色になっている場合があります。
Figure オブジェクトの Color プロパティを明示的に 'white' と設定しましょう。
fig = figure;
fig.Color = 'white';