After Effectsのスクリプトの可能性を広げる。

AEで処理を自動化する際によく用いられるのはスクリプトであるが、プラグインにもエフェクト以外にAEGPというスクリプト的なことも出来るタイプのものがある。大概のことはスクリプトでも出来るので、あまり出る幕がないのだが、AEGPのAPIを用いないと出来ないこともままあって、そういう場合は渋々C++を書かざるを得ない。しかし、特に単機能のものを書くためだけに、わざわざC++を持ち出すのも面倒であるし、何とかしたい。そこで、スクリプト側からAEGPのAPIを呼び出せるようなラッパー的なライブラリを作ることを考える。

つまり、斜線部を考える。こういうのをあらかじめ作ってさえおけば、あとはC++に立ち入ることなしに、スクリプトだけで完結するので、非常に便利である。ということで作った。


github.com

初代はどういうことが出来るのか色々実験した結果、迷走し複雑化したので、それを糧に二代目をあらためて作り直したものの、今度は別の構想がなかなか煮詰まらず、遅れに遅れた。

at_script/script/types/@script.d.ts at main · atarabi/at_script · GitHub

主なAPIはここを参照すればよいが、 document は まだ整備されてない (@script — documentation 最低限の体裁は整えた。(2023/9/6))。

Releases · atarabi/at_script · GitHub

必要なものは上から @script.zip をダウンロードすれば手に入る*1


最低限必要なものは、 プラグイン*2と Startup 内の !@script_initializer.jsx。後者は必要不可欠というわけではないが、Startup時にAPIを使用したい場合に、初期化処理のために用いる。というのも、AEのスクリプト機能自体がプラグインで実装されているため、こちらのプラグインがそれより先に読み込まれた場合に、スクリプト周りの初期化処理が出来ない。したがって、初期化処理を遅延させているのだが、そのタイミングがStartupのスクリプトの実行タイミングより遅れるため、こちらから促す必要がある。頭に ! をつけているのは、Startupスクリプトの中でも最初に実行してほしいため*3

いにしえより伝わる global にオブジェクトをひとつ作り*4、その下に根を生やしていく方式で構築している。使い方としては、

(() => {
    const comp = app.project.activeItem;
    if (!(comp instanceof CompItem)) {
        return;
    }
    const layers = comp.selectedLayers;
    const isAVLayer = (layer: Layer): layer is AVLayer => {
        return layer instanceof TextLayer || layer instanceof ShapeLayer || layer instanceof AVLayer;
    };
    for (const layer of layers) {
        if (!isAVLayer(layer)) {
            continue;
        }
        Atarabi.layer.setNull(layer, true); // ココ!
    }
})();

といった感じで利用する。上のは選択レイヤーにヌル属性を設定する例である。スクリプトではヌル属性が read-only であるが、AEGPのAPIだと書き込みもできる。


同梱物

APIを使用したスクリプト(使用していないものもある)を同梱してある。

Startup

!@script_initializer

上記通り、StartupにおいてAPIを利用したい場合は、これをStartupのフォルダに入れる必要がある。

@command_set_null

選択レイヤーにヌル属性を追加するSet NullコマンドをLayerメニューに追加する。

(() => {
    Atarabi.register.insertCommand('Layer', 'AtTop', 'Set Null', () => {
        // 内容は上の例とほぼ同じ
    }, 'LayerSelected');
})();

このように簡単にコマンドを差し込むことができる。'LayerSelected'は、レイヤーを選択しているときだけコマンドを有効化させることを意味する。当初はここも関数で指定できるようにしてあったが、Mac環境だとエラーが出るため、ご破産となった。

@fold_layers

一時期話題になっていたGM_FoldLayersのクローン。LayerメニューにAdd Separatorコマンドが追加され、実行すると、▾ Separatorという名前のシェイプレイヤーが出来る。これが区切り線がわりとなっており、ダブルクリックをすると、シャイレイヤーの機能によって開閉がなされる。が区切り線用のレイヤーか否かのマーカーになっているので、Separator部分の名前は変更可能。

Atarabi.register.hookCommand(3974/* レイヤーをダブルクリックする際に発行されるコマンド */, ctx => {
    const markedLayer = getMarkedLayer();
    if (!markedLayer) {
        ctx.fallback = true;
        return;
    }
    // メインの処理
});

既存のコマンドを上記のように簡単にhook出来るようにしてある。マーカー以外のレイヤーではデフォルトの挙動をして欲しい。そこで、渡されたctxfallbacktrueにすることで、デフォルトのコマンドの挙動にフォールバックするようにしてある。ここらへんはTJ氏がWQというプラグインを作る際にも、色々試行錯誤されていた。

