VEGAS Proのスクリプトで座標データからトラックモーションを編集する

VEGAS Proという動画編集ソフトは割と頻繁にセールがあって1万円ちょっとで手に入る。動画の切り貼りくらいしかしない自分にとっては、Premier Elementsと比べて使いやすく感じている。ただ、トラックモーションに関しては少し物足りなく感じることがある。

トラックモーション(≠モーショントラック)とは、映像トラック(画像編集ソフトでいうところのレイヤのイメージ)を動かす機能のことで、例えば字幕や別の映像を移動させるのに使う機能である。VEGAS Proにはトラックモーションを手動で編集する機能はあるものの、映像に追従するとか外部のデータを読み込むといった、凝った機能はない。

> ちなみに、別の動画編集ソフトHitfilmでは、モーショントラック(1つか2つの画像の特徴点を追跡する機能)が搭載されていて、追跡結果を映像の合成に使えるらしい。VEGASトラックモーション(映像トラックを動かす機能)は、追跡結果さえあればモーショントラックを実現するのに使えるものの、トラッキング機能は搭載されていない。

足りないところを補うために、VEGAS ProにはC#でスクリプトを開発できる機能がある。Visual StudioのIntelliSenseやデバッガを使うことができるようになっている。そこでこのページでは、この機能を使って座標データからトラックモーションを生成するスクリプトを作ってみる。

# どんなものを作るのか
別のツールで作成した目線の動き(時刻と座標のデータ)を読み込み、トラックモーションに反映するスクリプトを作成する。四角い枠の画像を置いたトラックのトラックモーションを、目線の動きに合わせて移動させることで、以下の画像のように見ているところ(ここでは看板)に四角いを表示できるようになる。
<a href="https://youtu.be/uz6X0j4Lmf8"><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/374475/1bfe7c3d-c3d7-be04-0c36-79b639075f5d.png"/></a>

# スクリプト開発の準備
何も情報が無いと大変なので、情報源と動作確認の仕方と、本番環境での利用方法をまとめる。

## 情報源
VEGAS Proが公開するAPIや、デバッグ方法などの情報は、以下から取得できる。なぜかウェブページになっておらず、HTMLファイルとして提供されている。FAQを読むとデバッグ方法が載っているので見ておくと良い。

* https://www.vegascreativesoftware.info/us/forum/vegas-pro-scripting-faqs-resources--104563/

APIの情報は、どのクラスが何に対応するか分かりにくいものの、基本的には人がUIを通して見えるVEGASと同じような感覚で情報や機能にアクセスできるようになっているので、それをイメージしながらクラスやプロパティを辿ることができる。

## サンプルとその実行
FAQにあるように、`EntryPoint`クラスのメソッド`FromVegas`メソッドを実装すると、VEGASから呼び出せるようになる。実行時には、そのメソッドにVEGASの情報へアクセスするための`Vegas`クラスオブジェクトが引数に与えられる。

```C#:Sample.cs
using System.Windows.Forms;
using ScriptPortal.Vegas;

namespace Sample
{
    public class EntryPoint {
        public void FromVegas(Vegas vegas) {
            MessageBox.Show("hello world");
        }
    }
}
```

実行するには、スクリプト(C#)をVEGASのインストールフォルダの`Script Menu`にそのまま入れるだけで良い。VEGASを起動するか、既に起動していたら「スクリプトメニューフォルダの再スキャン」を実行すると、スクリプトのファイル名(拡張子なし)がメニューに表示される。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/374475/92f643c5-0e2e-8ad3-c7c4-526fcf61d047.png)

ここで、上記`Sample.cs`に対応する`Sample`を選択すると。以下のようにメッセージボックスが表示される。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/374475/2e68444c-d06d-76ed-3294-5c1199651af7.png)

### スクリプトを置く場所
FAQによれば、以下のフォルダにスクリプトを置くことができるとのこと。(17.0はバージョン)

```
C:\Users\<username>\Documents\Vegas Script Menu\ 
C:\Users\<username>\AppData\Local\Vegas Pro\17.0\Script Menu\
C:\Users\<username>\AppData\Roaming\Vegas Pro\17.0\Script Menu\
C:\ProgramData\Vegas Pro\17.0\Script Menu\
C:\Users\<username>\AppData\Local\Vegas Pro\Script Menu\
C:\Users\<username>\AppData\Roaming\Vegas Pro\Script Menu\
C:\ProgramData\Vegas Pro\Script Menu\
```

