はじめに
Halideとは画像処理とその高速化に特化したドメイン固有言語で,C++に組み込まれて使用します.詳しい説明は過去記事に詳しく(詳しくないかもしれん)あるので,そちらを参照してください.乱暴に説明すれば,「いい感じに高速な画像処理が簡単にできる言語」です.この記事の内容は公式チュートリアルの内容を終えていれば一応理解は大丈夫だと思います.
Aviutlは言わずもがなWindowsの定番動画編集ソフトです.今更説明不要かと思いますが,ユーザがプラグインを任意に追加することで機能拡張ができ,SDKが公開されているためプラグインの自作も簡単に可能です.
このAviutlですが,老舗ソフト故かもっさりした処理部分がいくつかあります.その高速化を簡単にできないかとHalideを使ってみたところ,動くことが確認できたので,備忘録として記事にしました.
目標
とりあえず,提供されているSDKのサンプル(video_filter)をHalide実装にて実現することを目標にします(aufファイルをつくる).
もう少し具体的に,つくる機能は設定ウィンドウに表示されるトラックバーの値を,そのまま画素値に足しこむだけの機能になります.
Aviutlのプラグイン作成
プラグインの作成について,AviUtlプラグイン制作入門(作成からビルドまで) - 国道86号線にてサンプルのビルドまでを詳しく紹介されているので,そちらを参考にしました.開発環境は,Visual Studio 2019を使用しました.
上記サイトはaudio_filter.cppをベースにしていますが,今回は画像フィルタを目標にするので,video_filter.cppをベースに説明していきます.また,video_filter.cppについては永遠に工事中にて詳しく説明されています.
以下は今回最低限必要なものをピックアップして説明しています.
フィルタについて
今回はfilter.hとvideo_filter.cppを元にするため,この2つについて軽く説明します.
filter.h(一部抜粋)//---------------------------------------------------------------------------------- // フィルタプラグイン ヘッダーファイル for AviUtl version 0.99k 以降 // By KENくん //---------------------------------------------------------------------------------- // YC構造体 typedef struct { short y; // 画素(輝度 )データ ( 0 ~ 4096 ) short cb; // 画素(色差(青))データ ( -2048 ~ 2048 ) short cr; // 画素(色差(赤))データ ( -2048 ~ 2048 ) // 画素データは範囲外に出ていることがあります // また範囲内に収めなくてもかまいません } PIXEL_YC; // PIXEL構造体 typedef struct { unsigned char b,g,r; // 画素(RGB)データ (0~255) } PIXEL; // フィルタPROC用構造体 typedef struct { int flag; // フィルタのフラグ // FILTER_PROC_INFO_FLAG_INVERT_FIELD_ORDER >: フィールドオーダーを標準と逆に扱う ( 標準はボトム->トップになっています ) // FILTER_PROC_INFO_FLAG_INVERT_INTERLACE >: 解除方法を反転する ( インターレース解除フィルタのみ ) PIXEL_YC *ycp_edit; // 画像データへのポインタ ( ycp_editとycp_tempは入れ替えれます ) PIXEL_YC *ycp_temp; // テンポラリ領域へのポインタ int w,h; // 現在の画像のサイズ ( 画像サイズは変更出来ます ) int max_w,max_h; // 画像領域のサイズ int frame; // 現在のフレーム番号( 番号は0から ) int frame_n; // 総フレーム数 int org_w,org_h; // 元の画像のサイズ short *audiop; // オーディオデータへのポインタ ( オーディオフィルタの時のみ ) // オーディオ形式はPCM16bitです ( 1サンプルは mono = 2byte , stereo = 4byte ) int audio_n; // オーディオサンプルの総数 int audio_ch; // オーディオチャンネル数 PIXEL *pixelp; // 現在は使用されていません void *editp; // エディットハンドル int yc_size; // 画像領域の画素のバイトサイズ int line_size; // 画像領域の幅のバイトサイズ int reserve[8]; // 拡張用に予約されてます } FILTER_PROC_INFO;
>```cpp:video_filter.cpp(一部抜粋)
>//----------------------------------------------------------------------------------
>// サンプルビデオフィルタ(フィルタプラグイン) for AviUtl ver0.99e以降
>//----------------------------------------------------------------------------------
>#include <windows.h>
>
>#include "filter.h"
>
>
>//---------------------------------------------------------------------
>// フィルタ構造体定義
>//---------------------------------------------------------------------
>#define TRACK_N 3 // トラックバーの数
>TCHAR *track_name[] = { "track0", "track1", "track2" }; // トラックバーの名前
>int track_default[] = { 0, 0, 0 }; // トラックバーの初期値
>int track_s[] = { -999, -999, -999 }; // トラックバーの下限値
>int track_e[] = { +999, +999, +999 }; // トラックバーの上限値
>
>#define CHECK_N 2 // チェックボックスの数
>TCHAR *check_name[] = { "check0", "check1" }; // チェックボックスの名前
>int check_default[] = { 0, 0 }; // チェックボックスの初期値 (値は0か1)
>
>FILTER_DLL filter = {
FILTER_FLAG_EX_INFORMATION, // フィルタのフラグ
// FILTER_FLAG_ALWAYS_ACTIVE : フィルタを常にアクティブにします
// FILTER_FLAG_CONFIG_POPUP : 設定をポップアップメニューにします
// FILTER_FLAG_CONFIG_CHECK : 設定をチェックボックスメニューにします
// FILTER_FLAG_CONFIG_RADIO : 設定をラジオボタンメニューにします
// FILTER_FLAG_EX_DATA : 拡張データを保存出来るようにします。
// FILTER_FLAG_PRIORITY_HIGHEST : フィルタのプライオリティを常に最上位にします
// FILTER_FLAG_PRIORITY_LOWEST : フィルタのプライオリティを常に最下位にします
// FILTER_FLAG_WINDOW_THICKFRAME : サイズ変更可能なウィンドウを作ります
// FILTER_FLAG_WINDOW_SIZE : 設定ウィンドウのサイズを指定出来るようにします
// FILTER_FLAG_DISP_FILTER : 表示フィルタにします
// FILTER_FLAG_EX_INFORMATION : フィルタの拡張情報を設定できるようにします
// FILTER_FLAG_NO_CONFIG : 設定ウィンドウを表示しないようにします
// FILTER_FLAG_AUDIO_FILTER : オーディオフィルタにします
// FILTER_FLAG_RADIO_BUTTON : チェックボックスをラジオボタンにします
// FILTER_FLAG_WINDOW_HSCROLL : 水平スクロールバーを持つウィンドウを作ります
// FILTER_FLAG_WINDOW_VSCROLL : 垂直スクロールバーを持つウィンドウを作ります
// FILTER_FLAG_IMPORT : インポートメニューを作ります
// FILTER_FLAG_EXPORT : エクスポートメニューを作ります
0,0, // 設定ウインドウのサイズ (FILTER_FLAG_WINDOW_SIZEが立っている時に有効)
"サンプルフィルタ", // フィルタの名前
TRACK_N, // トラックバーの数 (0なら名前初期値等もNULLでよい)
track_name, // トラックバーの名前郡へのポインタ
track_default, // トラックバーの初期値郡へのポインタ
track_s,track_e, // トラックバーの数値の下限上限 (NULLなら全て0~256)
CHECK_N, // チェックボックスの数 (0なら名前初期値等もNULLでよい)
check_name, // チェックボックスの名前郡へのポインタ
check_default, // チェックボックスの初期値郡へのポインタ
func_proc, // フィルタ処理関数へのポインタ (NULLなら呼ばれません)
NULL, // 開始時に呼ばれる関数へのポインタ (NULLなら呼ばれません)
NULL, // 終了時に呼ばれる関数へのポインタ (NULLなら呼ばれません)
NULL, // 設定が変更されたときに呼ばれる関数へのポインタ (NULLなら呼ばれません)
NULL, // 設定ウィンドウにウィンドウメッセージが来た時に呼ばれる関数へのポインタ (NULLなら呼ばれません)
NULL,NULL, // システムで使いますので使用しないでください
NULL, // 拡張データ領域へのポインタ (FILTER_FLAG_EX_DATAが立っている時に有効)
NULL, // 拡張データサイズ (FILTER_FLAG_EX_DATAが立っている時に有効)
"サンプルフィルタ version 0.06 by KENくん",
// フィルタ情報へのポインタ (FILTER_FLAG_EX_INFORMATIONが立っている時に有効)
NULL, // セーブが開始される直前に呼ばれる関数へのポインタ (NULLなら呼ばれません)
NULL, // セーブが終了した直前に呼ばれる関数へのポインタ (NULLなら呼ばれません)
};
いきなり長々とコードを載せましたが,今回重要なのはほんの一部です.
まず,filter.hの方から,画素値データを扱うPIXEL_YC
構造体と,フィルタ関数にパラメータを渡すためのFILTER_PROC_INFO
構造体が定義されています.各メンバが持つ意味は,コメント部分を見てもらえば大丈夫かと思うので省略します.構造体からAviutlで扱う画像データはAoS形式(Halideチュートリアル的に言えばInterleaved形式)で,YCbCrフォーマットになっていることが分かります.
次に,video_filter.cppでは,フィルタDLL用構造体FILTER_DLL
のオブジェクトfilter
が定義されています(こいつがフィルターになる).このFILTER_DLL
のメンバであるfunc_proc
がフィルタ処理を行う関数になり,今回はこの関数について処理を記述します.
フィルタ処理関数について
video_filter.cpp(一部抜粋)
//---------------------------------------------------------------------
// フィルタ処理関数
//---------------------------------------------------------------------
BOOL func_proc( FILTER *fp,FILTER_PROC_INFO *fpip )
{
//
// fp->track[n] : トラックバーの数値
// fp->check[n] : チェックボックスの数値
// fpip->w : 実際の画像の横幅
// fpip->h : 実際の画像の縦幅
// fpip->max_w : 画像領域の横幅
// fpip->max_h : 画像領域の縦幅
// fpip->ycp_edit : 画像領域へのポインタ
// fpip->ycp_temp : テンポラリ領域へのポインタ
// fpip->ycp_edit[n].y : 画素(輝度 )データ ( 0 ~ 4096 )
// fpip->ycp_edit[n].cb : 画素(色差(青))データ ( -2048 ~ 2048 )
// fpip->ycp_edit[n].cr : 画素(色差(赤))データ ( -2048 ~ 2048 )
//
// 画素データは範囲外に出ていることがあります。
// また範囲内に収めなくてもかまいません。
//
// 画像サイズを変えたいときは fpip->w や fpip->h を変えます。
//
// テンポラリ領域に処理した画像を格納したいときは
// fpip->ycp_edit と fpip->ycp_temp を入れ替えます。
//
int x,y;
PIXEL_YC *ycp,*ycp2;
// 各要素にトラックバーの値を足し込む
for(y=0;y<fpip->h;y++) {
ycp = fpip->ycp_edit + y*fpip->max_w;
for(x=0;x<fpip->w;x++) {
ycp->y += (short)fp->track[0];
ycp->cb += (short)fp->track[1];
ycp->cr += (short)fp->track[2];
ycp++;
}
}
// check0がチェックされていたら横方向を1/2に縮小
if( fp->check[0] ) {
for(y=0;y<fpip->h;y++) {
ycp = fpip->ycp_edit + y*fpip->max_w;
ycp2 = fpip->ycp_temp + y*fpip->max_w;
for(x=0;x<fpip->w/2;x++) {
ycp2->y = (short)(( ycp[0].y + ycp[1].y )/2);
ycp2->cb = (short)(( ycp[0].cb + ycp[1].cb )/2);
ycp2->cr = (short)(( ycp[0].cr + ycp[1].cr )/2);
ycp+=2;
ycp2++;
}
}
// 幅を半分にしたので fpip->w を半分にする
fpip->w /= 2;
// fpip->ycp_temp に処理したデータを書き込んだので
// fpip->ycp_edit と fpip->ycp_temp を入れ替える
ycp = fpip->ycp_edit;
fpip->ycp_edit = fpip->ycp_temp;
fpip->ycp_temp = ycp;
}
// check1がチェックされていたら画像を1/2に縮小したものを中央に半透明で表示
if( fp->check[1] ) {
// fpip->ycp_temp に fpip->ycp_edit を1/2に縮小したものを作成
fp->exfunc->resize_yc(fpip->ycp_temp,fpip->w/2,fpip->h/2,fpip->ycp_edit,0,0,fpip->w,fpip->h);
// fpip->ycp_temp を fpip->ycp_edit の中央に半透明で描画
fp->exfunc->copy_yc(fpip->ycp_edit,fpip->w/4,fpip->w/4,fpip->ycp_temp,0,0,fpip->w/2,fpip->h/2,2048);
}
return TRUE;
}
上記がフィルタ処理を担う関数になります.今回は前半部分の各要素にトラックバーの値を足し込む部分だけを実装したので,他は見なくて大丈夫ですw
ここで引っかかったポイントとして,
* `fpip->ycp_edit`と`fpip->ycp_temp`の扱い
`fpip->ycp_edit`は現在のフレーム画像へのポインタになっていて,処理前は入力画像であり,最終的に処理後の画像へのポインタになる必要があります.また,`fpip->ycp_temp`はテンポラリ領域とのことで,画像領域と同じサイズの領域(?)へのポインタにっていて,はじめは真っ黒な画像になってます.`fpip->ycp_edit`と`fpip->ycp_temp`は入れ替えが可能であるため,「`fpip->ycp_edit`を入力,`fpip->ycp_temp`を出力にして処理」を行い,「`fpip->ycp_edit`と`fpip->ycp_temp`を入れ替え」を行うことで,フィルタリングが行えます(サンプルでは直接`fpip->ycp_edit`に出力していますが,Halideを使った実装のときは`fpip->ycp_temp`を使った方法で行います).
* 画像領域サイズについて
yループ内にて`ycp = fpip->ycp_edit + y*fpip->max_w;`になっているところがポイントです.yループは0から`fpip->w`のスキャンを行いますが,先の部分からy軸のストライドが`fpip->max_w`になっています.つまり,実際に処理を行う画像サイズは`fpip->w`×`fpip->h`であるのに対し,確保されている画像領域は`fpip->max_w`×`fpip->max_h`のより大きいサイズになっています.
# Halideでスタティックライブラリを作る
Halideでは,作成した処理をスタティックライブラリ化できます.色々やり方はありますが,今回は[チュートリアル・レッスン15-1](https://halide-lang.org/tutorials/tutorial_lesson_15_generators.html),[チュートリアル・レッスン15-2](https://halide-lang.org/tutorials/tutorial_lesson_15_generators_usage.html)にあるgeneratorを用いた方法で作成しました.また,Halideにはある程度適したスケジューリングを自動で適用してくれるAuto Schedulerがあり,generatorでも使用できます.ここらへんは[チュートリアル・レッスン21-1](https://halide-lang.org/tutorials/tutorial_lesson_21_auto_scheduler_generate.html),[チュートリアル・レッスン21-2](https://halide-lang.org/tutorials/tutorial_lesson_21_auto_scheduler_run.html)に詳しく載っています.
## generator作成
```cpp:generator.h
#pragma once
#include <Halide.h>
#include <stdio.h>
#pragma comment(lib,"Halide.lib")
using namespace Halide;
class MyFilter : public Halide::Generator<MyFilter> {
public:
Input<Buffer<short>> input_YCbCr{ "input", 3 };
Input<Buffer<int>> track{ "track",1 };
Output<Buffer<short>> output_YCbrCr{ "output", 3 };
void generate() {
output_YCbrCr(c, x, y) = saturating_cast<short>(input_YCbCr(c, x, y) + track(c));
}
void schedule() {
if (auto_schedule)
{
input_YCbCr.set_estimates({ {0,3}, { 0,1080 }, {0,1920} });
output_YCbrCr.set_estimates({ {0,3}, { 0,1080 }, {0,1920} });
track.set_estimates({ {0,3} });
}
else
{
}
}
private:
Var x{ "x" }, y{ "y" }, c{ "c" };
};
HALIDE_REGISTER_GENERATOR(MyFilter, my_filter)
generatorについてはチュートリアルにて詳しく説明されていますが,ざっくりとvoid generator()
で処理を記述し,void schedule()
でスケジューリングを記述します.Auto Schedulerを使用する場合は,Scheduleの探索ができるように,各変数がどれくらいの領域を走査するかset_estimates
で教えてあげるだけでOKです.
入力画像はInput<Buffer<short>>
で,トラックバー値はInput<Buffer<int>>
で受け取り,それぞれ3次元と1次元のバッファです.出力はOutput<Buffer<short>>
で,こちらも3次元バッファです.処理自体はシンプルで,input_YCbCrにtrackを足し合わせるだけです.
また,先述の通りAviutlでは画像データはInterleaved型であるので,チャネルループが最内側になるように注意します(make_interleaved的な機能もありますが,実際やっていることはこの手法とおなじっぽい).
このヘッダファイルを,Halideに付属するToolkitにあるGenGen.cppにインクルードし,"generator.exe"をビルドします.
こいつを実行することで,実際にスタティックライブラリを生成します.
> .\generator.exe -o . -g filter -p <path/to/autoschedule_adams2019.dll> -s Adams2019 target=x86-32-windows-avx-avx2-f16c-fma-sse41 auto_schedule=true
generate_schedule for target=x86-32-windows-avx-avx2-f16c-fma-sse41
Pass 0 of 5, cost: 6.0193, time (ms): 2
Pass 1 of 5, cost: 6.0193, time (ms): 0
Pass 2 of 5, cost: 6.0193, time (ms): 0
Pass 3 of 5, cost: 6.0193, time (ms): 0
Pass 4 of 5, cost: 6.0193, time (ms): 0
Best cost: 6.0193
Cache (block) hits: 160
Cache (block) misses: 40
Auto Schedulerを使用する場合には,-pでスケジューラライブラリのdllファイルを指定し,さらに-sで使用するスケジューラを指定します.今回はHalideに付属する"Adams2019"を使用しました.(他のオプションはヘルプ見るなりチュートリアル見るなりしてください..)
また,target=以下に使用する計算機環境を設定でき,ここは各々好きに設定してください.ただし,Aviutlは32ビットアプリケーションなので,必ずgenerator側でも32ビットをターゲットに指定してください.
実行するとスケジューリング探索が走って,my_filter.hとmy_filter.libができているはずです.
プラグインの作成
サンプルのvideo_filter.cppについて,フィルタ処理関数をHalideライブラリを使用した形に書き換えます.
事前にHalide.hと作成したmy_filter.hをインクルードしておいてください.
//---------------------------------------------------------------------
// フィルタ処理関数
//---------------------------------------------------------------------
BOOL func_proc(FILTER* fp, FILTER_PROC_INFO* fpip)
{
Halide::Runtime::Buffer<short> inputBuff(halide_type_of<short>(), fpip->ycp_edit, 3, fpip->max_w, fpip->max_h);
Halide::Runtime::Buffer<short> outputBuff(halide_type_of<short>(), fpip->ycp_temp, 3, fpip->max_w, fpip->max_h);
Halide::Runtime::Buffer<int> trackBuff(halide_type_of<int>(), fp->track, 3);
my_filter(inputBuff, trackBuff, outputBuff); // フィルタリング
// ycp_editとycp_tempの入れ替え
PIXEL_YC* ycp = fpip->ycp_edit;
fpip->ycp_edit = fpip->ycp_temp;
fpip->ycp_temp = ycp;
return TRUE;
}
作製したmy_filterにはgeneratorで宣言したとおりHalide::Runtime::Buffer
を使用して値を渡します.
先述の通り,表示領域と画像領域の違いから,画像入力・出力バッファどちらも3 × max_w × max_hで確保してください.
作製したmy_filter.libとHalide.libのリンクを行い,ビルドして完成です.また,対象が32ビットなので,使用するHalide.libも32ビットの方にしといてください.
実行
作製したプラグインのaufファイルをAviutlの適切なフォルダに配置すれば,フィルタが使用できるはずです.
また,Aviutl.exeがあるフォルダに32ビット版のHalide.dllを配置するのを忘れずに.
テスト
おわりに
今回は実験的に超簡単な実装のみだったので,オリジナルとの性能比較などはテストしていません(元々軽い処理だった).
なので,Halideを使うことで実際にどれだけ高速化が行えるのかはまだ未知数ですが,処理の記述や高速化の手軽さはかなり上げられるように思います.
また,処理部分が今回の実装ですとmax_w × max_hの領域で行われるので,これはかなり冗長であり,まだまだ課題はありそうです.
最後に,Halideはまだまだマイナーな言語(?)なので,これを機に多くの人に触ってもらえると嬉しいです.
おまけ
Auto Schedulerではgenerator.exe実行時に-e scheduleを指定することで,どのようなスケジュールが適用されたかを確認できます.
今回はこんな感じのスケジュールが適用されたみたいです.
#ifndef my_filter_SCHEDULE_H
#define my_filter_SCHEDULE_H
// MACHINE GENERATED -- DO NOT EDIT
// This schedule was automatically generated by Adams2019
// for target=x86-32-windows-avx-avx2-f16c-fma-sse41 // NOLINT
// with machine_params=16,16777216,40
#include "Halide.h"
inline void apply_schedule_my_filter(
::Halide::Pipeline pipeline,
::Halide::Target target
) {
using ::Halide::Func;
using ::Halide::MemoryType;
using ::Halide::RVar;
using ::Halide::TailStrategy;
using ::Halide::Var;
Func output = pipeline.get_func(2);
Var c(output.get_schedule().dims()[0].var);
Var ci("ci");
Var x(output.get_schedule().dims()[1].var);
Var xi("xi");
Var y(output.get_schedule().dims()[2].var);
Var yi("yi");
output
.split(x, x, xi, 17, TailStrategy::ShiftInwards)
.split(y, y, yi, 480, TailStrategy::ShiftInwards)
.split(c, c, ci, 16, TailStrategy::GuardWithIf)
.vectorize(ci)
.compute_root()
.reorder({ci, c, xi, yi, x, y})
.fuse(x, y, x)
.parallel(x);
}
#endif // my_filter_SCHEDULE_H