2013年8月25日日曜日

Transpose

transposeとは転置行列、つまり行列の行と列をひっくり返して新しい行列をつくることです。
画像をひとつの行列として見る場合、これをtransposeすると以下のようになります。


transeposeしたものに更にFlipHorizontal()をかければTurnRightになりますし、

FlipVerticalをかければTurnLeftになります。

VapourSynthにstd.Transpose()があるのに、TurnLeft/TurnRightがないのはこのためです。

さて、このtransposeは画像処理においては意外に重要な役割を果たします。
transposeすると、縦と横がひっくり返るため、フィルタ処理において縦と横でそれぞれ同じような処理をする場合(resizerとかconvolutionとか)、一方向の処理だけ書けば済むのです。
特にSSEとかCUDAとかでベクトル演算を利用するならば縦方向処理のほうが速いことが多いので、transposeの重要性は高まります。

というわけで、これは画像処理を嗜むものであれば自前でそこそこ速いやつを書けなければならんと思い、書いてみることにしました。

書いてみてわかったこと

1. SSE2を使う場合、8x8単位で処理するよりも16x16単位で処理するほうが速い
xmmレジスタの数的に32bit(8個)だと16x16はいろいろとペナルティが大きそうなのですが、それでも16x16のほうが速いようです。
もし64bitであれば、xmmレジスタの数は16個に増えるので、さらに有利になりそうな気がします。

2. VC++10は俺の書いたコードをろくにloop unrollしてくれない
このコードを書いた当初は配列とforループを使ったもうちょっとスッキリしたコードでした。でも、コンパイラがループアンロールしてくれないの...orz
手動アンロールしただけで数十fpsスピードが上がったのを目の当たりにしたときはもうなんというか力が抜けてしまいました。
こんな糞コンパイラが世にはびこっているせいでC99の普及が遅れたなんて、もうなんというかね...

2013年8月9日金曜日

Alpha Max Plus Beta Min Algorithm

画像処理フィルタを書いていると頻出する計算に次のようなものがあります。
X = sqrt(A * A + B * B)
要するに二つの数の二乗の和の平方根を求めるわけです。
普通にC等で書くならば特に問題ないわけですが、いざSIMD化しようとなるととたんに面倒になります。

まず整数演算の場合、掛け算は16bit同士からしかできません。
よって8bit同士の掛け算をしたい場合は、両者をまず16bitに変換しなければなりません。
__m128i xmm0 = _mm_setr_epi8(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15);
__m128i zero = _mm_setzero_si128()
__m128i xmm1 = _mm_unpackhi_epi16(xmm0, zero);
xmm0 = _mm_unpacklo_epi16(xmm0, zero);
更に、16bit同士の掛け算は最大で32bitになる可能性があるため(65,535 x 65,535 = 4,294,836,225)、出力は32bitになります。
しかも素直に32bitで出力してくれるような命令は存在せず、上位16bitと下位16bitがわかれた状態で出力されるか、積和計算を利用するかしなければなりません。
__m128i xmm2 = _mm_mullo_epi16(xmm0, xmm0);
__m128i xmm3 = _mm_mulhi_epi16(xmm0, xmm0);
xmm0 = _mm_unpacklo_epi16(xmm2, xmm3);
xmm2 = _mm_unpackhi_epi16(xmm2m xmm3);

xmm3 = _mm_unpackhi_epi16(xmm1, zero);
xmm1 = _mm_unpacklo_epi16(xmm1, zero);
xmm1 = _mm_madd_epi16(xmm1, xmm1);
xmm3 = _mm_madd_epi16(xmm3, xmm3);
ただ二乗するだけで、最初は128bitのXMMレジスタ一本で済んでいたのがいつのまにやら4本まで膨れあがり、コードもすでに12行です。
この操作をもう一つの数に対しても行い全部で8本になったレジスタを足しあわせて再び4本のレジスタになったあと、今度はこれらの平方根を求めなければなりません。
しかし、平方根を求める命令は整数演算にはないので、32bit整数を単精度浮動小数点に変換してから行うことになります。
__m128 flt0 = _mm_cvtepi32_ps(xmm0)
flt0 = _mm_sqrt_ps(flt0)
xmm0 = _mm_cvtps_epi32(flt0)
これを4回行った後、最後に再び8bit整数に戻します。
xmm0 = _mm_packs_epi32(xmm0, xmm1);
xmm1 = _mm_packs_epi32(xmm2, xmm3);
xmm0 = _mm_packus_epi16(xmm0, xmm1);
なんでたかがsqrt(A*A+B*B)を行うためにこんな辛い目にあわねばならないのでしょう。
しかもこれで計算できるのは一度にたったの16個です(SSE/SSE2の場合)。
普通にCで書いて16回計算するのとスピードは変わらないか下手すると遅くなってしまいます。
例えばTEdgeMaskはtriticalプラグインでは珍しくクリティカルパートがSIMD化されていないのですが、おそらくはこの計算が出てくるためにやらなかったのでしょう。

