ラベル Python の投稿を表示しています。 すべての投稿を表示
ラベル Python の投稿を表示しています。 すべての投稿を表示

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年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にあわせてちょっと書き直しました。

2012年11月20日火曜日

FineSharpen for VapourSynth

VapourSynth用にFineSharpenを移植してみました。

https://gist.github.com/4111749

avs scriptの移植のサンプルにと考えて、規模的にちょうどよさそうだったのでこれを選んだわけですが、よく考えてみたらRemoveGrain、Repairというavisynthプラグインに依存してしまうので、あまりよくなかったかもしれません。

まあ、「masktoolsの代替をどうするか」の参考くらいにはなるということで。

これで使っているspline()は、こちらの方のものを参考にさせていただきました。
これだけのためにNumpyとか使うわけにもいかんからなぁ…

使い方:
gistからスクリプトをDLして、Python3.x\\Lib\\site-packages以下にfinesharp.pyという名前で保存して下さい。
import vapoursynth as vs
import finesharp

core = vs.Core()
core.std.LoadPlugin('path/to/the/ffms2.dll')
core.std.LoadPlugin('path/to/the/RemoveGrain.dll')
core.std.LoadPlugin('path/to/the/Repair.dll')
fs = finesharp.FineSharp(core)

clip = core.ffms2.Source('path/to/the/video.mp4')
clip = fs.sharpen(clip)
...

ちなみに 'print(fs.usage())' で、sharpen()の説明が読めます。

2012年9月21日金曜日

VapourSynthでプレビュー

とりあえず現時点ではVapourSynthでクリップのプレビューが出来るエディタ等はないので、間に合わせで書いてみました。

#vsshow.py

import vapoursynth as vs
from ctypes import *
from PIL import Image

def show(core, clip, frame_number, vflip=1):
    format = clip.format.id
    width = clip.width
    height = clip.height
    if format == vs.GRAY16:
        clip = core.resize.Point(clip, width, height, vs.GRAY8)
    if format != vs.GRAY8 and format != vs.RGB24:
        clip = core.resize.Bicubic(clip, width, height, vs.COMPATBGR32)

    format = clip.format.id
    planes = range(clip.format.num_planes)
    frame = clip.get_frame(frame_number)
    data = [(frame.get_read_ptr(p), frame.get_stride(p)) for p in planes]
    buff = [b'\0' * data[p][1] * height for p in planes]
    for p in planes:
        memmove(buff[p], data[p][0], data[p][1] * height)

    if format == vs.COMPATBGR32:
        mode, src = 'RGBA', 'BGRA'
    else:
        mode, src = 'L', 'L'
    img = [Image.frombytes(mode, (width, height), buff[p], 'raw', src,
                           data[p][1], vflip) for p in planes]
    if len(planes) != 1:
        img = Image.merge('RGB', (img[2], img[0], img[1]))
    else:
        img = img[0]

    img.show()

使い方
>>> import vapoursynth as vs
>>> import vsshow
>>> core = vs.Core()
>>> core.std.LoadPlugin('vsavsreader.dll')
>>> clip = core.avsr.Eval('ColorBars()')
>>> vsshow.show(core, clip, 1234)

こんな感じで指定したフレーム番号のフレームがプレビューできます。
vsshow.pyはvapoursynth.pydと同じ場所に置いておけばいいです。
ビューワーにはPIL(Python Imaging Library)の仕様上、.bmpファイルに関連付けされているアプリケーションが使われます(MSペイントだったりPicasaだったりMangameeyaだったり)。
なお、Python3用のPILは、こちらからDLしてインストールして下さい。
フレームがYUVの場合のRGB変換はVS内蔵のswscaleが使われる都合上、BT601のlimitted-range限定です。
色々ひどいと我ながら思いますが、ないよりはマシくらいのおおらかな気持ちで使いましょう。

だれかPySideあたりでもっといいの書いてくれないかしら。


Pythonはいろいろ不慣れでこれだけでも苦労しました(おかげでPythonの学習自体ははかどりましたが)。
Twitterでnu774氏(qaacの中の人)に色々アドバイス頂いたおかげで随分助かりました。

2012年9月12日水曜日

VapourSynth

巷で話題沸騰中?のVapourSynthを、ここ数日ちょこちょこいじっております。

VapourSynthとはffms2でお馴染みのMyrsloikことFredric Mellbin氏によって開発されているもので、
・動画を扱うためのPython3用モジュール。
・AviSynth2.5のプラグインをマルチスレッドで利用できる。
といった特徴があります。

早い話がAviSynthの開発が過去との互換性やらinline-asmやらでMT化も64bit化も一向に進まないので、新しいAviSynthを作ってしまったわけですな。

とりあえず現時点ではまだまだ試作段階ですが、MVTools2やTIVTC(mode次第では)がマルチスレッドで安定して動くあたりを見るに、かなり期待できそうです。

というわけで、試しにこんなのを書いてみました。

#!/bin/env python3
#coding: utf-8

import vapoursynth as vs
import subprocess as sp

class RawSource(object):
    def __init__(self, core, path='C:\\AviSynth2\\plugins\\rawsource_2.5x.dll'):
        self.avs = core.avs
        if core.list_functions().find('RawSource') == -1:
            self.avs.LoadPlugin(path=path)

    def rawsource(self, file):
        return self.avs.RawSource(file=file)


