AEのスクリプトをTypeScriptで書く

環境

TypeScript

インストール

Node.jsが入ってる前提で始める。

typescriptはnpm経由で以下のようにすればグローバルにインストールされる。

npm i -g typescript

ナイトリービルド版は、@nextをつけて。

npm i -g typescript@next

今ちょうどTypeScriptの2.0.0のベータ版が出てて、割りと痒いところに手が届くようになったので、それを使いたい場合は、

npm i -g typescript@beta

とすればよい。

tsconfig.json

tsconfig.jsonは、TypeScriptをJavaScriptコンパイルする際のコンパイラオプション等を書いておくファイル。プロジェクトフォルダのルートに置いておくと、コンパイルする際に自動的に見に来てくれる。中身は単純なのだとこんな感じ。

{
    "compilerOptions": {
        "target": "es3",
        "outFile": "ScriptExample.jsx"
    },
    "files": [
        "typings/aftereffects/ae.d.ts",
        "ScriptExample.ts"
    ]
}

ExtendScriptはes3相当なので、targetはes3固定。後、デフォルトだと当然.jsで出力されるので、outFileで.jsxで出力するようにすれば楽。

型定義ファイル

型の恩恵を受けられないとTypeScriptを使う必要がない。型の恩恵を受けるには型定義ファイルが必要だ。 ということで、https://github.com/atarabi/aftereffects.d.tsから型定義ファイルを拾ってこよう。

Visual Studio Code

TypeScriptでの開発はVS Codeが便利。Visual Studio Codeからダウンロード出来る。

使用するTypeScriptを切り替える

デフォルトだとVS Codeに内蔵されたTypeScriptが使用されるので、それを変更する。 Ctrl+Shift+Pを押すとコマンドパレットが開くので、そこでPreferences: Open User Settingsを探す。 選択すると、settings.jsonが開かれるので、そこでtypescript.tsdkの項を追加し使用したいバージョンのTypeScriptのフォルダを指定する。

"typescript.tsdk": "C:/Users/ユーザー名/AppData/Roaming/npm/node_modules/typescript/lib"

のような感じになるはず。

スクランナー

VS Codeには、ビルド等のタスクをエディタ内から楽に実行出来るタスクランナーという機能がある。 この機能を使ってTypeScriptをwatchモードでビルドすることを考える。 Ctrl+Shift+Pを押してコマンドパレットを開き、Tasks: Configure Task Runnerを選択すると、いくつか項目が出るのでTypeScript - Watch Modeを選ぶ。 すると.vscode以下にtask.jsonが生成される。これで、コマンドパレットからTasks: Run Build Taskを選択するか、Ctrl+Shift+Bを押すと さきほど設定したタスクが実行され、tsconfig.jsonの内容に基づき、watchモードでファイル保存のたびに自動でコンパイルしてくれるようになる。

こんな感じだろうか

TypeScript 2.0環境前提。ae-typescript-example

TypeScriptその他

基本的な文法以外。

type guard、型推論まわり

TypeScript 2.0でタイプガード周りが向上したので、下記のように早期リターンがしやすくなった。

(() => {
    const comp = app.project.activeItem;
    if (!(comp instanceof CompItem)) {
        return;
    }
    comp;//上のif文でCompItem以外はreturnしたので、ここはCompItemと推測してくれる!今まではItemと認識されていた。
})();

プロパティに関してもタイプガードしてくれるようになった。

if (app.project.activeItem instanceof CompItem) {
    const comp = app.project.activeItem;//CompItemと認識してくれる!
}

クラスの判定

ExtendScriptにおけるサブクラスといった概念は、JavaScript的に継承してるのではなく、上位と同じメソッド、プロパティを持ってる程度でしかないので 例えばあるレイヤーがAVLayerかどうかをチェックしたい場合は、

if (layer instanceof AVLayer || layer instanceof TextLayer || layer instanceof ShapeLayer) {
    //layer is AVLayer!!
}

のように、AVLayerとそのサブクラスを羅列する必要がある。これを関数化した場合、

function isAVLayer(layer: Layer) {
    return layer instanceof AVLayer || layer instanceof TextLayer || layer instanceof ShapeLayer;
}

のようになるわけだが、これだと型周りの情報が抜けてtrueの場合にAVLayerであると認識してくれない。

if (isAVLayer(layer)) {
    layer;//Layerクラス!!
}