というわけで、もしどうしてもスピードがほしいなら普通はこんなに馬鹿正直な計算はしません。

sqrt(A*A+B*B) -> abs(A)+abs(B)

正直、手抜きにも程があるとは思います。
しかし、実用上これで問題が出るケースは意外と少ないので、これで済ませてしまったりするのですね。

とまあ、コストとパフォーマンスの兼ね合いという人類普遍のテーマについてつらつらと考えていたところ、prunedtree氏が"alpha max plus beta min algorithm"を教えてくれました。

sqrt(A*A+B*B) -> max(abs(A),abs(B))*alpha+min(abs(A),abs(B))*beta

alphaとbetaの選び方によっては最大誤差3.96%、平均誤差1.3%……これはイイ!
流石はgradfun2dbやFRfunシリーズを書いた人です。こういうテクニックをよく知ってらっしゃる。
抜け道というものは探せばあるものなんだなぁと思いました。


ちなみにprunedtree氏は、昔はmarcFDという名前でした。
なんでも若気の至りによって犯した過ち(DeenやaWarpSharpのことだと思われる)を悔いて改名したそうです。

2013年8月5日月曜日

Hysteresis mask

Doom9のほうで「GenericFiltersにmt_hysteresisの同等品を追加してくれないか?あれはhandyでいいものだよ」というリクエストがありました。

mt_histeresisねぇ…そりゃ、あんたにとってはhandyかもしれんが、書く方の身にもなれよ。
あれはIIRフィルタだからSIMD化も出来ないし、遅いものしか書けないんじゃないかなぁ。
だいたいあれってgenericなfilterじゃないだろ、specificなfilterじゃねえか。

てなことをブツブツ言いながらもとりあえず書いてみることにしたわけです。

ちなみにmt_hysteresisは、二値化する際の閾値を変えた二つのエッジマスクからノイズの少ない一つのエッジマスクを作り出すフィルタです。

まずはアルゴリズムやロジックを理解するため、avisynthで同等のものが書けるかどうかを試してみます。
vapoursynthプラグインは9/10/16bit対応とか可変解像度/フォーマットとかのことも考えないといけないので、avisynthよりも面倒なのです。
で、Masktools1の方を参考に(Masktools2のコードはmanao氏の趣味なのかテンプレートやSTL使いまくりのメタメタコードなのでC++がよくわからない自分には理解できない)書いてみたわけですが…

再帰を使うとstack overflowを起こすし、かといってSTLの使い方もよくわからんので自分でスタックを実装するはめになりました(Masktools2はSTLのlistとpairを使っている)。
結果として解像度に縦横ともに65535までの制限ができたりメモリの使用量が多くなったり(1920x1080で約8MB)しましたが、まあそこらへんは現時点ではそれほど問題ではないと目をつぶることにします。

さて、このプラグイン、出力はmt_hysteresisと変わらないわけですが最大の問題はスピードです。
さっそくベンチマークだ!
#benchmark.avs
MPEG2Source("1440x1080_6360frames.d2v").ConvertToY8()
base = last.mt_edge(thY1=30, thY2=30)
alt = last.mt_edge(thY1=5, thY2=5)
ret= mt_hysteresis(base, alt) # Masktools2a48
# ret = Hysteresis(base, alt) # 今回の自作フィルタ
return ret

$ for i in {1..3}; do avs2pipemod -benchmark benchmark.avs; done

      mt_hysteresis  Hysteresis
1st     24.255fps     48.490fps
2nd     24.242fps     48.523fps
3rd     24.310fps     48.583fps
--------------------------------
avg     24.269fps     48.525fps

まさかのダブルスコアです。