## Visual Studioでの開発方法
FAQの「How do create and debug a script using Visual Studio?」に載っている準備をしておくと、IntelliSenseによる自動補完やデバッガが使えるようになる。

まず、C#のクラスライブラリプロジェクトを作成する。`.cs`ファイルがスクリプトの名前になるので、適当に名前を付けておく。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/374475/d5fcdd1f-8f12-9613-f4b7-58775bbdcba7.png)

作成したプロジェクトの「参照」に、VEGASインストールディレクトリにある`ScriptPortal.Vegas.dll`を追加する。これによりプロジェクトの「参照」に`ScriptPortal.Vegas`が追加される。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/374475/fcfb7915-1286-1297-0276-3c0c732a038f.png)　![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/374475/cb0c1256-d8e5-d474-7e53-19513e29c1b4.png)

参照の追加ができたら自動補完が機能するようになる。後はほかに必要な参照や上記サンプルのように決められたクラスとメソッドを作成していく。`Vegas`クラスを通してVEGAS上の情報や機能にアクセスできるようになる。デバッグをするには更に以下の設定を行う。

> FAQによると、さらに別の参照が必要な場合、その情報を書いたXMLファイルが必要になるらしい。

プロジェクトのプロパティを開き、「デバッグ」タブの「開始動作」で「外部プログラムの開始」にVEGAS本体(以下の例ではバージョン15なので、vegas150.exe)を選択する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/374475/db851e70-d38b-1a3e-3653-bdbf09789140.png)

デバッガを起動すると、事前に設定した通りVEGASが起動する。こうして起動したデバッグ用のVEGASで、動作確認に使うファイルを開く等の準備ができたら、スクリプトを実行する。

スクリプトの実行には、まず「ツール>スクリプトの作成>スクリプトの実行」を選択して、対象のスクリプトのDLLを選択する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/374475/30ea73f1-c598-a3a9-6c88-16fc93791e2e.png)

> ファイルフィルタがうまく機能していないので、手入力する必要がある模様。「プロジェクトフォルダ > bin > Debug」でフォルダまでダ取り付いたら、プロジェクト名.dllをファイル名に書くと、候補が表示される。

DLLの選択をしたらスクリプトが実行されるので、スクリプトからの応答やブレークポイントで停止するのを待つ。以下はメッセージボックスを表示する行のでブレークがかかった時の様子。「自動変数」の一覧内で、この時点の`vegas`オブジェクトの中身を覗くことができるようになっていることが分かる。デバッガをうまく活用すれば、やみくもにトライアンドエラーを繰り返さなくて済むので、デバッガの設定・スクリプトの起動方法は覚えておいた方が良い。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/374475/a16a89f1-e601-a57b-549e-020dfe4fc6b6.png)

# スクリプトからVEGASを操作する
動作確認する準備までできたら、実際にスクリプトからVEGASを操作してみる。ここでは、座標データをもとにトラックモーションを作るのに関連する操作をピックアップする。

## 選択しているビデオトラックを取得する
VEGASでは、ビデオトラックとオーディオトラックという、映像と音声を表すトラックが存在する。選択したトラックを操作したいことはよくあるので、ここでは選択しているビデオトラックを取得する関数を作ってみる。こうした関数を作るには、「選択しているトラックを取得」し、「トラックがビデオトラックかの判定」することが必要となる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/374475/eac28d64-7663-f932-c2fc-f214c7b47232.png)

VEGAS本体、`EntryPoint`クラスの`FromVegas`メソッドの引数`Vegas`クラスのオブジェクト(ここでは`vegas`)から参照できる。そこから、開いているプロジェクトは`vegas.Project`プロパティで参照でき、更にプロジェクトに含まれるトラック群(ビデオ or オーディオ)は`vegas.Project.Tracks`プロパティで参照できる。トラック群には複数のトラックが含まれるので、`foreach`などで1つ1つのトラックを参照できる。トラックは、`Selected`プロパティで選択されているかどうかを、`IsVideo()`メソッドでビデオトラックかどうかをそれぞれ判定できるようになっている。

こうして得られる情報を使うことで、VEGAS上で選択しているビデオトラックを取得する関数を作ることができる。