そこで、下記のようにtrueの場合layerの型はAVLayerですよ!と書くと、

function isAVLayer(layer: Layer): layer is AVLayer {
    return layer instanceof AVLayer || layer instanceof TextLayer || layer instanceof ShapeLayer;
}

きちんと解釈してくれる。

if (isAVLayer(layer)) {
    layer;//AVLayerクラス!!
}

polyfill

TypeScriptはes5のpolyfillは用意してくれない(Startupにpolyfillをデフォルトで突っ込んでくれればいいと思うけど)。 for-of文は使えるので、配列周りで面倒なことはそこまでないと思う。

const comp = app.project.activeItem;
if (!(comp instanceof CompItem)) {
    return;
}

const layers = comp.selectedLayers;
const shape_layers: ShapeLayer[] = [];
for (let layer of layers) {
    if (layer instanceof ShapeLayer) {
        shape_layers.push(layer);
    }
}

es6

上記のfor-of文、let, constや、クラス、分割代入、テンプレート文字列等、es6の機能は普通に使えるのでどんどん使っていけばいいと思う。

const comp = app.project.activeItem;
if (!(comp instanceof CompItem)) {
    return;
}
const {name, width, height, pixelAspect, bgColor} = comp;
const [red, green, blue] = bgColor;
comp.layers.addSolid([1 - red, 1 - green, 1 - blue], `invert of "${name}"'s bgColor'`, width, height, pixelAspect);

AfterEffectsで機械学習 その2

前回のつづき。

トランジション

入力としてr, g, bにrateを加えて、rateを動かすことで色調を変化させていくことを試みる。


rateが0のときは元画像(左上)、50.0のときはコロラマを掛けた画像(右上)、100.0のときはVC Color Vibranceを掛けた画像(左下)、となるように学習させた。普通の写真よりかは色んな色が混ざっている方が学習するにはいいので、学習時はカラーノイズに差し替えてある。間がどう補間されるかっていうのは、モデルの構成や初期値等々によって結構変わるもんなんだろうか。

位置から色へ

今度はx, y(位置)を入力として、色を出力させてみる。


学習がすすむにつれ、何となく近づいていってるのが分かる。最終的な画は使用しているモデルがどこまで表現できるかに依存するわけだけども、300*200=60000pixelsの画を再現するのに、数百個程度の重みじゃ厳しい。

のちのち

次こそ畳み込み層も加えて色々したい。

AfterEffectsで機械学習

AEでPythonの例

機械学習Pythonが楽なのでとりあえずAEにPythonを組み込むことを考える。AEとPythonを連携させた例は以下のようなものがある。

ひとつ目は、スクリプトからbat経由でPythonスクリプトを実行するというもの。
ふたつ目は、スクリプトPythonを組み込むもの。
みっつ目の"Omino Python"は今回やりたいことの方向性と似ているけど、Cairoを扱って描写というのがちょっと扱いにくい(ピクセルをnumpyの配列で扱いたい)ので参考にしつつ自作する。

AEで実際にPython


AEプラグイン側から呼び出されるpythonのコードはこんな感じ。基本、AEプラグインと関数の命名、機能等を対応させる感じで。ParamsSetup()で、使用パラメータやチャンネルの順序を指定(OpenCVはBGRの順なため変えられるようにしてある)。SmartRender()でパラメータの実際の値や、入力画像であるnumpyの配列を受け取り色々処理した後AEプラグイン側にnumpyの配列を返して後はよしなに描写。



CC2015でOpenCV使おうとすると処理が戻ってこなくなったり、モジュールによっては落ちたりするけど、軽くフィルタを作る分には非常に楽。ひとつ前の記事の4コマの奴もこれでトライアンドエラーした。

AEで機械学習

上では紹介してないけど、トラッカーのようにある時間幅(現在フレームのみ、ワークエリア等)を指定してその間を逐次処理する機能も実装してある。最終的に何かの値をキーフレームにベイクする、連番ファイルを渡してプロジェクトに読み込む的な用途を期待してるんだけど、単純にフレームのピクセル情報を得て色々するのにも当然使えるので、それを利用して学習させる。

画像を扱うならCNNっていう感じなんだろうけど、まだあまり理解してないのと計算コストが大きいのとどういう方向でやると面白くなりそうかというのがまだ明確じゃない。なので、単純にピクセルのマッピングを学習させる。つまり、フィルタを掛ける前の画像のピクセル列を入力ピクセル、フィルタを掛けたあとの画像のピクセル列を教師ピクセルとして、そのフィルタを学習させる。


