AviUtl の拡張編集スクリプトでインターレース解除

# はじめに
インターレース及びインターレース解除の概要については，以下の動画がわかりやすい．

- [【VOICEROID解説】茜ちゃんが教えるインターレース解除【レトロゲーム機編】](https://www.nicovideo.jp/watch/sm33492662)
- [【VOICEROID解説】茜ちゃんが教えるインターレース解除【次世代機編】](https://www.nicovideo.jp/watch/sm33546392)

上記の動画では AviUtl では 60 fps でのインターレース解除ができないとされているが，本記事では，60 fps でのインターレース解除を実現する拡張編集スクリプトを書いてみる．本記事で作ったスクリプトは，インターレース解除以外のスクリプトとともに [GitHub で公開](https://github.com/cycloawaodorin/ksa) しているので，適宜参照，利用していただきたい．

## 背景と目的
上記の動画ではちゃんとしたインターレースの解除法が解説されているが，ぶっちゃけ AviUtl 以外のツールを使うのも，中間ファイルを必要とするのも面倒である．本記事では，まともな動き適応型のインターレース解除は実装しないが，単純なフィールド補間によるインターレース解除をやるだけでも，インターレース解除をしないとか，30 fps でインターレース解除をしてしまうとかよりもよほど見栄えがいいので，60 fps でのインターレース解除の敷居を下げたいというのが大きな目標である．

結構編集を頑張っているレトロゲームの実況動画で，インターレース解除されていないものを見るたびにとても残念な気持ちになるので，AviUtl で編集する場合のお手軽版 60 fps インターレース解除法として，本記事のスクリプトを使っていただけると幸いである．

なお，次の「使い方」の章より後は，スクリプト作成のための技術的なお話なので，使うだけなら読む必要はない．

## なぜスクリプトなのか
フィルタプラグインではフレームレートを上げることができず(多分)，インターレース解除フィルタはフィルタオブジェクトにできない[^1]．倍のフレームレートで読み込む入力プラグインとセットで使えばいいのかもしれないが，それも面倒なので，拡張編集とセットで使うことを前提にしたほうが早いと判断した．

[^1]: 通常のフィルタプラグインとしてインターレース解除フィルタを作ればよいが．

シーンを多用すると見通しが悪くなるという都合からも，カスタムオブジェクトかアニメーション効果としてインターレース解除ができると，ある程度便利であろうとの考えもある．

# 使い方
## 概要
スクリプトのインストールは [リサイズスクリプトの記事](https://qiita.com/cycloawaodorin/items/b76a3296d2f9b7dc2d62#%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB) を参照．

インターレース解除スクリプトを使うときは，拡張編集プロジェクトを作るときに，フレームレートを入力動画の 2 倍にして作成しておくことが必要である．具体的には，インターレースで 59.94 fps の映像を，29.97 fps であるかのように録画されたソース動画をインターレース解除したい場合，拡張編集プロジェクトを 59.94 fps で作成しておく．

フレームレートは後から修正できないので，29.97 fps で作成したプロジェクトの動画オブジェクトをインターレース解除したい場合，プロジェクトを新しく作り直す必要がある．

## パラメータ
トラックバーパラメータの「解除法」の数値で，以下のように解除アルゴリズムを選択できる．

|数値|アルゴリズム|
|:---|:---|
|`0`|インターレース解除なし|
|`1`|240p 用の補間なしの解除|
|`2`|空間方向補間 (フィールド補間，bob)|
|`3`|時間方向補間|
|`4`|時間方向補間 with 二重化|

「TFF/BFF」は`0`ならトップフィールドファースト，`1`ならボトムフィールドファーストとしてインターレース解除する．

「スレッド数」はマルチスレッド処理時のスレッド数，ファイル選択ダイアログでは入力動画ファイルを指定する．

## その他の注意点
カスタムオブジェクトでは，オブジェクトの長さが入力ファイルの長さによって制限されず，開始位置を設定できないので，動画オブジェクトのような自然な分割ができない．自然な分割や倍速等ができないのはアニメーション効果でも同様である．

これらの操作のためには，インターレース解除だけやるシーンを用意し，そのシーンに対して分割や倍速処理を行うとよい．シーン内部では，オブジェクトの長さが自動的にファイルの長さになる動画オブジェクトに対し，アニメーション効果でインターレース解除するのが便利と思われる．

また，アニメーション効果では，パラメータのファイルの画像で上書きされ，もとのオブジェクトは無視されるので注意が必要である．

# 各種解除法とその実装
## 実装にあたっての前提
### フレーム画像の取得
使い方の概要でも説明したように，入力動画の 2 倍のフレームレートでプロジェクトが作成されていることを前提とし，それぞれのフレームを入力動画から作成する．ソース画像を得るためには `obj.load("movie", filename, time)` を使うのだが，少々癖があるので，説明する．

まずファイル名だが，動画オブジェクトへのアニメーション効果として実装することを考えたのだが，エフェクトの対象オブジェクトの入力ファイル名を取得する手段がどうやら存在しないようなので，エフェクトの変数として入力する必要がある．そのため，もとのオブジェクトについては，オブジェクトの長さ以外の情報は無視することになる．そのため，カスタムオブジェクトとして実装するほうが素直ではあるが，動画オブジェクトの長さをそのまま使えるのも便利であるため，両方入れてある．

時間については，現在のフレームの画像を得るには `(obj.frame-0.5)/obj.framerate` とする．`obj.frame/obj.framerate` ではどうも 1 フレームずれた画像が入ってくるようなので，`-0.5` を入れている．インターレース解除のために，前後のフレームの画像が必要になるため，`obj.time` を使うより，`obj.frame` ベースのほうが一貫性を持って記述できるだろう．

### フィールドオーダーと補間すべきフィールド
行番号やフレーム番号を 0 オリジンで記述[^2]すると，入力の 2 倍のフレームレートでプロジェクトを作ると，プロジェクト基準で $2n$ 番目のフレームと $2n+1$ 番目のフレームは，ともに入力動画基準で $n$ 番目のフレームとなる．このとき，フィールドオーダーと補間すべきフィールドの関係は次のようになる．

[^2]: この場合，トップフィールドが偶数フィールド，ボトムフィールドが奇数フィールドとなり，1 オリジンで記述した場合と逆になる．

|フィールドオーダー|$2n$ 番フレーム|$2n+1$ 番フレーム|
|:-:|:-:|:-:|
|**TFF**|ボトムフィールド|トップフィールド|
|**BFF**|トップフィールド|ボトムフィールド|

それぞれ，表と逆のフィールドは，そのまま保持すべきフィールドということになる．ただし，補間の不完全さを平滑化でごまかすために，保持すべきフィールドの画素値を変更する場合もある．

## 240p のインターレース解除
240p の疑似インターレース映像のインターレース解除は [レトロゲーム機編の動画](https://www.nicovideo.jp/watch/sm33492662) で説明のある通り，トップフィールドとボトムフィールドの区別がないため，補間すべき画素値を対応する逆のフィールドの画素値と同じにしてやればよい．具体的なコードは以下のようになる．

```C++:240pのインターレース解除
for (int y=bottom; y<height; y+=2) {
	std::memcpy(data+(y*width), data+((y+1)*width), sizeof(PIXEL_BGRA)*width);
}
```

ここで，トップフィールドを補間するフレームでは `bottom==0`，ボトムフィールドを補間するフレームでは `bottom==1` である．また，`PIXCEL_BGRA` は `obj.getpixeldata("alloc")` で取得する `userdata` を読み書きするための構造体で，以下のように定義している．

```C++:PIXEL_BGRA構造体
using PIXEL_BGRA = struct pixel_bgra {
    alignas(1) unsigned char b; // 青
    alignas(1) unsigned char g; // 緑
    alignas(1) unsigned char r; // 赤
    alignas(1) unsigned char a; // アルファチャンネル
};
```

`data` は `obj.getpixeldata("alloc")` の返り値を C++ の関数に引数として渡して得られる，`PIXEL_BGRA` の列の先頭へのポインタである．

[動画](https://www.nicovideo.jp/watch/sm33492662) では一旦縦 240 ピクセルの画像にしてから縦に引き伸ばすというような説明となっているが，この実装では縦 240 ピクセルという状態は経由せず，補間すべきフィールドをそのまま塗り替えるといった感じになる．

## 空間方向補間
基本的に [次世代機編の動画](https://www.nicovideo.jp/watch/sm33546392) で bob として紹介されている方法と同じである．補間の方法は色々あるが，ここでは [リサイズ](https://qiita.com/cycloawaodorin/items/b76a3296d2f9b7dc2d62#%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB) でも使った，Lanczos3 関数による補間を採用しよう．

リサイズと違い，縦方向のみを計算すればよく，重みも固定なため，予め計算してリテラルで埋め込んでしまってよい．また，インターレース解除が必要なシーンは，常に完全不透明と考えてよいため，重みの合計が 1 になるように予め正規化してからリテラルにするとよいだろう．

```C++:空間方向補間
for (int y=bottom; y<height; y+=2) {
	int start=y-5, end=y+6, skip=0;
	if ( start<0 ) {
		if ( bottom ) {
			skip = -start;
		} else {
			skip = -start+1;
		}
	}
	if ( height<end ) {
		end = height;
	}
	for (int x=0; x<width; x++) {
		float b=0.0f, g=0.0f, r=0.0f;
		for (int sy=start+skip; sy<end; sy+=2) {
			float wy = WEIGHTS[(sy-start)>>1];
			const PIXEL_BGRA *s_px = data+(sy*width+x);
			b += s_px->b*wy;
			g += s_px->g*wy;
			r += s_px->r*wy;
		}
		PIXEL_BGRA *d_px = data+(y*width+x);
		d_px->b = uc_cast(b);
		d_px->g = uc_cast(g);
		d_px->r = uc_cast(r);
	}
}
```

```C++:空間方向補間で使う関数・定数の定義
constexpr float WEIGHTS[] = {
	0.024456521739130432f, -0.1358695652173913f, 0.6114130434782609f,
	0.6114130434782609f, -0.1358695652173913f, 0.024456521739130432f
};

unsigned char
uc_cast(float x)
{
	if ( x < 0.0f || std::isnan(x) ) {
		return static_cast<unsigned char>(0);
	} else if ( 255.0f < x ) {
		return static_cast<unsigned char>(255);
	} else {
		return static_cast<unsigned char>(std::round(x));
	}
}
```

## 時間方向補間
動画でフィールド結合として紹介されている方法と同じ……と言いたいところだが，違う．

「補間すべきフィールド」をその前または後ろのフレームの「保持すべきフィールド」からそのまま持ってくる weave とよばれる方法は，半フレームずれてしまうところがどうにも気になる．そのため，ここでは，前のフレームと後ろのフレームを両方使い，両者から補間する方法を使う．こちらでも Lanczos3 関数による補間を用いてもよいが，あまり離れたフレームの画像を使うのも大変なので，線形補間にしておこう．

```C++:時間方向補間
for (int y=bottom; y<height; y+=2) {
	for (int x=0; x<width; x++) {
		int idx = y*w+x;
		PIXEL_BGRA *px_d = dest+idx; // 現フレーム
		const PIXEL_BGRA *px_p = past+idx, *px_f = future+idx; // 前，後フレーム
		px_d->b = static_cast<unsigned char>( (static_cast<int>(px_p->b)+static_cast<int>(px_f->b))>>1 );
		px_d->g = static_cast<unsigned char>( (static_cast<int>(px_p->g)+static_cast<int>(px_f->g))>>1 );
		px_d->r = static_cast<unsigned char>( (static_cast<int>(px_p->r)+static_cast<int>(px_f->r))>>1 );
	}
}
```

2 点間の線形補間のため，前後のフレームの輝度値の和が奇数の場合，四捨五入だと常に切り上げになり，常に切り捨てにするのと同様にバイアスがかかる．バイアスを除くには乱数を使用するか，奇遇を使うしかないが，ここではとりあえず切り捨てとしておいた．ちなみに，昇格周りの事情はよくわからないので，わかりやすさ重視で全部明示的にキャストしている．

この時間方向補間では，weave と同様，動きのないシーンではきれいにインターレース解除できるが，動きのある部分では，weave と同様にコーミングノイズが残る．

## 時間方向補間 with 二重化
時間方向補間におけるコーミングノイズを除去するには，AviUtl で二重化とよばれているものと同様の処理を施す．これは何をやっているのかというと，トップフィールドからボトムフィールドを，ボトムフィールドからトップフィールドをそれぞれ空間方向補完し，両者の平均値によって全体を置き換えることである．

weave + 二重化では，保持すべきフィールドと補間すべきフィールドの重みが同等のため，両フィールドとも同じ濃さのゴーストとなるが，時間方向補間 + 二重化では，現フレームの重みが $1/2$，前後フレームの重みがそれぞれ $1/4$ となるため，現フレームのゴーストが，前後フレームのそれより濃くなる．他方，ゴーストが増えるため，weave + 二重化以上にボケ感は強くなる傾向がある．

# 高度なアルゴリズム
「動き適応型」では動きの有無に合わせて適応的に解除するとされるが，そこまで単純ではない．前後フレームの画素値の近さで空間方向と時間方向を選択すればそれなりのものになると思って実装してみたが，全くうまく行かなかった．そもそも，空間方向補間できれいに解除できないのは，縦 240 ピクセルでは，縦方向のナイキスト周波数が，480p の画像が持つ縦方向の最大周波数を下回ってしまっているためである．そのため，本質的に失われている情報を，何らかの方法で補う必要があるのだ．

動き適応型のアルゴリズムでは，カメラやオブジェクトの動きを計算することで，失われた位置の色を，別のフレームに写っている，対応する位置の色によって推定する，ということを行う．当然ちょうどのピクセルがあることは稀なので，補間も同時に行うことになる．オブジェクトをどの程度の複雑さや数で切り出すか，動きをどの程度細かく推定するかなどにより，計算量やインターレース解除の正確さが変わってくるものと思われる．単純に実装できる範囲でなんとかできないかなと思ったが，似た計算の経験もないので，少なくともすぐには無理そうだ．