驟雨

今週のお題はてなブログ フォトコンテスト 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

AfterEffectsで機械学習 その3

10ヶ月前にDCGANでゆゆ式のキャラの顔を訓練させたの。


これを、AEで読み込む。

かわいい。

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)

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

かわいい。