機械学習部のコードの断片。機械学習のライブラリはChainerのv1.5。別に特別なことはやっていない。入力はR, G, Bの3つ、いくつかの全結合層で繋いで、入力と同様に3つの値を出力。中間層の活性化関数はReLU。後、回帰なので誤差関数は平均二乗誤差。



左上が元画像で左下がLightroomで現像した画像、右下が学習したフィルタを適用した画像となっている。動画内では3エポック回してるが、学習が進むにつれ右下の画像の色合いが左下の画像の色合いに近付いていってるのが分かる。


f:id:kareobana:20160110043659j:plain
左が元画像、真ん中がLightroomで現像した画像、右が学習したフィルタを適用した画像である。学習したフィルタは一度3D LUTに出力してから適用してある。LUTはAEだとApply Color LUTかRed GiantのLut Buddy(Free)で読み込める。


f:id:kareobana:20160110043850j:plain
差分画像、ちょっと分かりにくいが、ヘルメットや勝負服、ブリンカー等の色鮮やかなところの再現性が少し劣っている。


f:id:kareobana:20160110045139j:plain
f:id:kareobana:20160110045144j:plain
他の画像に学習させたフィルタを適用した例。なかなか見栄えがする。

のちのち

ヴィネッティングの効果も学習させたい場合、r, g, bだけでなく位置情報も入力する必要があるけど、当然3D LUTには出力できないので可搬性に劣るのでそこらへんは何とかする必要がありそう。 後、適当なパラメータを入力に加えて、それを弄ることでトランジションさせていったりとか。
どっちにしろ、現状の1ピクセル入力、1ピクセル出力だとやれることに限度があるので、もうちょっと色々考えたい。

4コマ漫画のコマを切り抜く

esuji5.hateblo.jp

上記の記事に触発され自分も書いてみました。

4コマを切り出す

srcフォルダ以下にある画像を再帰的に処理してdstフォルダ以下に切り抜き画像を出力するようにしてあります。

まず

OpenCVにfindContoursという輪郭を抽出するメソッドがあるのでそれでコマの輪郭を抽出することを目指します。画像サンプルは手持ちで上からとった悪条件のものを。

二値化
def apply_adaptive_threshold(image, radius=15, C=5):
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    return cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 2 * radius + 1, C)

輪郭を抽出する前にまず二値化する必要があります。グレーに変換した後、Adaptive Thresholdを行います。Adaptiveの場合、画像全体で一意の閾値を適用するのではなく周囲のピクセルに応じて閾値を上下させるので影があっても綺麗に処理してくれます。
条件が悪い場合はブロックサイズを大きくしたり等パラメータを調整する必要がありますが、スキャナできちんとスキャンしたものを対象にする場合はそこまで気を使う必要はないと思います。

輪郭抽出
def find_external_contours(thresh):
    _, contours, hierarchy = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    external_num = hierarchy.shape[1] if hierarchy is not None else 0
    return contours[0:external_num]

findContoursで輪郭を抽出します。今回は一番外部の輪郭(コマの内部の輪郭は必要ない)のみが必要なので2つめのパラメータにcv2.RETR_EXTERNALを渡します。するとhierarchyに一番外部の輪郭のみのリストが返ってくるので、個数を数えてその分だけの輪郭データだけを返すようにしてます。
OpenCV3系では戻り値が変わって最初に画像を返すようになったので、OpenCV2系の場合は注意が必要です。

コマの輪郭だけを得、その外接矩形を取得する
def extract_rects_from_controus(contours, min_perimeter, max_perimeter):
    frames = []
    for contour in contours:
        frame = cv2.minAreaRect(contour)
        center, size, angle = frame
        # 縦・横が逆になっている場合、90度回転させる
        if angle < -45:
            size = tuple(reversed(size))
            angle = angle + 90
        w, h = size
        perimeter = 2 * (w + h)
        if min_perimeter < perimeter < max_perimeter and abs(angle) < 3.0 and 0.1 <= min(w, h) / max(w, h) <= 1.0:
            frames.append((center, (w + 2, h + 2), angle))  # パディングを加える
    return frames