@hook_adjustment, @hook_null, @hook_solid

調整レイヤーにしろヌルレイヤーにしろ平面レイヤーにしろ、既存の平面レイヤーを出来るだけ使いまわして欲しい。そこで、そこらへんのコマンドをフックすることで使い回すようにしてある。平面レイヤーは白色固定なので、カラフルにしたい場合は各自、改造してみるのもいいかもしれない。

昔、ここらへんの平面レイヤーを用いるコマンドをシェイプレイヤーに置き換えるAEGPプラグインを作ったことがあったが、このときは基本の処理はスクリプトで書き、プラグイン内でそれを実行し、ヌル属性を付加することろだけは、別途C++で書いていた。今は全てスクリプトで書けるので、いい時代になったものである。

@hook_fit_to_comp

Fit to Compコマンドは、縦横比を無視してコンポサイズに拡大縮小する。個人的に、縦横比を維持して欲しいなということが多いので、フックして縦横比を維持するようにしている。現状、background-size: contain;的な処理をしているが、background-size: cover;的な処理も欲しいなと思うことが時たまあるので、いずれ選択式にしたい。

@script_UI

私がScriptUIのGUIを楽して作る用のライブラリ。下記のScriptUI系のスクリプトは全てこれを用いて作っている。

ScriptUI

上記通り、@script_UIが必須。

選択しているエフェクトのドロップダウンリスト系のパラメータのテキストを羅列する。ダブルクリックすることで値を変更でき、さらにReal Timeをチェックすれば、クリックするだけで値が変わる。

@paramdefs_viewer

選択しているエフェクトのパラメータの詳細を得る。APIを通じて、Custom Propertyの値もBase64エンコードされた形で取得できる。これを用いて、Custom Propertyの書き込みも出来るが、内部的にポインタを用いてるものに対しては、無理に書き込もうとすると落ちる*5

エフェクトをFuzzy Search出来る。

@effect_preset

気軽にエフェクトのプリセットを作れる。ffxよりもうちょっと気軽にプリセット的なのを扱いたい用。上で書いたように、CurvesのようなCustom Propertyの値も扱えるので割りと使える。

@still_maker

コンポないし選択レイヤーの現在フレームを静止画として保存できる。

@movie_maker

コンポないし選択レイヤーをGIF, APNGとして保存出来る。最適化されているわけではないので、適当に扱ってもよいとき用。

@rpp_loader

REAPERのプロジェクトをAEに読み込む - Atarabi-Tumblr

これの移植、修正版。


余談。

{
    "compilerOptions": {
        "jsx": "preserve",
        "rootDir": "./src",
        "outDir": "./dst"
    },
    "include": [
        "./types/aftereffects.d.ts/ae.d.ts",
        "./src/**/*.tsx"
    ]
}

TypeScriptでスクリプトを書く際は、設定を上記のようにするのが楽。味噌は、スクリプトのファイルの拡張子をtsxにすることと、"jsx": "preserve"にすること。こうすることで、トランスパイルした際に自動的に拡張子がjsxになるので、リネームせずに済む。

*1:スクリプト関連は自分でトランスパイルしてもよい。

*2:Windowsの場合は@script.aex、Macの場合は@script.plugin。

*3:スクリプトはおそらくASCII順で実行される。そうでなかった場合は、ただのおまじないと化す。

*4:このライブラリに関してはAtarabi

*5:Mesh Warp、 Reshapeなど。

vending,machine,Candela,

興福寺の夜を待つために日の沈むのを眺めていたら、南円堂の前の自販機が目に入った。前々からこれを上手く撮りたいと思っていて、南円堂も枠内におさめ、幾枚か撮影するも自販機の灯りがか弱い。
南円堂は東面し、自販機は一つを除き北面しており、直交している。いい具合に顔をこちらに向けてもらうには、斜めから撮る他ない。自販機の機嫌をうかがいすぎると南円堂の正面性が損なわれるし、加減が難しい。


いい塩梅に松も立っている。松の全体を入れ込むと自販機とお堂の印象が弱くなるか、否、松が左に視線が行くのを遮るのでかえって強くなるのでは、などと構図を模索している内に自販機の輝きは増し、ついには空に伍するまでになった。


普段、マジックアワーのような時間帯を決めて出掛けることもないので、これがあのブルーアワーかと得心した。




東大寺大仏殿と猫段上の売店横の自販機も、ちょうど同じ位置関係にあるな、と思い出したので、後日、天気の良い日に向かった。二月堂で日が沈むのを見届け、一目散に向かう。


