2013年7月31日水曜日

VapourSource

Avisytnh2.6用プラグインの新作を公開。

VapourSource-20130731.zip
https://github.com/chikuzen/VapourSource/

VapourSourceはVSScriptの使い方を調べるために書いてみたAvisynthプラグインです。
VapourSynth script(r19以降)をAvisynthで読み込みます。

書き始めたのはr19のtest1のリリース直後でしたが、r19の正式公開までに二度のAPIブレークがありました。
やっぱこういうのって、実際に色々使ってみないと足りないところとかわからないものだよね。

vsrawsource その4

更新

vsrawsource-0.3.2.zip
https://github.com/chikuzen/vsrawsource/

* アルファ付ソース読み込み時のメモリリーク修正
* ファイル名に非ASCII文字が含まれる場合の対応

vsimagereader その2

VS r19にあわせて更新

vsimagereader 0.2.1.zip
https://github.com/chikuzen/vsimagereader/

* アルファ読み込み時のメモリリーク修正
* ファイル名に非ASCII文字を含む場合の対応
* libpngを1.6.2に更新

2013年7月21日日曜日

MotionMask

動画エンコードに手を出したばかりの頃「動き適応」(motion adaptive)という言葉によく頭を悩ませたものだった。

当時はAvisynth覚えたてでプラグイン漁りに夢中になっていたわけだが、しょっちゅう「動き適応ノイズ除去」とか「動き適応デインタレース」などと言う言葉に出くわすのである。
それまで筆者は「動き適応」なんていう珍妙な日本語は聞いたことがなかったので、一体どういう処理をしており、どういうメリットが有るのかもさっぱりわからなかった。
「動き適応」でググってみれば色々とそれらしき解説もあるにはあるが、読んでみても理解できないのである。

それでも長いこと続けていれば次第に知識や経験は増えていくわけで、かつては理解不能だったこともわかるようになっていくわけだが、「動き適応」を理解し処理内容をイメージできるようになったのは、プログラミングに手を出してデジタル映像の処理自体を稚拙ながらも自分で書くようになってからだった。

さて「動き適応」であるが、これは映像を動いている部分と動いていない部分に分け、それぞれに別の処理を行う(もしくは一方だけに処理を行う)ことを意味する。
そして、映像における動きとは、前か後ろ(もしくは両方)のフレームとの差分のことを意味する。
より具体的に言えば、あるフレームとその一つ前のフレームで引き算を行い、同一座標における二つのサンプルの差の絶対値を求める。そして、その値が閾値を超えていれば動いているとみなし、それ以外は静止しているとみなす。

この処理をavsで書くとすれば次のようになる。
threshold = 10 # 動き判定用閾値 前フレームとの差が10未満のサンプルは静止しているとみなす

src = something # ソースクリップ
prev = src.Loop(2, 0, 0) # ソースを先頭フレームだけ重複させ1フレームずらしたもの
diff = src.mt_lutxy(prev, "x y - abs", chroma="process") # srcとprevの各サンプルの差の絶対値を求める
mask = diff.mt_binarize(threshold, chroma="process") # 閾値で二値化(閾値未満は0、以上は255)

StackVertical(StackHorizontal(src.Subtitle("src"), prev.Subtitle("prev")),
\             StackHorizontal(diff.Subtitle("diff"), mask.Subtitle("mask")))
上記スクリプトのmaskクリップを一般にモーションマスクと呼ぶ(ちなみにmt_motion()は、上記の処理を行うための専用関数である)。

さて、このモーションマスクを使って「動き適応ぼかし」を行なってみよう。
動き適応ぼかしは静止している部分には手を加えず動きのある部分だけをぼかすことで、あまり劣化を目立たせずにエンコード時の圧縮率を稼ぐという、Xvid全盛期頃にはよく行われた処理である。
src = something
blur = src.Blur(1.0).Blur(1.0)
mask = src.mt_motion(10, chroma="process")
last = src.mt_merge(blur, mask)