輪郭の外接矩形を取得し、その矩形の、周長、角度、縦横比でフィルタを掛け、コマの輪郭だけを抽出を試みてます。

コマをソートする
def cmp_frame(tolerance):
    def _cmp(lhs, rhs):
        return (lhs > rhs) - (lhs < rhs)

    def _cmp_frame(lhs, rhs):
        if lhs[0] == rhs[0]:
            return 0
        x1, y1 = lhs[0]
        x2, y2 = rhs[0]
        if abs(x1 - x2) < tolerance:
            return _cmp(y1, y2)
        else:
            return _cmp(x2, x1)

    return _cmp_frame

コマを外接矩形の中心位置に基づいて並び替えます。傾いている場合も考慮して、x方向は許容範囲内であれば同じx位置と見て、y方向のみで比較するようにしてます。

rects = sorted(rects, key=cmp_to_key(cmp_frame(tolerance)))

python3系ではsortedにcmpを渡せなくなったのでfunctools.cmp_to_keyを用いてます。python2系の場合は直接cmpに渡せば大丈夫かと思います。

コマの部分を切り抜く
def cut_frame(image, rect):
    center, size, angle = rect
    size = int(np.round(size[0])), int(np.round(size[1]))
    box = cv2.boxPoints(rect)
    M = cv2.getAffineTransform(np.float32(box[1:4]),  np.float32([[0, 0], [size[0], 0], [size[0], size[1]]]))
    return cv2.warpAffine(image, M, size)

アフィン変換で傾きを直しつつ切り抜いてます。

かわいい。

無意識を意識するには

 惡の華を見ましたが面白いですね。去年の謎の彼女Xといい講談社は変な原作を持ってきますね。

 一話の構成は、日常を淡々と描いてこれから起こる非日常的体験に対するお膳立てといった感じですね。当然ですが非日常という起伏を描くには日常という平坦を描く必要があるわけです。非日常を続けまくったら非日常性が強化されるかといったらそうではなくて、それがその世界の日常なんだなと認識されるだけです。裏拍をずっと続けたらいつの間にか表拍として聞こえるように。また、ああいった生徒時代の帰り道での友人との会話とかあまりにも日常過ぎてわざわざ意識して覚えようとしないため記憶に残らないものですよね。それを年取って当事者ではなく第三者として観察せざるをえない状況になってどう思うか。アニメは実写より観察するようになる、と高畑勲が言ってた気がしますが、アニメの場合背景は基本変化しないので必然的に動いているモノに注目するわけですね。で、惡の華の場合細田守のように影もないので余計ちょっとした目の動きだとか手の動きだとかに目が行くわけです。関係者の方が、これをトレスしてるアニメータは勉強になるだろうなと言ってましたが、自分自身の無意識のうちにやってるちょっとした動きっていうのは当然気づくわけがないので、そういった動きを描こうと思ったら"意識的に"他人が無意識にやっている行為を観察しないといけないわけですね。しかもそういった無意識な動きが動きを自然に見せる要素ともなりうるので、自然な動きを描く(特に日常生活での)っていうのは難しいわけです。今回アニメータはトレスをする際に、必然的にそういった無意識の動きの線を引くことになって人間って存外動いてるんだなというのを思わざるを得なくて、結果として勉強になるということですね。友人の山田の髪とか小泉純一郎の切り抜き並にやりたくないですが。

塔のある風景

http://www.flickr.com/photos/kareobana/sets/72157631651892208/

 街中から見える塔って惹かれませんか。特に八坂通から見る法観寺の五重塔は格別ですね。ということで、塔とその周りの風景を題材に何か撮ろうと前々から考えていて、それと同時に写真の写真を撮る表現も何かしたいなとも考えていて(こういうのは例として、Dear Photographのような昔の写真と現代の風景との対比させているものや、PENCIL VS CAMERAみたいな虚実が一体化したようなものがありますね)結果としてああいう感じになりました。標準域で撮る、周りの景色が十分映るよう塔が全体の1/4にすっぽりはまるぐらいの大きさに撮ると決めていたので、撮影範囲は自ずと限られてその中でいい構図を探す必要性があったり、上下左右方向だけでなく奥行き方向も合わせる必要性があったり、塔が右上ならバランス保つために左下に何か副主題がほしいなと考えながら撮ったり、なかなかに面白かったです。これからも適当にテーマを見繕っては撮って行きたい。