原因がMSVC++のSTL実装が糞すぎるのか(Masktools2の配布バイナリはVC++10で筆者と同じ)、それともこんなクリティカルな処理にSTLを使うのが間違いなのかはわかりませんが、C++大好き人間は御託を並べる前にもう少し基本に立ち返るべきなのではなかろーかとか思いました。
世はC++11サイコーとか騒がしいですが、肝腎な部分でラクなものに走れば大事なものを失うことになりかねません。
つーか、Masktools2遅すぎるだろ。

さて、次はvapoursynth版書かなきゃならんのか…。

追記:
TurboPascal7氏よりmt_hysteresisが遅い件について指摘を受けました。
彼曰く、「あれはmanaoがlistを使ってるから遅いんだよ、vector使えば2、3倍速くなるのは確認済みだよ」
なるほど、vectorですか。失礼しました。
俺もmallocで一度に最大分確保するのやめてrealloc使うことにしよ。

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"という名前で静的領域に用意するだけです。
あとは今までと何も変わりません。

2013年6月30日日曜日

VapourSynth r19(テスト版)

VapourSynthがr19で色々変わるようです。
とりあえずテスト版でのr18までとの違いを簡単にまとめておきます。

*この記事は現時点でのテスト版を元に書かれており、今後の展開によっては特に断りなく修正されます*

変更1:Coreクラスがsingletonになり、Core()がget_core()になった。

singletonは、デザインパターン(オブジェクト指向プログラミングのノウハウ)の一種ですが説明はメンドイので省きます。
具体的には、いままでこう書いていたのが
import vapoursynth as vs
core = vs.Core(threads=2, add_cache=False, accept_lowercase=True)
c0 = core.std.BlankClip(format=vs.YUV420P8)
c1 = core.std.BlankClip(format=vs.YUV422P9)
c2 = core.std.BlankClip(format=vs.YUV444P16)
こうなります。
import vapoursynth as vs
core = vs.get_core(threads=2, add_cache=False, accept_lowercase=True)
c0 = core.std.BlankClip(format=vs.YUV420P8)
c1 = core.std.BlankClip(format=vs.YUV422P9)
c2 = core.std.BlankClip(format=vs.YUV444P16)
もっと言えばこう書いてもいいです(あまりオススメはしませんが)。
import vapoursynth as vs
c0 = vs.get_core(threads=2, add_cache=False, accept_lowercase=True).std.BlankClip(format=vs.YUV420P8)
c1 = vs.get_core().std.BlankClip(format=vs.YUV422P9)
c2 = vs.get_core().std.BlankClip(format=vs.YUV444P16)
これまでは
Note that the loaded plugins are per core instance and not global so if you do create
several processing cores at once (not recommended) you will have to load the plugins
you want for each core.
と、Coreオブジェクトは何個でも作れましたが、r19以降は1プロセスに付き1個しか作れなくなりました(最初にget_core()した時に作られます)。
これにより、いままではユーザー定義のクラスや関数に引数でCoreオブジェクトを渡す必要がありましたが、今後はなくなりました(必要なところでget_core()すれば、どこで呼んでもプロセス内で共通のものが使われます)。

変更2:output()がなくなり、set_output()とvspipe.exeが出来た。またlastというクリップ名に特別な意味がなくなった。

これまでは
#sample_pre.vpy
import vapoursynth as vs
core = vs.Core()
last = core.std.BlankClip(format=vs.YUV420P8)

if __name__ == '__main__':
    last.output(sys.stdout, y4m=True)
このスクリプトをコマンドラインで使う場合は
$ python sample_pre.vpy | x264 - --demuxer y4m -o out.264
という感じで実行し、VirtualDub等で読みこめばlastクリップがプレビュー出来ましたが今後はこうなります。
#sample_r19.vpy
import vapoursynth as vs
core = vs.get_core()
clip = core.std.BlankClip(format=vs.YUV420P8)
clip.set_output()
このスクリプトをVirtualDub等VfWを利用するソフトウェアで読みこめば、set_output()したクリップ(変数名はなんでもよい)がプレビューできます。
そしてx264等に入力するにはvspipe(インストールすればおまけでついてきます)を使います。
$ vspipe sample_r19.vpy - -y4m | x264 - --demuxer y4m -o out.264 
output()がなくなってしまったので、これまでのようにスクリプト内でsubprocessつかってコマンド実行が困難(出来ないことはないけど)になりましたが、かわりに外部プログラムから使うためのAPIがかなり整備されたので、そのうちx264等にAVS入力同様、VS入力が実装されるでしょう。