```cs:FindSelectedVideoTrack

public class EntryPoint
{
  public void FromVegas(Vegas vegas)
  {
    // 選択しているトラックを取得する
    VideoTrack track = FindSelectedTrack(vegas.Project.Tracks);

    if (track == null ) {
      MessageBox.Show("ビデオトラックを選択してください。");
      return;
    }
  }
}

VideoTrack FindSelectedVideoTrack(Tracks tracks)
{
  foreach (Track track in tracks)
  {
    if ( track.Selected && track.IsVideo() )
    {
      return (VideoTrack)track;
    }
  }
  return null;
}
```
## 選択したトラックイベントの時間を取得する
トラック上の映像や音声などの「トラックイベント」は、`Track`クラスオブジェクトの`TrackEvents`プロパティの要素である`TrackEvent`クラスオブジェクトから参照できるようになっている。`TrackEvent`は選択されているかの情報を`Selected`プロパティで持っているので、例えば以下のように選択された`TrackEvent`を調べ、その開始時刻・終了時刻・幅を取得することができる。

```cs:TrackEvent.cs
public void FromVegas(Vegas vegas)
{
  VideoTrack track = FindSelectedTrack(vegas.Project.Tracks); // 別途作成した関数

  foreach( TrackEvent evt in track.Events ) {
    if (evt.Selected)
    {
      MessageBox.Show(String.Format("Start: {0}\nLength: {1}\nEnd: {2}",
                      evt.Start,
                      evt.Length,
                      evt.End ) );
    }
  }
}
```
以下は、「Goose」というトラックイベントを選択した状態でスクリプトを実行した結果となる。6:30に開始、8:00に終了していることが正しく表示できている。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/374475/15c25d64-81b2-4077-19f5-7b4d9671206b.png)

## 選択している時間を取得する
VEGASでは、ある期間の映像を操作するために時間軸のカーソル・選択範囲がある。このカーソルや選択範囲は`Vegas`クラスの`Transport`プロパティで参照することができる。

```cs:TransportControl
public class EntryPoint
{
  public void FromVegas(Vegas vegas)
  {
    MessageBox.Show(String.Format( "Start: {0}\nLength: {1}\nCursor: {2}",
                                    vegas.Transport.SelectionStart,
                                    vegas.Transport.SelectionLength,
                                    vegas.Transport.CursorPosition ) );
  }
}
```

上記スクリプトを実行すると、その時選択している範囲のカーソル位置・選択範囲の開始時刻と幅を取得できる。ここで、選択範囲の開始時刻はカーソル位置となるため、選択の仕方によっては幅が負の値をとることに注意する。(時間軸の左から右に向かって選択すると、後の方がカーソルになり、幅は負となる)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/374475/960dfb40-f316-942a-b047-f0ec50c958ca.png)

## トラックモーションを編集する
ビデオトラックは、映像の座標系に対して「いつ(キーフレーム)・どんな姿勢」という情報である「トラックモーション」を持つ(厳密には、映像そのものだけでなく、影・光彩それぞれ別々のトラックモーションを持っている)。トラックモーションは`VideoTrack`クラスの`TrackMotion`プロパティからアクセスできる。特にスケールファクタの設定をしなければ、下図のように映像と一対一の関係となる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/374475/ba67ba51-7178-f976-01df-07aca7f237d8.png)

ビデオトラックのトラックモーションへの操作はUIの時と同様で、まずキーフレームを作成し、キーフレームに対する姿勢(UIでは位置と書かれている)がどうなっているかを設定する。

キーフレームを追るには、時刻を`timecode`[ms]を指定して`track.TrackMotion.TrackMotionKeyframes.Insert(timecode)`メソッドを呼び出す。このメソッドは戻り値に`TrackMotionKeyframe`クラスのオブジェクトを返すので、得られたキーフレームに対して姿勢(x,y,zや回転など)を設定すればよい。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/374475/ea274ae4-0eaa-dc21-8c9c-82fb2529af6a.png)
この操作をまとめると以下のようになる。`InserstMotionKeyFrame`でキーフレーム`frame`を作成し、その座標をプロパティ`Position*`や`Rotateion*`などにより設定する。

```cs:InsertMotionKeyFrame
// track: 対象のビデオトラック
// x, y座標
// ms: 時刻[ms]

// キーフレームを作成して
TrackMotionKeyframe frame = track.TrackMotion.InsertMotionKeyframe(new Timecode(ms));

// 姿勢を設定する
frame.PositionX = x;
frame.PositionY = y;

// トラックの全キーフレームを消したい場合はClear()
track.TrackMotion.MotionKeyframes.Clear();
```