class MVTools(object):
    def __init__(self, core, path='C:\\AviSynth2\\plugins\\mvtools2.dll'):
        self.avs = core.avs
        if core.list_functions().find('MSuper') == -1:
            self.avs.LoadPlugin(path=path)

    def superclip(self, clip, pel=2, chroma=True, sharp=2):
        return self.avs.MSuper(c1=clip, chroma=chroma, sharp=sharp)

    def analyseclip(self, super, delta, blksize=8, overlap=4):
        return [[self.avs.MAnalyse(
            c1=super, delta=d, blksize=blksize, isb=b, overlap=overlap)
            for b in (True, False)]
            for d in range(1, delta + 1)]

    def mdegrain1(self, clip, pel=2, chroma=True, sharp=2, blksize=8,
                  overlap=4, thSAD=400):
        super = self.superclip(clip, pel, chroma, sharp)
        vec = self.analyseclip(super, 1, blksize, overlap)
        return self.avs.MDegrain1(
            c1=clip, c2=super, c3=vec[0][0], c4=vec[0][1], thSAD=thSAD)

    def mdegrain2(self, clip, pel=2, chroma=True, sharp=2, blksize=8,
                  overlap=4, thSAD=400):
        super = self.superclip(clip, pel, chroma, sharp)
        vec = self.analyseclip(super, 2, blksize, overlap)
        return self.avs.MDegrain2(
            c1=clip, c2=super, c3=vec[0][0], c4=vec[0][1], c5=vec[1][0],
            c6=vec[1][1], thSAD=thSAD)

    def mdegrain3(self, clip, pel=2, chroma=True, sharp=2, blksize=8,
                  overlap=4, thSAD=400):
        super = self.superclip(clip, pel, chroma, sharp)
        vec = self.analyseclip(super, 3, blksize, overlap)
        return self.avs.MDegrain3(
            c1=clip, c2=super, c3=vec[0][0], c4=vec[0][1], c5=vec[1][0],
            c6=vec[1][1], c7=vec[2][0], c8=vec[2][1], thSAD=thSAD)

    def mflowblur(self, clip, pel=2, chroma=True, sharp=2, blksize = 8,
                   overlap=4, blur=15):
        super = self.superclip(clip, pel, chroma, sharp)
        vec = self.analyseclip(super, 1, blksize, overlap)
        return self.avs.MFlowBlur(
            c1=clip, c2=super, c3=vec[0][0], c4=vec[0][1], blur=blur)


if __name__ == '__main__':
    core = vs.Core(threads=4)
    rs = RawSource(core)
    mvt = MVTools(core)
    clip = mvt.mdegrain2(rs.rawsource("D:\\source\\derf\soccer.y4m"))
    cmd = ('D:\\tools\\x264.exe - --demuxer y4m --frames %i'
           ' --preset ultrafast -o output.mp4' % clip.num_frames)
    clip.output(sp.Popen(cmd.split(), stdin=sp.PIPE).stdin, y4m=True)

for文やif文、リスト内包表記、そして様々な他のPythonモジュール等が使えるあたり、可能性はAviSynthよりも高そうです。
それにPythonですから、avsと違って仕事や学業等にも汎用的に使えます。
憶えて損はないでしょうね。

2012年2月5日日曜日

Pythonで関数の引数の順序を入れ替える

メモ替わり

L-SMASHのコードで使われている関数のいくつかについて、引数の順序を入れ替える必要が発生した。

こういった作業はIDEを使っていれば簡単にできるのかも知れないが、何かしら書くときはいつもテキストエディタを使い、コンパイルはCLI操作で済ませているので、使い方がいまだにさっぱり分からない。
かといっていちいち関数名で検索しては手作業で修正するのも面倒なので、久々にPythonを書いた。


制限事項:
一つの関数の記述が複数行にまたがる場合
int var = ItIsNoNeedToCareAboutTheLengthOfFunctionNamesBecauseEverybodyUsesIDE(foo,
                                                                               bar,
                                                                               fizz,
                                                                               buzz,
                                                                               bleh,
                                                                               fuck);
は処理できないので、warningを頼りに手動で修正すること。

2011年1月31日月曜日

中間出力のマルチプロセス化

先日、JEEB氏がAviSynth2.6.0αの非公式ビルドを始めた。

(JEEB) そういや、うちのAvs2.6ビルドってChikuzen氏がテストした奴と性能がどんな感じ?
(Chikuzen) 若干スピードが上がってるみたいよ
(Chikuzen) ちょっとベンチマーク回してみるか
      official 2.6.0α2       JEEB's 2.6.0(20110125)
1st   00:01:11.003(14.08fps)   00:01:07.254(14.87fps)
2nd   00:01:11.010(14.08fps)   00:01:07.003(14.92fps)
3rd   00:01:10.753(14.13fps)   00:01:07.003(14.92fps) (スクリプト等は前回と同じ)
(JEEB) interesting

たしかに興味深い。
どうやら最適化はα2配布後にさらに進んでいたようである。

さて、前回、自分はマルチスレッドによる高速化ではなく、マルチプロセスによる高速化を選んだことを書いた。
ここで問題となるのが、同時にエンコードするための複数のソースがない場合である。
そもそも自分は放送ソースの保存用エンコなんてまったくしない。そんなことには興味がない。
いろいろなフィルタやエンコーダーを試して遊ぶのが好きなのである。
当然、エンコードするべきソースは大抵1本しかなく、3本同時にエンコードなんていままでに数えるほどしかやったことがない。

では、どうすればいいのか?
エンコード対象がひとつしかないのであれば、ひとつを複数に分割してやればいいだけだ。
3本同時にエンコードできる余裕があるなら、1本を3分割してそれぞれ中間出力し、ふたたびくっつけてやればよい。
それが簡単にできるのがAviSynthである。
中間出力用のファイルサイズを気にするような時代でもないしね。

てことで、自動で分割->中間出力->結合->エンコードを行うAvsPマクロを書いた。
#multiprocess intermediate file generator.py
#dividing source script into X -> generate intermediate files at same time -> final encode
#requiaments : VirtualDub.exe, vcf file, ffmpeg or x264 (optional)
import sys
import os.path
import subprocess

vdub = r'C:\VirtualDub-1.9.11\VirtualDub.exe'
vcf  = r'C:\VirtualDub-1.9.11\fastrecomp_uly0_noaudio.vcf'

entries = avsp.GetTextEntry(['Number of dividing', 'SetMemoryMax(?)'], ['3', '1024'])
dnum, mmax = [int(i) for i in entries]

imf_outdir = avsp.GetDirectory('Select a directory to output intermediate files.')