変更3:get_plugins()とget_functions()が追加された。

r18までは、core.list_functions()で現在使えるすべてのフィルタ及びその引数の一覧を文字列として取得出来ましたが、r19でさらにcore.get_plugins()とcore.***.get_functions()が追加されました。
get_plugins()は、list_functions()では文字列として取得できた情報がdictとして取得出来ます。
>>> import vapoursynth as vs
>>> vs.get_core().get_plugins()
{'com.vapoursynth.resize': {'namespace': 'resize', 'identifier': 'com.vapoursynt
h.resize', 'functions': {'Bicubic': 'clip:clip;width:int:opt;height:int:opt;form
at:int:opt;yuvrange:int:opt;', 'Sinc': 'clip:clip;width:int:opt;height:int:opt;f
ormat:int:opt;yuvrange:int:opt;', 'Bilinear': 'clip:clip;width:int:opt;height:in
t:opt;format:int:opt;yuvrange:int:opt;', 'Spline': 'clip:clip;width:int:opt;heig
ht:int:opt;format:int:opt;yuvrange:int:opt;', 'Gauss': 'clip:clip;width:int:opt;
height:int:opt;format:int:opt;yuvrange:int:opt;', 'Lanczos': 'clip:clip;width:in
t:opt;height:int:opt;format:int:opt;yuvrange:int:opt;', 'Point': 'clip:clip;widt
h:int:opt;height:int:opt;format:int:opt;yuvrange:int:opt;'}, 'name': 'VapourSynt
h Resize'}, 'com.vapoursynth.avisynth': {'namespace': 'avs', 'identifier': 'com.
vapoursynth.avisynth', 'functions': {'LoadPlugin': 'path:data;'}, 'name': 'Vapou
rSynth Avisynth Compatibility'}, 'com.vapoursynth.std': {'namespace': 'std', 'id
entifier': 'com.vapoursynth.std', 'functions': {'Loop': 'clip:clip;times:int:opt
;', 'PropToClip': 'clip:clip;prop:data:opt;', 'StackVertical': 'clips:clip[];',
'Transpose': 'clip:clip;', 'FlipVertical': 'clip:clip;', 'PEMVerifier': 'clip:cl
ip;upper:int[]:opt;lower:int[]:opt;', 'Splice': 'clips:clip[];mismatch:int:opt;'
, 'ClipToProp': 'clip:clip;mclip:clip;prop:data:opt;', 'PlaneDifference': 'clips
:clip[];plane:int;prop:data:opt;', 'Lut2': 'clips:clip[];lut:int[];planes:int[];
bits:int:opt;', 'AssumeFPS': 'clip:clip;src:clip:opt;fpsnum:int:opt;fpsden:int:o
pt;', 'Cache': 'clip:clip;size:int:opt;fixed:int:opt;', 'FlipHorizontal': 'clip:
clip;', 'Expr': 'clips:clip[];expr:data[];format:int:opt;', 'LoadPlugin': 'path:
data;forcens:data:opt;', 'MaskedMerge': 'clips:clip[];mask:clip;planes:int[]:opt
;first_plane:int:opt;', 'ShufflePlanes': 'clips:clip[];planes:int[];format:int;'
, 'CropRel': 'clip:clip;left:int:opt;right:int:opt;top:int:opt;bottom:int:opt;',
 'SeparateFields': 'clip:clip;tff:int;', 'Reverse': 'clip:clip;', 'StackHorizont
al': 'clips:clip[];', 'Trim': 'clip:clip;first:int:opt;last:int:opt;length:int:o
pt;', 'SelectEvery': 'clip:clip;cycle:int;offsets:int[];', 'Interleave': 'clips:
clip[];extend:int:opt;mismatch:int:opt;', 'ModifyFrame': 'clips:clip[];selector:
func;', 'Turn180': 'clip:clip;', 'Lut': 'clip:clip;lut:int[];planes:int[];', 'Pl
aneAverage': 'clip:clip;plane:int;prop:data:opt;', 'BlankClip': 'clip:clip:opt;w
idth:int:opt;height:int:opt;format:int:opt;length:int:opt;fpsnum:int:opt;fpsden:
int:opt;color:float[]:opt;', 'DoubleWeave': 'clip:clip;tff:int;', 'CropAbs': 'cl
ip:clip;width:int;height:int;x:int:opt;y:int:opt;', 'SelectClip': 'clips:clip[];
src:clip[];selector:func;', 'AddBorders': 'clip:clip;left:int:opt;right:int:opt;
top:int:opt;bottom:int:opt;color:float[]:opt;', 'Merge': 'clips:clip[];weight:fl
oat[]:opt;'}, 'name': 'VapourSynth Core Functions'}}
はい、dictのなかにさらにdictがたくさんあって、なにがなにやらさっぱりわかりません……。
get_functions()は、もう少しマシです。
>>> vs.get_core().std.get_functions()
{'Loop': 'clip:clip;times:int:opt;', 'PropToClip': 'clip:clip;prop:data:opt;', '
StackVertical': 'clips:clip[];', 'Transpose': 'clip:clip;', 'FlipVertical': 'cli
p:clip;', 'PEMVerifier': 'clip:clip;upper:int[]:opt;lower:int[]:opt;', 'Splice':
 'clips:clip[];mismatch:int:opt;', 'ClipToProp': 'clip:clip;mclip:clip;prop:data
:opt;', 'PlaneDifference': 'clips:clip[];plane:int;prop:data:opt;', 'Lut2': 'cli
ps:clip[];lut:int[];planes:int[];bits:int:opt;', 'AssumeFPS': 'clip:clip;src:cli
p:opt;fpsnum:int:opt;fpsden:int:opt;', 'Cache': 'clip:clip;size:int:opt;fixed:in
t:opt;', 'FlipHorizontal': 'clip:clip;', 'Expr': 'clips:clip[];expr:data[];forma
t:int:opt;', 'LoadPlugin': 'path:data;forcens:data:opt;', 'MaskedMerge': 'clips:
clip[];mask:clip;planes:int[]:opt;first_plane:int:opt;', 'ShufflePlanes': 'clips
:clip[];planes:int[];format:int;', 'CropRel': 'clip:clip;left:int:opt;right:int:
opt;top:int:opt;bottom:int:opt;', 'SeparateFields': 'clip:clip;tff:int;', 'Rever
se': 'clip:clip;', 'StackHorizontal': 'clips:clip[];', 'Trim': 'clip:clip;first:
int:opt;last:int:opt;length:int:opt;', 'SelectEvery': 'clip:clip;cycle:int;offse
ts:int[];', 'Interleave': 'clips:clip[];extend:int:opt;mismatch:int:opt;', 'Modi
fyFrame': 'clips:clip[];selector:func;', 'Turn180': 'clip:clip;', 'Lut': 'clip:c
lip;lut:int[];planes:int[];', 'PlaneAverage': 'clip:clip;plane:int;prop:data:opt
;', 'BlankClip': 'clip:clip:opt;width:int:opt;height:int:opt;format:int:opt;leng
th:int:opt;fpsnum:int:opt;fpsden:int:opt;color:float[]:opt;', 'DoubleWeave': 'cl
ip:clip;tff:int;', 'CropAbs': 'clip:clip;width:int;height:int;x:int:opt;y:int:op
t;', 'SelectClip': 'clips:clip[];src:clip[];selector:func;', 'AddBorders': 'clip
:clip;left:int:opt;right:int:opt;top:int:opt;bottom:int:opt;color:float[]:opt;',
 'Merge': 'clips:clip[];weight:float[]:opt;'}
たとえば「Lut2の引数ってどんな感じだったっけ?」と思ったら、
>>> core.std.get_functions()['Lut2']
'clips:clip[];lut:int[];planes:int[];bits:int:opt;'
このようにcore.name.get_functions()['フィルタ名']とタイプすれば確認できます。
状況によってlist_functions()と使い分けましょう。

変更4:スクリプトに使用する文字セットをUTF-8で統一

VSScript(外部アプリケーションとの連携用API)の導入に伴い、スクリプトはUTF-8で書くように統一されたようです。
スクリプトはPythonに渡された後、UTF-8決め打ちで一旦Pythonのバイトコードにコンパイルされます。
このためUTF-8以外で日本語用文字等を使っている場合は、内部で文字化けを起こしてデコードエラーになります。
これまでは日本語ファイル名等を扱う場合はCP932で保存したほうがよかったですが、今後はUTF-8で保存するようにしましょう(もちろんBOMなしで)。
そしてもうひとつ大事なことですが、Windowsの場合、日本語ファイル名が扱えるかどうかはプラグインの実装次第となりました。
とりあえず見た感じ、avisource.dllはちゃんと対応してたけどd2vsourceはどうも現状ではダメっぽいですな。
ちなみに拙作のソースフィルタ群も軒並みダメです。ああ、直すのメンドイorz

2013年2月22日金曜日

EasyVFR for VapourSynth

なんかEasyVFRのVS版を欲しがってる人たちがいるようなので、やってみました。

easyvfr.py

使い方

まずeasyvfr.pyをPython3/Lib/site-packagesに置きます。

あとはこんな感じ
#!/usr/bin/env python3

import vapoursynth as vs
import easyvfr

vs.get_core().std.LoadPlugin('/path/to/d2vsource.dll')
d2vsrc = vs.get_core().d2v.Source

def ivtc(clip, offset, cycle=10, tff=True):
    sf = clip.std.SeparateFields(tff)
    dw = sf.std.DoubleWeave(tff)
    return dw.std.SelectEvery(cycle, offset)

def bob(clip, tff=True):
    sf = clip.std.SeparateFields(tff)
    return sf.resize.Bicubic(height=clip.height)


src = d2vsrc('/path/to/the/source.d2v')

# 適当にTrimして、デインタレ/IVTC/Bobしたりする
av0 = ivtc(src[100: 2000], [0, 3, 5, 8]) # 24fps
op0 = ivtc(src[2000: 3456], [0, 2, 5, 7]) # 24fps
op1 = bob(src[3456: 3501]) # 60fps
op2 = ivtc(src[3501: 6541], [1, 4, 6, 9]) # 24fps
a00 = ivtc(src[8000: 12000], [1, 3, 6, 8]) #24fps
a01 = src[12000: 12151] # 30fps
a02 = ivtc(src[12151: 20000], [0, 2, 5, 7]) #24fps

# av0, op0, a00の先頭フレームにチャプタを打ち、IDRフレームにしたい
av0 = easyvfr.ChapterClip(av0, 'アバンA')
op0 = easyvfr.ChapterClip(op0, 'OP')
a00 = easyvfr.ChapterClip(a00, 'Aパート')

# 各クリップをひとつのリストにまとめる
clips = [av0, op0, op1, op2, a00, a01, a02]

'''
22行目以降はこういう書き方もあり
cc = easyvfr.ChapterClip
clips = [
    cc(ivtc(src[100: 2000], [0, 3, 5, 8]), 'アバンA'),
    cc(ivtc(src[2000: 3456], [0, 2, 5, 7]), 'OP'),
    bob(src[3456: 3501]),
    ivtc(src[3501: 6541], [1, 4, 6, 9]),
    cc(ivtc(src[8000: 12000], [1, 3, 6, 8]), 'Aパート'),
    src[12000: 12151],
    ivtc(src[12151: 20000], [0, 2, 5, 7]),
]
'''

# timecodeファイル、chapterファイル、x264用QPファイルの出力、及び各クリップを結合
vfr = easyvfr.EasyVFR(clips)
vfr.write_tcq('/path/to/the/files')
#vfr.write_timecode('/path/to/the/timecode.txt')
#vfr.write_chapter('/path/to/the/chapter.txt')
#vfr.write_qpfile('/path/to/the/qpfile.txt')
vfr.splice_clips().set_output()

timecodeのフォーマットはv2のみです。
chapterファイルのエンコーディングはUTF-8になります。

追記(20160402):最近のVapourSynthにあわせてちょっと書き直しました。

2013年1月17日木曜日

GenericFilters その5

更新しました。

GenericFilters-0.4.1.7z
https://github.com/chikuzen/GenericFilters

* Convolution/ConvolutionHV/Blur:フレームが16bitだった場合の処理を修正

uint16_tとint16_tの乗算が出来る組み込み命令がないのって面倒すぎると思うんですが…。

2013年1月13日日曜日

GenericFilters その4

更新しました。

GenericFilters-0.4.0.7z
https://github.com/chikuzen/GenericFilters

* 新関数'Binarize2'を追加

Binarize2はBinarizeと同じくクリップを二値化するフィルタですが、閾値によって分けるのではなく、Sierra-2-4Aという誤差拡散法(いわゆるディザリングで使われるアルゴリズム)の一種によって処理します。

具体的には、これが
こうなります。
簡単に出来そうだったからやってみただけですので、実用性とかは気にしてはいけません。


2013年1月12日土曜日

vsimagereader その2

更新しました。

vsimagereader-0.2.0.7z
https://github.com/chikuzen/vsimagereader

* VapourSynthのAPIバージョンを3に更新
* アルファチャンネルの読み込みに対応/'alpha'オプションの追加
* 幅/高さ/出力フォーマットがバラバラな画像の読み込みに対応

以前も書きましたが、VapourSynthはavisynth等と違って各フレームの解像度やフォーマットが異なっていても扱えるようになっています。
今回の更新で、「解像度や保存形式がバラバラなものを一度に読み込んで、解像度を揃えて出力」なんてことが出来るようになりました。

一度にプラグイン4つも更新したので、大変疲れました。

GenericFilters その3

更新しました。

GenericFilters-0.3.0.7z
https://github.com/chikuzen/GenericFilters

* 関数に'Blur'を追加。

普通の3x3のぼかしフィルタです。
単なるConvolution()のaliasですな。

vsrawsource その4

更新しました。

vsrawsource-0.3.1.7z
https://github.com/chikuzen/vsrawsource

* アルファチャンネルのサポート方法を変更

vsavsreader その7

更新しました。

vsavsreader-0.1.0.7z
https://github.com/chikuzen/VS_AvsReader

* アルファチャンネルのサポート方法を変更
* 'alpha'オプションを追加

今回からこれにもバージョン番号を付けることにしました。

2013年1月6日日曜日

GenericFilters その2

更新しました。

GenericFilters-0.2.2.7x
https://github.com/chikuzen/GenericFilters

* Sobel/Prewitt: rshiftオプションを追加
* いろいろバグフィックス

ちょっとだけ内部処理の説明を書いておきます。

画像処理を行う場合、各ピクセルの値は、そのフォーマットによって扱える範囲が限定されます。
8bitフォーマットであればピクセルは0~255、16bitフォーマットであれば0~65535の範囲内に存在する値をとらなければなりません。
しかし、いろいろな計算を行った場合、出力値はしばしばこの下限/上限を超えることがあります。

この対処法として、最終的に出力する前に次のようなことが行われます。
1. 単純に、下限値/上限値で切り捨て。
2. 絶対値で扱う(値が下限値の0を超えた場合の対策)
3. 値を割ることで範囲内に収める(値が上限を超えた場合の対策)

GenericFiltersのConvolution(HV)はsaturateオプションをFalseにすれば処理2(絶対値化)が行われます。
divisorオプションによって処理3が行われます。
最後に処理1(値の切り捨て)を行うことで、出力値は範囲内に収められます。

さて、SobelとPrewittも多少工程が複雑であることを除けば単なる畳み込み演算なのですが、これらはエッジ検出に特化したアルゴリズムなので、自由度はConvolutionよりも低くなっています。
まず、処理2の絶対値化は必ず行われます。
そして、処理3の割る数は(主に処理速度の都合により)2の整数乗(rshiftで指定)に限定されています。

では今回はこのへんで

2013年1月5日土曜日

GenericFilters

VapourSynth用のプラグインを書きました

GenericFilters-0.2.0.7z
https://github.com/chikuzen/GenericFilters

名前のとおり、デジタル画像処理で一般的によく知られているフィルタ(畳み込み演算、二値化、膨張、収縮、メディアン等)の詰め合わせです。
avisynthで言えばmasktoolsにあたります。
VapourSynthは標準フィルタとして画像合成用のLut/Lut2/Expr/MaskedMergeを装備しているので、これらと組み合わせれば大幅に出来ることが増えます。

これを書くためにまず試作品としてConvo2Dを書き、ついでこれを書いたわけですが、最初はC言語オンリーだったので遅いったらありません。
とりあえず色々チュ-ニングしては見ましたが、やはり根本的な解決には至らず、今回とうとうSIMD(SSE2)に手を出してしまいました。

intrinsicなので多少はラクなのでしょうが、いや疲れたのなんのって、頭の中がバイト列の組み合わせパズルで占領されてしまいました。

UtVideoの梅澤さんに「よくアセンブラとか書けますね」って聞いてみたら、intrinsicよりMASMやNASMのほうが彼にはとってはラクなのだとか…怖いなぁ。