# 実際にスクリプトを書いてみる
準備が長くなったが、これまでの情報を使って、スクリプト実行時点で選択されたビデオトラックに対して、トラックイベント(映像・画像)が存在する期間だけ、読み込んだ座標データの座標をトラックモーションに反映するスクリプトを作成する。

例えば以下のように、赤い丸が描かれた画像のトラックイベントをトラックモーションを反映したい期間だけ配置しておき、そのトラックを選択した状態でスクリプトを実行すると、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/374475/ca3739c4-8cda-ef94-c754-f7afe02d6348.png)!
以下のように、トラックイベントのあるところだけ、座標データから読み込んだ座標がトラックモーションが反映され、赤い丸の画像を移動することができる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/374475/57be329c-661c-48cb-02a1-8bca54c074db.png)!

## 作成したスクリプト
以下に示すように、大きく分けて5つの処理と補助関数で実現した。実施していることはコードに書いてあるコメントのままだが、要約すると、

1. トラックモーションを反映する対象を取得する。ビデオトラックが選択されていないか、ビデオトラックにトラックイベントが存在しなければ中断する。
2. 座標データのファイルを選択するダイアログを表示し、ファイル名を取得する。
3. 座標データを読み込むストリームの作成と、データ点数の算出する※。
4. 既存のトラックモーションをいったんクリアしたうえで、座標データの全点を順番に読み込みながら処理していく。トラックイベント外の期間はスキップするよう、読み込んだデータの時刻(現在時刻と呼ぶ)で、処理を分けている。
    1. 現在時刻が最後のトラックイベントの終了時刻を超えたら、ループを抜ける。
    2. 現在時刻が直近のトラックイベントの終了時刻を超えたら、「直近のトラックイベント」の開始・終了時刻を更新する。
    3. 現在時刻が直近のトラックイベントの期間内なら、データを間引いてトラックモーションに反映する。
5. 終了前の処理(ここではストリームを閉じるだけ)を行う。