encbin  = avsp.GetFilename('Set Path to Encoder(ffmpeg or x264)')
#encbin = r'G:\Enctools\ffmpeg.exe'
#encbin = r'G:\Enctools\x264_x64.exe'
encout  = avsp.GetSaveFilename('Set final output file name')
encopts = avsp.GetTextEntry('Input commandline options').split(' ')

source = avsp.SaveScript()
frames = avsp.GetVideoFramecount()

if dnum > 1 and mmax > 0 and imf_outdir and frames and os.path.isfile(vdub) and os.path.isfile(vcf):
    start = 0
    vdubprocess = []
    catfilename = os.path.splitext(source)[0] + '_imf.avs'
    catfile = open(catfilename, 'w')

    for i in range(dnum):
        end = frames / dnum * (i + 1)
        if i < dnum - 1:
            trimline = 'trim(%i, %i)' % (start, end)
        else:
            trimline = 'trim(%i, 0)' % start
        lines = 'SetMemoryMax(%i)\nImport("%s")\n%s\n' % (mmax, source, trimline)
        start = end + 1

        imf_avsname = os.path.join(imf_outdir, os.path.basename(source)).rstrip('avs') + 'imf_%02i.avs' % i
        file = open(imf_avsname, 'w')
        file.write(lines)
        file.close()

        imf_aviname = os.path.splitext(imf_avsname)[0] + '.avi'
        vdubargs = [vdub, '/s', vcf, '/p', imf_avsname, imf_aviname, '/r', '/c', '/x']
        p = subprocess.Popen(vdubargs)
        vdubprocess.append(p)

        if i < dnum - 1:
            catfile.write('AVISource("%s") + \\\n' % imf_aviname)
        else:
            catfile.write('AVISource("%s")\n' % imf_aviname)

    catfile.close()

    if encbin and encout:
        for p in vdubprocess:
            p.wait()

        if os.path.basename(encbin).count('ffmpeg'):
            encargs = [encbin, '-i', catfilename] + encopts + ['-y', encout]
        elif os.path.basename(encbin).count('x264'):
            encargs = [encbin, catfilename, '-o', encout] + encopts
        else:
            sys.exit()

        subprocess.Popen(encargs)

2010年12月19日日曜日

TAEC.py

またまた某所にて
(silverfilain) どうせならMaxMBPSとMaxFSも考慮して幅・高さ・fpsから
最適なレベルを表示するのも作って欲しいぉ
(Chikuzen) じゃあ、ちょっと調べてみる
てな感じで、また書くことになった。
#!/bin/env python
# coding: utf-8
#****************************************************************************
#  TAEC(Tiny Avc Encode Consultant).py 
#                                                     written by Chikuzen
#  Reference literature:
#    Rec. ITU-T H.264 (03/2010) – Prepublished version
#    インプレス標準教科書シリーズ改訂版 H.264/AVC教科書
#           著者:(監修)大久保 榮/(編者)角野 眞也、菊池 義浩、鈴木 輝彦
#    http://en.wikipedia.org/wiki/H.264/MPEG-4_AVC
#    猫科研究所( http://www.up-cat.net/ )
#         x264(vbv-maxrate,vbv-bufsize,profile,level),H.264(Profile/Level)
#
#****************************************************************************

__version__ = '0.3.3'
import sys
import getopt
import math

def set_default():
    width   = int(1280) 
    height  = int(720)
    fpsnum  = int(30000)
    fpsden  = int(1001)
    profile = 'high'
    mode    = 'progressive'
    return [[width, height], [fpsnum, fpsden], profile, mode]

def usage():
    param = set_default()
    print "\nUsage: taec.py [options]\n"
    print "  -r, --resolution <string> :set 'width x height' ('%ix%i')" % tuple(param[0])
    print "  -f, --fps <string>        :set 'fpsnum / fpsden' ('%i/%i')" % tuple(param[1])
    print "  -p, --profile <string>    :set 'profile' ('%s')" % param[2]
    print "  -i, --interlaced          :specify interlaced mode (not specified)"
    print "  -v, --version             :display version"
    print "  -h, --help                :display this help and exit\n"

def check_res_and_fps(arg, r_or_f):
    try:
        param = [abs(int(i)) for i in arg.split('x' * r_or_f or '/')]
        if len(param) != 2:
            raise SyntaxError
    except:
        print "\nERROR : invalid %s setting." % ('resolution' * r_or_f or 'fps')
        usage()
        sys.exit()
    else:
        return param

def check_profile(profile, ipflag):
    if profile in ('baseline', 'main', 'high'):
        if profile != 'baseline' or ipflag != 'interlaced':
            return 1
        else:
            print "\nERROR : baseline cannot accept interlaced."
    print "\nERROR : invalid profile setting."
    usage()
    sys.exit()

def calc_bs(resolution, fps, ipflag):
    fstmp = [int(math.ceil(i / 16.0)) for i in resolution]
    fs = dbp = fstmp[0] * fstmp[1]
    mbps  = fs * fps[0] // fps[1]
    if ipflag == 'interlaced':
        dbp += fstmp[0] * (fstmp[1] % 2)
    return [mbps, fs, dbp]

def calc_lv(bs, ipflag, spec):
    for i in spec:
        if bs[0] <= i[1] and bs[1] <= i[2] and bs[2] <= i[3]:
            return i[0]
    if ipflag == 'interlaced':
        print "ERROR : interlaced encoding cannot be done to this video."
    print "ERROR : there is no suitable setting."
    usage()
    sys.exit()