南円堂に比べ、大仏殿から自販機の距離があるので、角度に余裕がなく、木々も遠くを隠し、カメラ位置の制約は大きい。また、南円堂の経験から、石畳も視線をお堂へ誘う線として用いることができる、と想定していたが、自販機や外灯からやや離れており、暗く厳しい。
制約が大きいということは、探るべき domain も絞られるので、いい面でもあるのだが、得られる極大が高々ということもあり得る。地形の現状変更は倫理的に行えないので、どうしようもないことではあるが。


また、二台の自販機を結ぶ線の先に大仏殿が大まかにあってほしいし、大仏の瓦は暗く見づらいので、最低限、鴟尾と屋根の反りのシルエットははっきりと見えて欲しい。
こんな感じでどんどん条件を課していくと、あまりよろしくない局所解に嵌まる危険性もあり、帰って写真を見返した際に後悔することも多いのだが、時間も限られるし仕方がない。




そういえば、自販機と雪の素晴らしい写真があったな、と調べたら、大橋英児氏の作品であった。

eijiohashi.com

Roadside Lights I

太陽なきみそらのもとでは自販機がお山の大将であり、ダミーサンプルが煌々としている。したがって、街の景も取り込もうとすると、自販機が明るすぎる。一方、カラー写真であるし、ダミーサンプルのカラフルさも見せたい。となると、露出を変えて幾枚か撮影してHDR合成を、ということになる。しかし、ハイライトを抑えてラベルをよく見せようとすると、今度は神々しさが薄れる。眩しきもののディティールとグローの両立はなかなかに難しい。

Being There

モノクロのシリーズとなると、色彩は影を潜めるので、輝きが重視され、自由気ままに白飛びさせてある。カラーと白黒とで何を重視するのか、というのがはっきりと表れている。




興福寺五重塔のライトアップがしばらくお休みに入るので、その最終日に投光器を撮りに行った*1


帰りしな、南円堂と自販機をHDR合成用に撮影した。現像は、ハイライトを白飛びさせつつも、ダミーサンプルの範囲だけは銘柄が微かにわかる程度に明るさを抑えた。眩しきもののディティールとグローの両立は難しい。

*1:照明のデータ・シート (No.208) に投光器設置時当初の仕様が載っているが、それ以降どう更新されていったのかは追えなかった。

#sailormoonredraw gan

www.youtube.com

発端

スクレイピング

twitterAPI代わりにtogetterを代用するのはよくやっていて、togetterでとりあえず一日単位で#sailormoonredrawの画像付きツイートをまとめて、それをスクレイピングして画像を収集。

前々処理

今回の場合、オリジナルの画像(複数種類あった)も比較対象として同時に上げているツイートが多かったのでそれを取り除く必要があった。画像の比較にはPerceptual Hashを使用。性質上オリジナルと似た画像が多いため、閾値を少し上げただけで結構な数の画像を弾いてしまった。そこで、ハッシュ値が完全に一致した場合のみ弾くことにした。

前処理

学習用のデータとするために、顔の部分を中心として512x512の矩形に切り抜く必要がある。顔検出にはlbpcascade_animeface.xmlを用いた。
ultraist.hatenablog.com

どう切り抜くかであるが、リファレンスとしてオリジナルの画像を512x512に切り抜いたものを顔検出。
f:id:kareobana:20200613225715p:plain
画像の外枠の正方形と顔検出された矩形との対応関係が求まるので、後はそれを元に個々の収集した画像を顔検出して切り抜き。誤検知、関係ない画像を目視で取り除いて、計9578枚。

学習

StyleGANを使用。Google Colaboratoryで学習させるために、下記のを少し改変したものを用いた。
github.com
Google Colaboratoryはセッション切れ、タイムアウトがあるので、それを考慮してモデルの途中保存の頻度を大きくした。

プレ映像

GANの潜在変数をぐるぐるさせる系の映像作品は、実際の展示でも既に3回ぐらいは見ていて食傷気味である。画像を大量に学習させて少しずつ潜在変数を移動させれば、絵となる映像が容易に出力出来るので、流行るのもむべなるかな、といった感じではあるが、見せ方が中央に一つドンと見せるかグリッド状に見せるかのどちらかが多かったので、そこに多少のアレンジを加えた。
f:id:kareobana:20200613232115p:plain
つまり、複数解像度の正方形を組み合わせて、タイル状に並べるようにした(StyleGANが低解像度から徐々に高解像度へと学習をすすめる仕組みなのでそれを利用したというのもある)。

final int width= 60;
final int height = 34;
final int scaling = 16;

int grid[][];
ArrayList<Cell> cells;

class Cell {
    int x, y, size;
    color col;