```cs:LoadTrackMotion.cs
using System;
using System.IO;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using ScriptPortal.Vegas;

namespace Sample
{
  public class EntryPoint
  {
    public void FromVegas(Vegas vegas)
    {
      // ----------------------------------------------------------------
      // [1] スクリプト起動時に選択していたビデオトラックを取得
      // ----------------------------------------------------------------
      VideoTrack track = FindSelectedTrack(vegas.Project.Tracks);
      if (track==null)
      {
        return;
      }
      
      // ビデオトラックが選択されていなければ中断
      if (track == null) {
        MessageBox.Show("ビデオトラックを選択してください。");
        return;
      }

      // ビデオトラックにトラックイベントがなければ中断
      TrackEvents events = track.Events;
      if (events.Count == 0)
      {
        MessageBox.Show("トラックにビデオが含まれていません。");
        return;
      }

      // ----------------------------------------------------------------
      // [2] 座標データを含むバイナリファイルを選択する
      // ----------------------------------------------------------------
      OpenFileDialog ofd = new OpenFileDialog();
      ofd.CheckFileExists = true;
      ofd.CheckPathExists = true;
      if (ofd.ShowDialog() != DialogResult.OK)
      {
        MessageBox.Show("読み込みを中止します。");
        return;
      }

      // ----------------------------------------------------------------
      // [3] 座標データ( long tick, float x, float y )を
      // 　　を読み込むストリームを開く
      // ----------------------------------------------------------------
      BinaryReader reader = new BinaryReader(File.OpenRead(ofd.FileName));

      // 座標データ1つあたりのサイズ【座標データの形式によって変える】
      const long dataPerRow = sizeof(long) + sizeof(float) + sizeof(float);

      // 座標データ数
      long recordCount = reader.BaseStream.Length / dataPerRow;

      // ----------------------------------------------------------------
      // [4] トラック内のトラックイベントがある期間のトラックモーションを読み込む
      // ----------------------------------------------------------------
      // ビデオトラックのトラックモーションをクリアする
      track.TrackMotion.MotionKeyframes.Clear();

      // 座標データの間引き【お好みで変える】
      const long skipCount = 3;                           // 間引き数
      long skip = 0;                                      // 間引き数カウンタ

      // 最後のトラックイベントの情報
      double farthestEnd = FindLastEventEnd(track);       // 終了時刻[ms]

      // 直近のトラックイベントの情報
      double nearestStart = 0;                            // 開始時刻[ms]
      double nearestEnd = 0;                              // 終了時刻[ms]

      // 座標データ数だけ以下を繰り返す
      for (long i = 0; i < recordCount; i++)
      {
        // 座標データの読み込み 【座標データの形式によって変える】
        double nt = (double)(reader.ReadInt64() / 10000); // 時間変換(決め打ち: 1tick = 100ns から msに変換)
        double nx = 1920 * (reader.ReadSingle() - 0.5f);  // 座標変換(決め打ち: [0,1]を[-960, 960]に変換)
        double ny = 1080 * (0.5f - reader.ReadSingle());  // 座標変換(決め打ち: [0,1]を[540,-540]に変換)

        // [4-A] 現在時刻ntが最後のトラックイベントの終了時刻を超えた時
        if (nt > farthestEnd)
        {
          // 座標データを読み込む必要がないため、ループを抜ける。
          break;
        }

        // [4-B] 現在時刻ntが直近のトラックイベントの範囲内の時
        if (nt >= nearestStart && nt <= nearestEnd)
        {
          // データを間引く
          skip--;
          if (skip <= 0) {
            skip = skipCount;

            // キーフレームを追加し、座標をセットする
            TrackMotionKeyframe frame = track.TrackMotion.InsertMotionKeyframe(new Timecode(nt));
            frame.PositionX = nx;
            frame.PositionY = ny;
          }
        }

        // [4-C] 現在時刻が、直近のトラックイベントの終了時刻を超えた時
        if (nt > nearestEnd)
        {
          // 間引きカウンタリセット
          skip = 0;

          // 直近のトラックイベントの開始・終了時刻を更新する
          nearestStart = FindNearestEventStart(track, nt);
          nearestEnd = FindNearestEventEnd(track, nt);
        }
      }

      // ----------------------------------------------------------------
      // [5] 後処理…ストリームを閉じるだけ。
      // ----------------------------------------------------------------
      reader.Close();
    }

    VideoTrack FindSelectedTrack(Tracks tracks)
    {
      foreach (Track track in tracks)
      {
        if (track.Selected && track.IsVideo())
        {
          return (VideoTrack)track;
        }
      }
      return null;
    }

    // 以下あまり賢くないつくりの関数

    // ----------------------------------------------------------------
    // currentで与えた時刻に一番近いトラックイベントの開始時刻を取得
    // ----------------------------------------------------------------
    double FindNearestEventStart(Track track, double current) 
    {
      double start = Double.MaxValue;
      foreach (TrackEvent evt in track.Events)
      {
        double evtStart = evt.Start.ToMilliseconds();
        if ( evtStart > current && evtStart < start )
        {
          start = evtStart;
        }
      }
      return start;
    }

    // ----------------------------------------------------------------
    // currentで与えた時刻に一番近いトラックイベントの終了時刻を取得
    // ----------------------------------------------------------------
    double FindNearestEventEnd(Track track, double current)
    {
      double end = Double.MaxValue;
      foreach (TrackEvent evt in track.Events)
      {
        double evtEnd = evt.End.ToMilliseconds();
        if (evtEnd > current && evtEnd < end)
        {
          end = evtEnd;
        }
      }
      return end;
    }

    // ----------------------------------------------------------------
    // 最後のトラックイベントの終了時刻を取得
    // ----------------------------------------------------------------
    double FindLastEventEnd(Track track)
    {
      double end = 0;
      foreach (TrackEvent evt in track.Events)
      {
        double evtEnd = evt.End.ToMilliseconds();
        if (evtEnd > end)
        {
          end = evtEnd;
        }
      }
      return end;
    }
  }
}
``` 

# まとめ
動画編集ソフトのVEGAS Proは、C#でスクリプトをを作成・デバッグできる。スクリプトからは`Vegas`クラスのオブジェクトを操作することで、UIでの操作と同じ感覚でVEGASを操作することができる。スクリプトにより、VEGASに元々存在しなかった「座標データを読み込んでトラックモーションに反映する」機能を、ほんの200行ほどで作成することができた。

スクリプトを使えば、解説動画に不可欠な字幕が作りやすくなるかもしれないので、Youtuberに限らず動画を作る人は、高いツールが無いからと手作業で頑張っているところがスクリプトで実現できないか考えてみると良いかと思う。