OpenCVでプレイ動画の切り出しを自動化した話・GUI・他ソフト連携編

# 記事の目的

[前回](https://qiita.com/mickie895/items/8d77c477d9f094ca9e94)までで切り出し用のアルゴリズムは出来上がった。
今回はGUI面でどうすれば切り出しができるだろうかを考えた話をする。

# (おさらい)作業内容

前回までで、ゲームのポーズがかかっている場所を抜き出すことができるようになった。
次の課題はWindowsアプリとしてどうやって関数を紐付けるか、そしてAviutlを操作するかだ。

# GUI紐付け

今回はOxygen Not Includedのゲーム画面を抜き出すだけだけど、
今後もし同じような動画を作ることになった場合、切り出しの基準となる場所は当然異なってくる。

となると、最も良いのは任意の長方形を切り出し範囲にできることだと思う。
これをWindowsのGUIに落とし込むことを考える。

先に完成品ののGUIを出すが、このようなGUIなら直感的に切り出せると考えた。
![GUI解説.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395414/f8b3fbe0-4612-21eb-43cd-579f45a08a41.png)

正直、動画の表示周りの解説はOpenCVで作業をしようとしているのなら不要だろう。
これを実装するに当たり最も困るのは「ドラッグで切り出し範囲を指定」の部分だと思う。

これを現実的な時間で実装するために、今回はPictureBoxを継承したカスタムコントロールを使用した。

※カスタムコントロールの作り方は[WinFormでボタンを継承したユーザコントロールを作る](https://takumi-oda.com/blog/2018/08/12/post-299/)を参考にした。

## カスタムしたPictureBoxの作成

今回のようにピクチャボックスを表示したい場合、考える必要がある点は2つだけだ。

+ どうやってドラッグ範囲を検出するかと、
+ どうやって表示するかだ。

### ドラッグ範囲の抽出

まずはドラッグ範囲の検出。これはマウスイベントを取得すれば良い。
押された部分を検出するとなると、シンプルに考えるとこうなる。

```c#

        protected override void OnMouseDown(MouseEventArgs e)
        {
            StartPoint = e.Location;
            mousePressing = true;
            base.OnMouseDown(e);
        }

        protected override void OnMouseMove(MouseEventArgs e)
        {
            if (mousePressing)
            {
                EndPoint = e.Location;
                // コントロールの再描画要求
                Invalidate();
            }
            base.OnMouseMove(e);
        }

        protected override void OnMouseUp(MouseEventArgs e)
        {
            if (mousePressing)
            {
                mousePressing = false;
                EndPoint = e.Location;
            }
            base.OnMouseUp(e);
        }

        protected override void OnLeave(EventArgs e)
        {
            mousePressing = false;
            base.OnLeave(e);
        }

```

単純に、ピクチャーボックス上でマウスを押し始めたらマウスを監視し、離れたら監視を終了するだけ。
MouseEventArgsのLocationプロパティはピクチャボックスの左上を0,0としていたため、
ピクチャボックスの位置とかを意識する必要はなかった。
(枠は影響あるかもしれないが動画の画素数が大きかったために致命的な影響は出なかった)

OnMouseUpのif文やOnLeaveは不要なタイミングで入ってきた入力を防ぐだけだ。
これらが無いと、主にファイルを保存するダイアログのボタンをピクチャボックスの真上で押すと、
OnMouseUpだけがピクチャボックス上で発行されることになり、
切り出し範囲の表示がおかしくなる事がある。

これは単にダイアログを表示する前に切り出し位置を確定させてしまえば問題のない話だし、
動画の読み込み直後に位置がずれていてももう一度選択しなおせば良いだけなので、
運用上無くても問題は全くない。

これでドラッグした位置をStartPointとEndPointに格納することができた。

### 切り出し予定位置の表示

これはC++などでWindowsアプリを作った記憶がないと少しややこしいかもしれない。
PictureBoxに限らずWindowsフォームアプリでは、Paintイベントで「必要になったら」描画を行う。
基本的には、他のウィンドウに隠れた部分が出てきたりしたら描画を行う必要があり、Paintイベントを書き換えただけでは意図したタイミングでコントロールの描画を行ってくれない。
そのため今回はマウスを押した状態でPictureBoxの上を動かしたらInvalidate()を呼び出し、再描画を養成した。

あとは非同期処理用のBackGroundWorkerや切り出し判定を確認するためのChartを付け足したりしてるけど、
カスタムなどはしていないので省略。

# Aviutl上の操作

Aviutlで動画のカット編集をする場合、カットしたいフレームを選択範囲として指定した上で、編集メニューの選択範囲を削除を選択すれば削除できる。
Aviutlはツールの制作なども盛んなため、[指定したメニューを起動させるスクリプト](https://aji0.web.fc2.com/index.html)もC言語で書かれている。
これをC#に書き直しても良かったが、選択範囲指定用のウィンドウの調査などが面倒だったので、キーコードを原始的に送る形にした。

## Aviutlの準備

Aviutlはほぼすべてのメニューに対してショートカットキーを紐付けることができる。
今回は選択範囲の指定、選択範囲を削除にCtrl+Shift+EとDを指定した。
![Aviutlショートカット.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395414/a3a37961-63ec-4655-0ed5-3be0ba4366c0.png)

自分の環境で選択範囲の指定を選択したとき、次のような画面が出る。
![Aviutlショートカット2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395414/d33fa498-1055-5665-628e-0f00b51adc8c.png)

Tabキー2回で選択開始フレームのテキストボックスにフォーカスが合い、テキストが全選択されている状態だったためキーコードを送信すれば選択開始フレームは編集できた。
同様に選択終了フレームも編集できる。

## キーコードの送信

当然の話だが、動画の頭をカットしたとき、カットした後の再生位置の動画は前に詰める。要は普通に順番にカットしようとしたら2つ目以降の選択範囲がずれてしまう。
今回は計算が面倒だったので、後ろから順番にカット編集することにした。

ファイルを読み込んで、カットする範囲を後ろからになるように調整してからSendKeys.SendWait()でキーコードを送ったのが下記の部分。
キーに対応する文字を確認するのには[英語のリファレンス](https://docs.microsoft.com/en-us/dotnet/api/system.windows.forms.sendkeys.send?view=netcore-3.1)を参照する必要があった。
(日本語のリファレンスは機械翻訳なせいでまともな情報が見れなかった)

``` C#

            List<string[]> inputFrames = new List<string[]>();
            using (StreamReader reader = new StreamReader(openKeyDialog.FileName))
            {
                string line;
                while((line = reader.ReadLine()) != null){
                    inputFrames.Insert(0, line.Split(','));
                }
            }

            foreach (string[] keyFramesPair in inputFrames)
            {
                SendKeys.SendWait("+^E");
                System.Threading.Thread.Sleep(10);
                SendKeys.SendWait("{TAB}{TAB}");
                System.Threading.Thread.Sleep(10);
                SendKeys.SendWait(keyFramesPair[0]);
                System.Threading.Thread.Sleep(10);
                SendKeys.SendWait("{TAB}");
                System.Threading.Thread.Sleep(10);
                SendKeys.SendWait(keyFramesPair[1]);
                System.Threading.Thread.Sleep(10);
                SendKeys.SendWait("~");
                System.Threading.Thread.Sleep(10);
                SendKeys.SendWait("+^D");
                System.Threading.Thread.Sleep(10);
            }

            MessageBox.Show("完了");
```

Ctrl+Shift+E→Tab2回→選択開始の位置を入力→Tab一回→選択終了の位置を入力→Enter→Ctrl+Shift+Dで一箇所カットできるので、
それを入力された行数だけ繰り返した。

どう考えてもSleep(10)はお行儀が良い処理ではないが、止める時間がたかが知れているのと、入力同士の間隔をある程度開けないとAviutlの処理が間に合わないことから今回は原始的にsleepさせる事になった。

# 最後に

https://github.com/mickie895/ONI_Movie_Cutter
今回作成したソースは上記にまとめました。

今回、動画の編集作業を自動化させることにより、手作業で5時間かかった動画作成の前準備を拘束時間1分に短縮させることができた。
これをきっかけに動画の編集処理などを自動化させられないか考える人が増えたら幸いである。