StackVertical(StackHorizontal(src.Subtitle("src"), blur.Subtitle("blur")),
\             StackHorizontal(mask.Subtitle("mask"), last.Subtitle("last")))

モーションマスクによって、動きのない部分はsrcクリップ、動きのある部分はblurクリップになるように合成されているのがわかると思う。

Avisynthには、masktools2のmt_motion()の他にmvtools2のMMask()もある、
こちらはmt_motion()のような単純なモーションマスクではなくモーションベクトルの長さをマスクとして利用するもので、アルゴリズム的には優れているが計算量は半端無く跳ね上がるし、実装も難しい。
また、素材や行いたい処理によっては単純なフレーム間差分のほうが良い場合もあるので、使い分けは結構重要である。

2013年7月14日日曜日

LutとLut2を改良した

VapourSynthのissueリストにこんなのがありました。
http://code.google.com/p/vapoursynth/issues/detail?id=52
「LutとLut2なんだけど、いちいちlook up tableを作ってから渡すのってメンドイから、生成用関数渡したらフィルタ側で作るようにしたいんだけど、誰かやらんかね?」

たしかにあれはめんどくさい。
特にLut2の場合は二重ループ必須だから、余計に書くのがめんどくさい。
というわけで、パッチ書いて送ったのが昨日mergeされました。

core.std.Lut(clip:clip;planes:int[];lut:int[]:opt;function:func:opt;)
core.std.Lut2(clips:clip[];planes:int[];lut:int[]:opt;function:func:opt;bits:int:opt;)

例:クリップのすべてのYの値を50下げたい場合。
clip = something

#これまでの書き方
lut = [max(x - 50, 0) for x in range(2 ** clip.format.bits_per_sample)]
clip = core.std.Lut(clip, lut=lut, planes=0)

#改良後
def func(x):
    return max(x - 50, 0)
clip = core.std.Lut(clip, function=func, planes=0)

#または
clip = core.std.Lut(clip, function=lambda x: max(x - 50, 0), planes=0)

例:二つのクリップを平均化して新しいクリップを作りたい。
clipx = something
clipy = something