def calc_result(profile, bitstream, line):
    vbv = [int(i * ((profile == 'high') * 1.25 or 1)) for i in line[4]]
    ref = [16 * (i > 16) or i for i in [line[3] // bitstream[2]]]
    return tuple([line[0]] + vbv + ref)

def display_result(level, profile, bitstream, spec):
    index = [i[0] for i in spec]
    try:
        for i in xrange(len(index)):
            if index[i] == level:
                line = spec[i]
                print "%5s%12i%13i%8i" % calc_result(profile, bitstream, line)
                level = index[i + 1]
    except:
        return 0

def get_spec():
#H.264/AVC spec [(level, MaxMBPs, MaxFS, MaxDbpMBs, [MaxBR, MaxCPB], ipflag]}
    return [('1.0',   1485,    99,    396, [    64,    175], 'p'),
            ('1b ',   1485,    99,    396, [   128,    350], 'p'),
            ('1.1',   3000,   396,    900, [   192,    500], 'p'),
            ('1.2',   6000,   396,   2376, [   384,   1000], 'p'),
            ('1.3',  11880,   396,   2376, [   768,   2000], 'p'),
            ('2.0',  11880,   396,   2376, [  2000,   2000], 'p'),
            ('2.1',  19800,   792,   4752, [  4000,   4000], 'i'),
            ('2.2',  20250,  1620,   8100, [  4000,   4000], 'i'),
            ('3.0',  40500,  1620,   8100, [ 10000,  10000], 'i'),
            ('3.1', 108000,  3600,  18000, [ 14000,  14000], 'i'),
            ('3.2', 216000,  5120,  20480, [ 20000,  20000], 'i'),
            ('4.0', 245760,  8192,  32768, [ 20000,  25000], 'i'),
            ('4.1', 245760,  8192,  32768, [ 50000,  62500], 'i'),
            ('4.2', 491520,  8192,  34816, [ 50000,  62500], 'p'),
            ('5.0', 589824, 22080, 110400, [135000, 135000], 'p'),
            ('5.1', 983040, 36864, 184320, [240000, 240000], 'p')]

def set_param(opts, param):
    for opt, arg in opts:
        if opt in ("-r", "--resolution"):
            param[0] = check_res_and_fps(arg, 1)
        elif opt in ("-f", "--fps"):
            param[1] = check_res_and_fps(arg, 0)
        elif opt in ("-p", "--profile"):
            param[2] = arg
        elif opt in ("-i", "--interlaced"):
            param[3] = 'interlaced'
        elif opt in ("-h", "--help"):
            usage()
            sys.exit()
        elif opt in ("-v", "--version"):
            print "tiny avc encode consultant %s" % __version__
            sys.exit()
    return param

if __name__ == '__main__':
    try:
        opts, args = getopt.getopt(sys.argv[1:], "r:f:p:ihv",
            ["resolution=","fps=","profile=","interlaced","help","version"])
    except:
        usage()
        sys.exit()

    param = set_default()

    if len(opts) > 0:
        param = set_param(opts, param)
    else:
        usage()

    check_profile(param[2], param[3])

    print
    print " resolution       : %i x %i" % tuple(param[0])
    print " fps              : %i / %i" % tuple(param[1])
    print " profile          : %s"      % param[2]
    print " encoding mode    : %s\n"    % param[3]

    bitstream = calc_bs(param[0], param[1], param[3])
    print " MBPS ... %6iMB/s" % bitstream[0]
    print " FS   ... %6iMBs"  % bitstream[1]
    print " DPB  ... %6iMBs\n"  % bitstream[2]

    if param[3] == 'interlaced':
        avcspec = [i for i in get_spec() if i[5] == 'i']
    else:
        avcspec = get_spec()

    minlv = calc_lv(bitstream, param[3], avcspec)

    print " suitable settings are ...\n"
    print " level  vbv-maxrate  vbv-bufsize  max-ref"
    print " ----------------------------------------"
    display_result(minlv, param[2], bitstream, avcspec)

#changelog
# 2010/12/19     0.1.0 公開
# 2010/12/19     0.1.1 いろいろ計算がおかしかったのを修正
# 2010/12/20     0.2.0 lambda式面白い
# 2010/12/21     0.3.0 リスト内包とgetoptの存在を知る
#    〃          0.3.1  getoptの長文形式における要引数要素に=を付けていなかったのを修正
# 2010/01/02     0.3.2  処理が重複する関数(check_resolution, check_fps)をひとつにまとめた
# 2011/03/17     0.3.3  cosmetics
# 2011/10/12     0.3.4  Fix 10L
なにせ「コンサルタント」ですから、まず役にたたないと思われます。

追記:
06_taro氏がメンテナンスを引き継いでくれたようです。
https://gist.github.com/2325004

2010年12月18日土曜日

avc_refcalc.py

某所にて
(boiled_sugar) level計算機とかないのかなー
(Chikuzen) どんな計算したいの?
(boiled_sugar) 解像度とlevelで最大ref計算
(Chikuzen) この前D_Sがそんなの書いてx264に組み込んでなかったっけ?
(boiled_sugar) 独立したプログラムが欲しい
というわけで、書いてみた。
#!/bin/env python
# coding: utf-8

#****************************************************************************
#  avc_refcalc.py 0.20
#                                   written by Chikezun
#  Reference literature:
#    インプレス標準教科書シリーズ改訂版 H.264/AVC教科書
#           著者:(監修)大久保 榮/(編者)角野 眞也、菊池 義浩、鈴木 輝彦
#    http://en.wikipedia.org/wiki/H.264/MPEG-4_AVC
#    猫科研究所(http://www.up-cat.net/FrontPage.html)
#         x264(vbv-maxrate,vbv-bufsize,profile,level),H.264(Profile/Level)
#
#****************************************************************************

import sys
import math

def usage():
    print "Usage: avc_refcalc.py [options]\n"
    print "  -r, --resolution <string> :set 'width x height' ('1280x720')"
    print "  -l, --level <string>      :set 'level' ('4.1')"
    print "  -p, --profile <string>    :set 'profile' ('high')"
    print "  -i, --interlaced          :specify interlaced mode (not specified)"
    print "  -h, --help                :display this help and exit\n"

def check_prof(pr, ip):
    for i in ['baseline', 'main', 'high']:
        if i == pr:
            if i != 'baseline' or ip != 'interlaced':
                return i
            else:
                print "ERROR : baseline cannot accept interlaced."
    print "ERROR : invalid profile setting.\n"
    usage()
    sys.exit()

def check_level(lv, ip, dic):
    lvl = lv.replace('0','').replace('.','')
    if dic.has_key(lvl):
        if ip[0] != 'i' or dic.get(lvl)[0] == 'i':
            return lvl
        else:
            print "ERROR : specified level cannot accept interlaced."
    print "ERROR : invalid level value.\n"
    usage()
    sys.exit()

def calc_mbs(w, h, ip):
    mbh = int(math.ceil(float(w) / 16))
    mbv = int(math.ceil(float(h) / 16))
    if mbv % 2 == 1 and ip == 'interlaced':
        mbv += 1
    mbs = mbh * mbv
    if mbs > 0:
        return mbs
    else:
        print "ERROR : invalid resolution setting.\n"
        usage()
        sys.exit()

def calc_vbv(lv, pr, dic):
    vbvmax = dic.get(lv)[1]
    vbvbuf = dic.get(lv)[2]
    if pr == 'high':
        return [int(vbvmax * 1.25), int(vbvbuf * 1.25)]
    else:
        return [vbvmax, vbvbuf]

def calc_maxref(lv, mbs, dic):
    ref = int(dic.get(lv)[3] / mbs)
    if ref > 16:
        ref = 16
    if ref > 0:
        return ref
    else:
        print "ERROR : resolution is too large to level.\n"
        usage()
        sys.exit()

options = sys.argv
len_opt = len(options)

#set default values
width  = 1280
height = 720
level  = '4.1'
prof   = 'high'
mode   = 'progressive'
help   = 0

#H.264/AVC level dictionary {level: [interlaced flag, MaxBR, MaxCPB, MaxDbpMbs]}
avcdic = {'1' :['p',     64,    175,    396], '1b':['p',    128,    350,    396],
          '11':['p',    192,    500,    900], '12':['p',    384,   1000,   2376],
          '13':['p',    768,   2000,   2376], '2' :['p',   2000,   2000,   2376],
          '21':['i',   4000,   4000,   4752], '22':['i',   4000,   4000,   8100],
          '3' :['i',  10000,  10000,   8100], '31':['i',  14000,  14000,  18000],
          '32':['i',  20000,  20000,  20480], '4' :['i',  20000,  25000,  32768],
          '41':['i',  50000,  62500,  32768], '42':['p',  50000,  62500,  34816],
          '5' :['p', 135000, 135000, 110400], '51':['p', 240000, 240000, 184320]}

if len_opt > 1:
    for i in range(len_opt):
        try:
            if options[i] == '-r' or options[i] == '--resolution':
                res    = options[i + 1].split('x')
                width  = int(res[0])
                height = int(res[1])
            if options[i] == '-l' or options[i] == '--level':
                level = options[i + 1]
            if options[i] == '-p' or options[i] == '--profile':
                prof = options[i + 1]
            if options[i] == '-i' or options[i] == '--interlaced':
                mode = 'interlaced'
            if options[i] == '-h' or options[i] == '--help':
                help = 1

        except:
            print "ERROR : invalid arguments\n"
            help = 1

        else:
            pass

    if help == 1:
        usage()
        sys.exit()

else:
    usage()

profile = check_prof(prof, mode)
lv_tmp  = check_level(level, mode, avcdic)
mbs     = calc_mbs(width, height, mode)
vbv     = calc_vbv(lv_tmp, profile, avcdic)
maxref  = calc_maxref(lv_tmp, mbs, avcdic)

print " resolution       : %i x %i" % (width, height)
print " level            : %s" % level
print " profile          : %s" % profile
print " mode             : %s" % mode
print " vbv-maxrate(vlc) : %i" % vbv[0]
print " vbv-bufsize(vlc) : %i" % vbv[1]
print " max ref number   : %i" % maxref

2010年10月19日火曜日

AvsPのブックマーク位置をずらすマクロ

AvsP(mod)でTrimSelectionを使ってCMカットすると、各パートの終端フレームにブックマークが打たれた状態になる。
これが結構曲者で、例えば動画にチャプターを打つ場合、チャプターポイントは各パートの先頭フレームに来るように打つものであるが、ブックマークが打たれている場所が各パートの終端だと、そのまま"Bookmarks to Chapter.py"を使うわけにいかない。
各パートの終端フレームの次のフレームは当然、次のパートの先頭フレームになるわけだが、この1フレームのずれを修正するために、いちいちブックマークを消して、1フレーム移動して、もう一度ブックマークを打ち直すのはめんどくさい、なんとかならんもんかいな?

と思ったので、これもマクロで何とかしてみようというのが今回のお話。
-----------------------------------------------------------------------------
既存のブックマークすべてを1フレーム右(未来方向)にずらすだけなら簡単である。
#Shift Bookmarks right+1.py
before=avsp.GetBookmarkList()
if len(before):
    after = [i + 1 for i in before]
    after.sort()
    avsp.SetBookmark(before) #ブックマークは二重に打つと消える。
    avsp.SetBookmark(after)
しかし、これをそのまま使うと、もし最終フレームにブックマークが打たれていた場合、そのブックマークは範囲外に飛び出して消えてしまう。
それに、すでにブックマークを1フレームずらし終えたのに、何らかの操作ミスでもう一回ずらしてしまったらどうしよう? マクロを使った場合、undoは効かないよ?
はたまた、例えばブックマークを打ち終え、チャプターを出力する段になったところでトリムのし残しを発見してしまった場合は? その部分が全部で10個打ったブックマークの2番目と3番目の間に30フレームあったりしたら、1番目と2番目は打ち直すとしても、3番目以降のためにこれを30回実行するの?
ずらすフレーム数を任意に指定できるようにしたとして、全部で10000フレームしかないクリップに対して、桁を間違えて100000なんて指定してしまったら?
などと不測の事態についていろいろ考えていたら、最後にはこうなっていた。
#Shift Bookmarks.py
def newBM(present, shift, last):
    if 0 <= present + shift <= last:
        return present + shift
    elif present + shift > last:
        return newBM(present, shift - last - 1, last)
    else:
        return newBM(present, shift + last + 1, last)

input = avsp.GetTextEntry('Input an integer value',default='1')
shift = int(input)
before = avsp.GetBookmarkList()
if shift and len(before):
    framecount = avsp.GetVideoFramecount() 
    after = [newBM(old, shift, framecount - 1) for old in before]
    after.sort()
    avsp.SetBookmark(before)
    avsp.SetBookmark(after)
うーむ、再帰なんて初めて使ったよ。
-------------------------------------------------------------------------
使い方:
ブックマークが打たれた状態でマクロを実行するとダイアログが現れるので、数値を入力する。デフォルトは1。
正ならば右(未来方向)、負ならば左(過去方向)にすべてのブックマークが指定した数値分だけ移動する。
最終フレーム + 1 = 0番フレーム(0番フレーム - 1 = 最終フレーム)として循環処理される。
数値以外の文字列を入力した場合はエラーとなり何もしない。小数点以下は切り捨て。
操作を取り消したい場合はもう一度マクロを実行し、事前の指定値の反数を入力すること。

2010年10月18日月曜日

CM検出マクロ for AvsP

この前のSCx264 to Bookmarksは、少々時間がかかり過ぎるので、CMカットにはいまいち不向きなようだった。
もっと短時間で可能な方法はないものだろうか?
しばし悩んだ末、そういえばAviUtlにもなにやらCMカット用のシーンチェンジ検出プラグインがあると小耳に挟んだことを思い出したので、見てみることにした。
「チャプター編集 for AviUtl by ぽむ + 無音&シーンチェンジ検索機能 by ru」
読み込んでいる動画の音声トラックをスキャンし、無音連続部分を検出して、その「開始部分」にチャプターを打ちます。
その後チャプターへのシーク時に無音部分内のシーンチェンジを検出し、その位置へ移動できます。
うーむ、なるほど…。
シーンチェンジ検出を無音部分に限定すれば高速化が可能かも!?
-------------------------------------------------------------------------
では早速やってみましょう。

まずサンプルクリップを用意します。
今回はMPEG2-TS、音声AACで再生時間31分、総フレーム数55749フレームの地デジソース(1440x1080)です。
#sample.avs
MPEG2Source("sample.d2v")
AudioDub(last, WAVSource("sample.wav"))

#とりあえず普通に再生できる音声(FAWとかの偽装WAVはこの場合は駄目)でディレイ補正処理を済ませておく。
#今回はソースが地デジTSなので、とりあえず
#[DGIndexでdemuxしたaac -> FAWを2回通してディレイ処理済のaac -> FAADでPCM.wavに変換]
#したものを使った。
#処理終了後に差し替えれば、偽装WAVでも大丈夫。
#もしaacを直読みしたい場合は、BassAudioでも使ってDelayAudio()で補正することになる。
このクリップの無音部分を検出しなければならんわけですが、使えそうな道具は…MinMaxAudioかな?
AudioMax(0)が-100dbよりも低ければ、そのフレームは無音と判定することにしましょう(テキトー)。
#sample.avs
MPEG2Source("sample.d2v")
AudioDub(last, WAVSource("sample.wav"))

LoadPlugin("MinMaxAudio.dll")
AudioDub(BlankClip(last, width=160, height=80), last)
#スピードアップのため、クリップを映像160x80で真っ黒、音声はそのままに変換する
WriteFileIf("silence.log", "AudioMax(0) < -100", "current_frame")
#該当フレームのフレーム番号をテキストに書き出す
このavsをVirtuaDubの"Run Video Analysis pass"にかけてやると、約1250fpsくらいのスピードでsilence.logが生成されます。
結果を見てみると、どうやら無音のフレームは、各CM及び本編の切れ目あたりにだいたい10~30フレーム入るみたいです。
具体的には全55749フレームのうち、41-53、475-504、926-954、1376-1402、1825-1837、2289-2302、2725-2738、9077-9092、9990-10003、10874-10905、26915-26923、27373-27385、27808-27821、28707-28738、46494-46507、49179-49206、49628-49656、50078-50103、50525-50553、50975-50987、51439-51451、51874-51902、52783-52788、53043-53101、53523-53535、54422-54434、54886-54898、55320-55350、55735-55748の全29箇所561フレームが無音(-100db未満)です。

そこでsample.avsをこうしてやり
#sample.avs
MPEG2Source("sample.d2v")
AudioDub(last, WAVSource("sample.wav"))

PointResize(last.width / 4, last.height / 4)
#シーンチェンジ検出には320x270程度で十分なのでPointResizeでリサイズ
FreezeFrame(0,41,41)
FreezeFrame(53,475,475)
FreezeFrame(504,926,926)
#途中24行省略
FreezeFrame(54898,55320,55320)
FreezeFrame(55350,55735,55735)
FreezeFrame(55748,0, 0)

#AviSynthの仕様ではFreezeFrameでコピーされるフレームはデコードされないので、
#MPEG2Sourceのボトルネックが解消されるはず
このavsをこの前のSCx264 to Bookmarks.pyにかけてやると、x264はなんと約2200fpsという想像を絶するスピードでシーンチェンジ検出を終え、ブックマークを打ち終わりました。
あとはFreezeFrameの行を消してやれば…YATTA!!
-----------------------------------------------------------------------------
てなことがありまして、これらの操作を自動で行うべく書いたAvsPマクロが以下の通りです。
使い方は、とりあえずAudioDubまで書いたavsを用意して、マクロを実行するだけです。
FAW使用者は、実行後に音声を偽装WAVに差し替えればいいです。ちょっと書き換えれば、そこらへんも自動化できるでしょう
前回約10分かかってたのが、約1分まで縮まりました(on Q9450定格)。
著作権は…発生するのかどうか知りませんが、もし発生するのであればGPLv3です(なんとなく)。
つーか、Pythonスクリプトですから、バイナリと違って秘密にはしようがないと思うけど…。

EDIT:
よく考えたらavs2aviのnull出力とx264の--preset ultrafast -o nul ではスピードは大して変わらないので(っていうか、今度はMinMaxAudioがボトルネックになっているので)、avs2aviをx264に変更。
avs2aviは要らなくしました。
#Silent_Scenechanges to Boolmarks.py for AvsP
#Author : Chikuzen (http://csbarn.blogspot.com/)
#Requiament: MinMaxAudio.dll, x264.exe, (avs2avi.exe)

#--------------preparation----------------------------------
import os

avsname = avsp.SaveScript()
script_org = avsp.GetText()

#あらかじめ各ツールのパスを設定しておくこと
#ファイルパスを変数に入れる際、前にrをつけるのはPythonのお約束
mmaudio = r'C:\AviSynth 2.5\Plugins_x86\MinMaxAudio.dll'
x264cli = r'G:\Enctools\x264_x86.exe'
#avs2avi = r'G:\Enctools\avs2avi.exe'
if mmaudio and x264cli and avsname and script_org:
#if mmaudio and x264cli and avs2avi and avsname and script_org:
#--------------------------------------------------------------
    #Search Silence
    logfile_si = avsname + '_silence.log'
    text1 = '\nLoadPlugin("%s")' % mmaudio
    text1 += '\nAudioDub(BlankClip(last, width = 160, height = 120), last)'
    text1 += '\nWriteFileIf("%s", "AudioMax(0) < -100", "current_frame", append = false)' % logfile_si
    avsp.InsertText(text1, pos = -1, index = None)
    avsp.SaveScript()
    os.system('%s %s --preset ultrafast --crf 51 -I infinite -o nul' % (x264cli, avsname))
    #os.system('%s %s -c null -o n' % (avs2avi, avsname))

    #silence_log to freezeframelines
    avsp.SetText(script_org)
    sifile=open(logfile_si)
    lines_si=sifile.readlines()
    sifile.close()
    text2='\nPointResize(last.width / 4, last.height / 4)'
    if len(lines_si):
        x = 0
        for line_si in lines_si:
            log_si = int(line_si.strip())
            if x != log_si - 1:
                text2 += '\nFreezeFrame(%i, %i, %i)' % (x, log_si, log_si)
                x = log_si
            else:
                x += 1
        text2 += '\nFreezeFrame(%i, %i, %i)\n' % (int(lines_si[-1].strip()), 0, 0)
    avsp.InsertText(text2, pos = -1, index = None)
    avsp.SaveScript()
    
    #SCx264
    logfile_sc = avsname + '_scenechange.log'
    opts = '--preset ultrafast --crf 30 -I infinite -i 1 --scenecut 50 -o nul'
    os.system('%s %s -p 1 --stats %s %s' % (x264cli, avsname, logfile_sc, opts))
    avsp.SetText(script_org)
    avsp.SaveScript()

    #scenechange_log to bookmarks
    scfile = open(logfile_sc)
    lines_sc = scfile.readlines()
    scfile.close()
    bookmarks = []
    for line_sc in lines_sc:
        log_sc = line_sc.split(' ')
        if log_sc[2] == 'type:I':
            bmpoint = int(log_sc[0].lstrip('in:'))
            bookmarks.append(bmpoint)
    avsp.SetBookmark(bookmarks)

2010年10月16日土曜日

SCx264 to Bookmarks for AvsP

またまたAvsPマクロネタ。

さて、AviSynthのプラグインにはいろいろと毛色の変わったものがありますが、そんな中にSClavcSCXvidというものがあります。
オリジナルのSClavcはx264のペンギン様ことLoren Merritt氏作、SCXvidの作者は#darkholdのいかつい人、MyrsloikことFredrik Mellbin氏であります。(以前一度、JEEB氏に写真を見せてもらったことがあるのですが、Myrsloik氏はやたら人相の怖い太っちょでした。一方、一緒に写ってたTheFluff氏は小柄で柔和そうな感じで、普段の言動から受ける印象とあまりにも違いすぎるので、思わず大笑いしたものです)

で、これらが何のためにあるかというと、要するにffmpegやXvidの2pass用のログを利用して、クリップ内のシーンチェンジの場所とかを割り出して色々利用しようというものらしい。
なんで「らしい」かといえば、使い方がよくわからないから。
とりあえずavs書いてVirtualDubに食わせて"Run Video Analysis pass"を走らせるなりすれば、2pass用のlogファイルが作られるわけですが、そこから先がどうしたものやら…。
AviSynth.infoによれば、「元々は、Yattaにおいて、libavcodecのシーンチェンジ・メトリクスの利用を可能するために書かれた。」とありますが、Yattaってねぇ…あれの使い方がわかる日本人なんているのかしら?
7月ごろにJEEB氏に「#yattaでmenter氏が初心者講習会を開くので来ないか?」と誘われたのでROMってみましたが、何をしゃべってるのかさっぱりわかりませんでした。(そもそも知りたかったのは理論やテクニックではなく、操作方法そのものなのですが、そこらへんはやってくれなかったし…orz)

まあ、Yattaの使い方は今もさっぱりわかりませんが、2pass用logからシーンチェンジ検出というのは理解できます。
そこでふと思ったことが「それならx264のlogファイル使ったほうがよくね? lavcのmpeg4やXvidよりもlog出力速いじゃん」。
で、こんなのを書いてみた。
#SCx264 to bookmarks.py
frames = avsp.GetVideoFramecount()
logfilename = avsp.GetFilename(title='Select x264_logfile')

if logfilename:
    logfile = open(logfilename)
    logs = logfile.readlines()
    logfile.close()
    if len(logs) == frames + 1:
        bookmarks = []
        for logline in logs:
            log = logline.split(' ')
            if log[2] == 'type:I':
                bmpoint = int(log[0].lstrip('in:'))
                bookmarks.append(bmpoint)
        avsp.SetBookmark(bookmarks)
    else:
        avsp.MsgBox('This log is not corresponding to the number of frames of current clip.',title='Warning')

あらかじめx264.exeで --pass 1 をつけてエンコしたlogから、IDRフレームと判定されたフレームにブックマークを自動で打つマクロである。(またブックマークか…)
とりあえずシーンチェンジを検出して、ちゃんとIDRと判定されるようなオプションにしていれば、ほぼ100%検出できるはずである。

さて、ただlog読んでブックマーク打つだけでは面白くないので、さらに発展させてみる。
#SCx264 to bookmarks AUTO.py
import os
avsname = avsp.SaveScript()
#使用するx264.exeを毎回指定したい場合はこちら
#exe = avsp.GetFilename(title = 'Specify x264.exe used.')
#x264.exeのパスを固定したい場合は、こちらにフルパスで書いておく
exe = r'G:\Enctools\x264_x86.exe'

if avsname and exe:
    x264opt = '--preset ultrafast --crf 30 -p 1 -I infinite -i 1'
    thresh = '--scenecut 50'
    logfilename = avsname + '.sc_log'
    os.system('%s %s %s %s --stats %s -o nul' % (exe, avsname, x264opt, thresh, logfilename))

    logfile = open(logfilename)
    logs = logfile.readlines()
    logfile.close()
    bookmarks = []
    for logline in logs:
        log = logline.split(' ')
        if log[2] == 'type:I':
            bmpoint = int(log[0].lstrip('in:'))
            bookmarks.append(bmpoint)
    avsp.SetBookmark(bookmarks)

log出力も一緒にやるようにしてみた。

ううむ、これをどう使えばいいのだろうか?

例えばMPEG2Source("hogehoge.d2v")だけのavsの状態でこれを実行するとCMカットは楽になる。
なぜって、ただブックマーク移動していくだけで本編とCMの切れ目を確実にシークできるから。
AvsP初期装備のTrimEditorと併用すれば、ほとんどストレスなくCMカットできるはず(ただし、TrimEditorでTrimを実行する際は、ブックマークをすべてクリアしてからにすること。これを忘れるとプレビューの更新で随分時間を食うことになります)。
x264は--preset ultrafastならものすごく高速です。自分のそろそろ時代遅れなQ9450でも、1440x1080
のMPEG2-TSを90fps~100fpsで処理します。つーかMPEG2Sourceがボトルネックになっているので、本当はもっと速いです。2本同時にlog出力したりするといいかもしれません。

なお、「30分のTSだとlog出力だけで10分かかるじゃねえか、それだけあればCMカットなんか終わってるよ」とかいう意見は無視します…orz

2010年10月15日金曜日

分割結合マクロ for AvsP

前回のTrimEditorは、いざ使ってみるとメリットはそれほどなかったような気もするけど、まあ、マクロの練習にはなった。
さて、今回もBookmarkを使ったマクロを書いてみる。
#Divide and Concatenate.py
bookmarks=avsp.GetBookmarkList()
if bookmarks:
    bookmarks.sort()
    start, trim, cat = 0, "", "\n"
    for i in xrange(len(bookmarks)):
        trim += "\nV%02i = last.Trim(%i, %i)" % (i, start, bookmarks[i] - 1)
        cat += "V%02i ++ " % i
        start = bookmarks[i]
    i += 1
    trim += "\nV%02i = last.Trim(%i, 0)" % (i, start) 
    cat += "V%02i\n" % i
    avsp.InsertText(trim + cat, pos=None, index=None)
例えば
AVISource("hoge.avi")
AudioDub(Last, WAVSource("hoge.wav"))
なんていうavsがあったとして、1234,5678,9012フレームにブックマークを打ってからこのマクロを実行すると
AVISource("hoge.avi")
V01 = Last.Trim(0, 1233)
V02 = Last.Trim(1234, 5677)
V03 = Last.Trim(5678, 9011)
V04 = Last.Trim(9012, 0)
V01 + V02 + V03 + V04
AudioDub(Last, WAVSource("hoge.wav"))
というように、いったんブックマークの位置で分割して、最後に結合するスクリプトが挿入される(挿入位置はカーソルの位置)。
なににこんなものを使うかといえば、
AVISource("hoge.avi")
V01 = Last.Trim(0, 999).Sharpen(1.0)
V02 = Last.Trim(1000, 1999).Blur(1.0)
V03 = Last.Trim(2000, 2999).FlipVertical()
V04 = Last.Trim(3000, 0).FlipHorizontal()
V01 + V02 + V03 + V04
AudioDub(Last, WAVSource("hoge.wav"))
てな感じでFilterRangeのかわりにするとか、はたまたEasyVFRとか使う人には便利かもしれない。

2010年10月13日水曜日

俺様TrimEditor for AvsP

スクリプトなんて簡単なBash scriptとAviSynth scriptしか書けない私ですが、最近はほんの少しだけPythonをいじるようになりました。
なんでPythonかといえば、AvsP(AviSynth script editor)がPythonで書かれていて、マクロもPythonで書かないといけないから。

さて、本日のお題はTrimEditor。
AvsPには始めからTrimEditorが付いていますが、これが自分にはどうも使いにくい。
まずHomeを押すと出現するダイアログがウザい。
ある範囲を選択(削除)するごとにTrim(a,b)を1行挿入するので、Trimし終わったころには、それだけで7,8行のスクリプトになってしまうのもウザい。
というわけで、自分用のTrimEditorをでっち上げます。
#Bookmarks to TrimLine.py
bookmarks = avsp.GetBookmarkList()
num = len(bookmarks)
if num % 2 == 0 and num:
    bookmarks.sort()
    text = ''
    for i in range(0, num, 2):
        trimrange = bookmarks[i:i+2]
        cattext = '++Trim( %i, %i)' % (trimrange[0], trimrange[1])
        text += cattext
    avsp.InsertText(text.lstrip('++') + '\n', pos=None, index=None)
else:
    avsp.MsgBox('The number of bookmarks is odd (or zero).', title='Warning')
使い方:
ブックマーク(Ctrl+B)を適当にガンガン打って、マクロ実行。
2つのブックマークにはさまれた部位を残すスクリプトが、カーソル位置に1行で挿入されます。
例)ブックマークを1000,2000,3000,4000,5000,6000フレームに打って実行したら Trim(1000,2000)++Trim(3000,4000)++Trim(5000,6000) となる
なお、10行目のpos=Noneをpos=-1に書き換えると、スクリプトの最後に挿入位置が変更されます。