    Cell(int x, int y, int size) {
        this.x = x;
        this.y = y;
        this.size = size;
        this.col = color(random(255), random(255), random(255));
    }

    void draw() {
        fill(this.col);
        rect(this.x * scaling, this.y * scaling, this.size * scaling, this.size * scaling);
    }

    void write(PrintWriter writer) {
        writer.println(this.x + "\t" + this.y + "\t" + this.size);
    }
}

void generateCells() {
    for (int y = 0; y < height; ++y) {
        for (int x = 0; x < width; ++x) {
            grid[y][x] = 0;
        }
    }

    cells.clear();

    for (int size : new int[]{8, 4, 2}) {
        for (int retry = 0; retry< 10000; ++retry) {
            int x = (int)random(0, width - size + 1);
            int y = (int)random(0, height - size + 1);
            boolean ok = true;
            for (int yy = y; yy < y + size; ++yy) {
                for (int xx = x; xx < x + size; ++xx) {
                    if (grid[yy][xx] == 1) {
                        ok = false;
                        break;
                    }
                }
                if (!ok) {
                    break;
                }
            }
            if (ok) {
                for (int yy = y; yy < y + size; ++yy) {
                    for (int xx = x; xx < x + size; ++xx) {
                        grid[yy][xx] = 1;
                    }
                }
                cells.add(new Cell(x, y, size));
            }
        }
    }

    for (int y = 0; y < height; ++y) {
        for (int x = 0; x < width; ++x) {
            if (grid[y][x] == 0) {
                grid[y][x] = 1;
                cells.add(new Cell(x, y, 1));
            }
        }
    }
}

void settings() {
    size(width * scaling, height * scaling);
}

void setup() {
    stroke(0);
    grid = new int[height][width];
    cells = new ArrayList<Cell>();
    generateCells();
}

void draw() {
    noLoop();
    background(255);
    for (Cell cell : cells) {
        cell.draw();
    }
}

void mousePressed() {
    generateCells();
    loop();
}

String zfill(int number) {
    return String.format("%02d", number);
}

void keyPressed() {
    if ((key == 'S') || (key == 's')) {
        String name = "cells_" + zfill(month()) + zfill(day()) + zfill(hour()) + zfill(minute()) + zfill(second());
        PrintWriter writer = createWriter(name +  ".txt"); 
        for (Cell cell : cells) {
            cell.write(writer);
        }
        writer.flush();
        writer.close();
        save(name + ".png");
    }
}

具合の良いタイルにしたいので、簡単にprocessingで生成出来るようにして、tsvとして個々のタイルの位置とサイズを出力するようにした。

映像

あとは、潜在変数が動き回る映像をランダムシードを変えて複数解像度で大量に生成した後、タイル状に並べるだけである。当初は、AEに映像を読み込んでスクリプトでtsvの指示通りに配置した後、少し加工して出力するつもりであったが、レンダリングの想定時間が一日半を示したため断念した。AEだと大量のファイルを読み込んで1フレーム出力また大量のファイルを読み込んで1フレーム出力といった処理となるため、I/O部分がネックになるのである。そこで、メモリ上に画像バッファを大量に確保しておいて、そこに映像を一つずつ読み込んで書き込んでいき、最後に、一気に画像バッファを出力するというPythonスクリプトを書いた。これだと一時間足らずで処理が終わった。
f:id:kareobana:20200613234654p:plain
AEでノイズを抑えるためにCC Wide Timeを掛けたり、色調を整えたりして完成。

おわり

ここらへんの技術はアイディア次第で上手くオリジナルの映像作品へと昇華出来そうではあるが自分の中にまだ確固たるものがない。

驟雨

今週のお題はてなブログ フォトコンテスト 2017夏」


f:id:kareobana:20170709154834j:plain雨は突然に、貴賤、人種関係なく等しく降り注ぐ。


f:id:kareobana:20170709155753j:plain雨具を忘れた彼、彼女らも、僕と同様、どこか屋根のある場所を探しているみたいだ。


f:id:kareobana:20170709155800j:plainやっとの思いで到着した軒下は、既に先客でいっぱいだ。


f:id:kareobana:20170709161116j:plain遠慮して隅の方で休憩。早く雨やまないかな。でも、しばらくこのままでもいいかも。

AfterEffectsで機械学習 その4

下記の線画着色と、スタイル変換で遊んだ。両者ともにCPUで処理したのだけども、線画の方は1分くらい処理に時間が掛かったので、早送りしてある。今のところAEをただのGUIとしてしか扱っていないのでもうちょっと何とかしたい。


qiita.com


qiita.com

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);