#これまでの書き方
lut = []
for y in range(2 ** clipy.format.bits_per_sample):
    for x in range(2 ** clipx.format.bits_per_sample):
        lut.append((x + y) // 2)
clip = core.std.Lut2([clipx, clipy], lut=lut, planes=[0, 1, 2])

#改良後
clip = core.std.Lut2([clipx, clipy], planes=[0, 1, 2], function=lambda x, y: (x + y) // 2)

functionの引数名はmasktools2のmt_lut/mt_lutxyに合わせて、xとyで固定です。
今までのようにlutをPython側で作って渡すこともできますが、lutとfunctionの両方を渡したらfunctionは無視されます。

次のリリース(多分r19test5)から使えるようになります。

2013年7月13日土曜日

MultiByteToWideCharToMultiByte その4

5.VapourSynthと文字エンコード

VapourSynthはr19でいろいろと大きな変更が入ることが見えたきたが、そのなかでも特に大きなものはVSScriptの導入である。

r18までのVSはvapoursynth.dll(Linuxならlibvapoursynth.so)と、vapoursynth.pydの二つに分かれていた。
vapoursynth.dllはメイン処理を担当するC++で書かれたライブラリであり、vapoursynth.pydはvapoursynth.dllをPythonで操作するためのcython製モジュールである。
この構成は全てをPython上で実行するだけなら特に問題はなかったが、いざPython以外の外部プログラムと連携させようとするとどうやったらいいものなのかが非常にわかりにくかった。
それこそコードを全部読みこんでMyrsloik氏自身と同程度の理解を要求されるレベルなのだ。
Windowsならばvsvfwとvsfsがあるのでまだ良いが、LinuxやMacではどうしたらいいものかさっぱりだった(筆者自身、半年ほど前にavs2yuvのようなツールを作ってみようとして、半日で挫折している)。

VSScriptはこのような状況を改善し外部プログラムとの連携を容易にするために作られたAPIである。
現段階ではまだ未完成ではあるが、既にAvsPmod開発者のvdcrim氏などは非常に強い関心を寄せている。
LinuxやMacでもAvsPmodを使ってVS用スクリプトを編集できるようになる日もそう遠くはないかもしれない。

さて、VSScriptのコードを追っかけてみると、vapoursynth.pydを構成するコード(vapoursynth.pyx)に次のような記述が追加されていた。
cdef public api int vpy_evaluateScript(VPYScriptExport *se, const char *script, const char *errorFilename) nogil:
...
        comp = compile(script.decode('utf-8'), errorFilename.decode('utf-8'), 'exec')
        exec(comp) in evaldict
...
このコードから察するに、VSScriptを使ってメモリ上に展開されたスクリプトのバイト列をPythonに渡すと、Pythonはバイト列をUTF-8でデコードしてユニコード文字列に変換し、しかる後にバイトコードにコンパイルすることになる。
これは事実上、r19以降はスクリプトの記述はUTF-8(BOMなし)で統一されるというこどだ。

そもそもPythonでは、スクリプトはASCII文字のみで書くか、それがダメならばUTF-8にすることとPEP8で決められている。
これは絶対の掟ではないが、今時Shift_jisやEUC-JPでPython書きたがるのはただの馬鹿だ(過去に書かれた負債のため嫌々書くならば理解できる)。
また、Avisynthでも前回で述べたようなファイル名の問題等を解決するため、スクリプトはUTF-8で書くように出来ないかとか、ユニコード対応ソフトウェアに改修しようといった議論が行われたことがあった(結局互換性重視の方針により実現はしなかったが)。
この変更は、大いに歓迎すべきものだろう。

VapourSynthはまだまだ若いプロジェクトであり、プラグイン作者もそれほど多くはない。
しかもほぼ全員が#darkhold(IRCチャンネル)に常駐しているため、舵取りも容易な状態である。
あきらかに自分のコードの書き方が悪かっただけなのに、プラグインの互換性を損ねたと駄々をこねる困った子も今のところはいない。
変更を追いかけるのはキツイかもしれないが、あと1年くらいは色々と変わるのではないかと思う。

2013年7月10日水曜日

MutiByteToWideCharToMultiByte その3

3. fopen()と_wfopen()

fopen()はC言語でファイルを開く際に使われる標準関数だが、Windowsには別に_wfopen()という独自関数がある。
FILE *fopen(const char *filename, const char *mode)
FILE *_wfopen(const wchar_t *filename, const wchar_t *mode)
そもそもNT系Windowsのカーネルはファイル名はユニコードで扱い、NTFSはファイル名をユニコードで保存している。
たとえば"あいうえお.txt"というファイルを開く場合、
fopen("あいうえお.txt", "rw")
は、まず"あいうえお.txt"というACPでエンコードされた文字列をユニコードに変換した後、そのユニコード文字列と合致する名前のファイルを検索し、見つかればそれを開く。
一方、
_wfopen(L"あいうえお.txt", L"rw")
は、"あいうえお.txt"というUTF-16LEでエンコードされた文字列(文字列リテラールの前にLを付ければ、その文字列はコンパイル時にUTF-16LEに変換される)をユニコードに変換してから処理を行う。

では、もし両関数に"魔法少女♥マジカルJEEBたん 第3話.mkv"という文字列を渡すとどうなるか?

_wfopen(L"魔法少女♥マジカルJEEBたん 第3話.mkv", L"rw")
は、ファイルが存在するならば成功する。
しかし
fopen("魔法少女♥マジカルJEEBたん 第3話.mkv", "rw")
は、ファイルの有無に関わらずWindowsでは失敗する。
なぜなら文字列中の''という文字はACPにはない文字なので、ユニコードへの変換ができないからである。
ACPに存在しない文字が使われていた場合、その文字は'?'(クエスチョンマーク)に置換えられ、その後の処理を行う。

4. Avisynthと文字列のエンコード

Avisynthの全関数のうちで最も重要なものは一体なんだろうか?

答えはもちろんEval()である。
Eval()はメモリ上に展開された文字列データをパースしてどのような処理を行えばいいのかを判別し、それを実行する機能である。
Eval()とは、まさにAvisynthインタプリタそのものなのだ。
AVISource("abcde.avi")
というスクリプトを実行するとき、Avisynth内部では
Eval("""AVISource("abcde.avi")""")
を行なっている。
どんなスクリプトもEval()なしでは決して動かない。

さて、この大事な大事なEval()は、ASCII非互換なエンコーディングの文字列をパースすることが出来ない。
よって、あなたはスクリプトをBOM付きUTF-8やUTF-16、UTF-32で書いてはいけない

では、Eval()の次に重要な関数はなにか?

答えはもちろんImport()だ。
Import()はスクリプトが書かれたファイルを開いて中のデータをメモリ上に展開し、Eval()を呼び出す機能である。
VirtualDubであれx264.exeであれ、avsを入力する際はImport()が働いている。
Import()なしで動くプログラムなんて、AvsPmodのプレビューとvsavsreaderのavsr.Eval()くらいしかない(これらは自らスクリプトをメモリ上に展開し、直接Eval()を呼び出している)。

さて、このとっても大事なImport()だが、ファイルを開く際にはfopen()を使っている。
よって、あなたは決してavsのファイル名にユニコード限定文字を使ってはならない

さらにもう一つ重要なことを述べておく。
Avisynthは開発開始よりこれまでずっとマルチバイト文字ソフトウェアであったため、内部関数もプラグインも文字列のエンコーディングはACPであることを前提として書かれてきた。
よってあなたは自分の使っているWindowsのシステムロケールのACP以外でスクリプトを保存してはならない。
そしてこれは、素材となる動画/音声ファイル名にもACPにない文字を使ってはならないということを意味する。

唯一の例外としてffms2のutf8オプションがある。
ffms2は主にwine上でAvisynthを使う非Windows環境のユーザーのため、このオプションを設けている。
しかしffms2が考慮しているのはffms2が読み込む対象のファイル名だけなので、それ以外に非ASCIIな文字がスクリプト中に存在すれば、結果は保証されないものとなるだろう。

2013年7月8日月曜日

MultiByteToWideCharToMultiByte その2

承前

2.FFmpegとAvisynth

前回に書いたような経緯でffmpeg.exeやavconv.exeでも日本語ファイル名のavsを読めるようになったのだが、今年の3月に今度はFFmpegのほうだけでこれが再発した。
原因はこのコミット
Avxsynthの開発者がAvxsynth入力を導入するべくFFmpegのほうだけavformat/avisynth.cを全部書きなおしてしまったことにより発生したのだった。
そして、自分がバグの再発を知ったのは先月(6月)の半ば頃のことである(2ちゃんねるのffmpegスレで見かけた)。

さて、問題のコードは次のとおりだった。
arg = avs_new_value_string(s->filename);
val = avs_library->avs_invoke(avs->env, "Import", arg, 0);
このコード、x264のavs.cを参考にしているわけだが、大事なことを忘れている。
前回も書いたように、s->filenameの中身はUTF-8でエンコードされた文字列である。
一方、Avisynthはマルチバイト文字アプリケーションで、文字列はACPエンコードが前提になっている。
だから非ASCIIな文字を含んだ文字列をそのままでAvisynthに渡したら、Avisynthはファイルを見つけられないのである。

書きなおすのは構わないんだけど、できればもうちょっと既存のコードを注意して読んでおいて欲しかった……。
ちょっと腹がたったから言っちゃうけど、Avxsynthユーザーなんて、非英語圏のAvisynthユーザーの千分の一もいないよ。

さて、原因は前回と同じなので対策も前回とまったく同じである。
再びパッチを書いて今度はFFmpegのMLに送った(Avxsynthの中の人はFFmpegだけに送ったのでLibavは昔のまま)。
かくしてコードは次のように変わった。
#ifdef _WIN32
    char filename_ansi[MAX_PATH * 4];
    wchar_t filename_wc[MAX_PATH * 4];
    MultiByteToWideChar(CP_UTF8, 0, s->filename, -1, filename_wc, MAX_PATH * 4);
    WideCharToMultiByte(CP_THREAD_ACP, 0, filename_wc, -1, filename_ansi, MAX_PATH * 4, NULL, NULL);
    arg = avs_new_value_string(filename_ansi);
#else
    arg = avs_new_value_string(s->filename);
#endif
    val = avs_library->avs_invoke(avs->env, "Import", arg, 0);
変数名が変わったのと MAX_PATH * 4 は、そのほうがいいんじゃね?とレビューしてもらったDaemon404(Derek Builtenhuis)氏に言われたから。

さすがにもう再発することはないと思うけど、もし再発しても次は知らない。気づいた人が直して下さい。
(そもそも筆者はエンコード素材のファイル名は半角英数以外使わないので、自分で気づくことはまずありません)

それともうひとつ。
このFFmpegの新Avisynth入力は、Avisynth2.5.8では不具合が出るそうです(参考:avisynth scripts fail to load in ffmpeg)。
まだ2.5.8を使ってる人は、とっとと2.6alphaに更新しましょう。

2013年7月7日日曜日

MultiByteToWideCharToMultiByte その1

最近、文字のエンコード/デコード関連のコードを書くことが多いのでメモ程度に。

1.FFmpeg/LibavとAvisynth

FFmpeg/Libavのライブラリは文字エンコードはUTF-8で統一されている。
しかし世の中にはUTF-8をそのまま扱うには色々問題があるOS(つまりWindows)が存在するので、2011年4月にいくつかのハックが取り入れられた。

ハックその1:
cmdutils(ffmpeg/avconv/avprobeといったコマンドラインツール用共通コード)は入力されたコマンドラインをWindowsの場合のみACP(ANSI code page、日本語版Windowsなら標準はCP932)からUTF-8に変換する。
ハックその2:
FFmpeg/Libavのツール/ライブラリにおいて、ファイルの開閉はavformatの仕事である。ほとんどのファイルはavformat_open_input()を使って開かれる。
int avformat_open_input(AVFormatContext **ps, const char *filename, AVInputFormat *fmt, AVDictionary **options);
avformat_open_inputは普通はio.hのopen()関数を使ってファイルを開くが、Windowsの場合はfilename(UTF-8)をワイド文字(UTF-16LE)に変換してから_wsopen()関数(マイクロソフトによる独自拡張)で開く。
このようになるべく問題が出ないよう配慮されているわけだが、avformatの対応形式の中にはこれだけだとまずいものが存在する。それがAvisynthである。

FFmpeg/LibavにおけるAvisynth入力は、昔DivX社の人が書いてMLに送ったもので、ファイルの操作はVideo for Windowsで行なっていた。
res = AVIFileOpen(&avs->file, s->filename, OF_READ|OF_SHARE_DENY_WRITE, NULL);
このコード、上記のハックが取り入れられるまでは特に問題がなかったのだけど、2011年4月以降はs->filenameの中身がUTF-8でエンコードされた文字列になってしまったので、ASCII以外の文字を使ったファイル名は文字化けしてAVIFileOpen()が失敗するようになったのである。

さて、このバグの存在を自分が知ったのは昨年の5月だった(2ちゃんねるのcygwin/mingwスレで見かけた)。
で、その日のうちにパッチを書いてLibavのMLに送り(Libavに入った変更は大抵3日以内にFFmpegにも取り込まれるので、送るならLibavのほうが手間が省ける)、ML上でのちょっとしたやり取りの後、以下のように変わった。
wchar_t filename_wchar[1024] = { 0 };
char filename_char[1024] = { 0 };
MultiByteToWideChar(CP_UTF8, 0, s->filename, -1, filename_wchar, 1024);
WideCharToMultiByte(CP_THREAD_ACP, 0, filename_wchar, -1, filename_char, 1024, NULL, NULL);
res = AVIFileOpen(&avs->file, filename_char, OF_READ|OF_SHARE_DENY_WRITE, NULL);
ACP -> UTF-16LE -> UTF-8とcmdutilsで変換された文字列を今度は逆にUTF-8 -> UTF-16LE -> ACPと変換するだけ。
変換用の一時的なバッファのサイズが1024で決め打ちなのは「とりあえずこの程度あればいいんじゃね?」と適当に書いただけで特に意味は無い。そしてなぜか誰もこのマジックナンバーにツッコミを入れなかったのでそのままになった。
かくしてffmpeg.exeやavconv.exeで再び日本語ファイル名が(ひょっとするとUTF-8で255文字を超えるファイル名の場合、コケる可能性があるけど)使えるようになった。

あと、ひょっとするとCP_THREAD_ACPよりもCP_OEMCPのほうが良かったのかもしれないけど、そこらへんはよくわからないので分かる人がいたらよろしくお願いします。

次回へ続く。

2013年7月3日水曜日

AviSynth2.6用プラグインの書き方

AviSynthプラグインを書く場合、各プラグインは必ず#include "avisynth.h"しなければならないわけですが、このavisynth.hが2.6でこれまでのものに比べて大きく変更されました。

2.5xまでのavisynth.hは
...
  bool HasVideo() const { return (width!=0); }
  bool HasAudio() const { return (audio_samples_per_second!=0); }
  bool IsRGB() const { return !!(pixel_type&CS_BGR); }
  bool IsRGB24() const { return (pixel_type&CS_BGR24)==CS_BGR24; } // Clear out additional
...
の様な感じで、helper系の関数はヘッダーに実体が書かれているというなんとも自由奔放で微妙なものでした。
さすがにこれはかっこ悪すぎるだろってことで2.6からはこれらの関数はavisynth.dllの内部に用意されるようになったわけですが、これにともなって新しいavisynth.hを使用したプラグインは2.6以降でしか使えなくなりました。

まあ、2.6でも2.5用のプラグインは特に問題なく使えますし、新しいものを古いavisynthで使うのはナンセンスです。
そもそもsource forgeで配布されている公式バイナリは、かの悪名高きVC++6でビルドされているため、XPどころかPentium2なWindows2000でも動きます。
みんながIanB氏に「いい加減コンパイラ新しくしてよ」と言っても頑なに古いものにこだわり続けているのです(そういえばAviUtlもVC++6だっけ?)。
まだ2.5xを使っている人は早急に2.6に変えましょう。

さて、2.6用avisynth.hを使う場合のプラグインの書き方ですが、以下のようになります。

・AVS_Linkage構造体
これまでavisynth.hに実体が書かれていた(つまり各プラグイン毎にコンパイルされていた)関数群がavisynth.dll内にAVS_Linkage構造体としてまとめられ、各プラグインはLoadPlugin()する際にこれのポインタを受け取るように変更されました。これに伴いinitializerもこれまでのAvisynthPluginInit2からAvisynthPluginInit3に変更されています。
で、avisynth.hには以下のように記述されていますので
extern const AVS_Linkage* AVS_linkage;
よってPlugin.cppはこう書きます。
...
const AVS_Linkage* AVS_linkage = 0;
...
extern "C" __declspec(dllexport) const char* __stdcall
AvisynthPluginInit3(IScriptEnvironment* env, const AVS_Linkage* const vectors)
{
    AVS_linkage = vectors;
    env->AddFunction("FunctionName", "arguments string", create_filter_function, user_data);
    return "some string";
}
avisynth.dllが各プラグインのinitializerを呼び出す際にAVS_Linkageのポインタを渡してくるので、それを保持するための変数を"AVS_linkage"という名前で静的領域に用意するだけです。
あとは今までと何も変わりません。