#sailormoonredraw gan
発端
セーラームーンのあれはスクレイピング的なあれしてGANとかであれこれしたいよね
— mas (@mas134) 2020年5月24日
スクレイピング
twitterのAPI代わりにtogetterを代用するのはよくやっていて、togetterでとりあえず一日単位で#sailormoonredrawの画像付きツイートをまとめて、それをスクレイピングして画像を収集。
前々処理
今回の場合、オリジナルの画像(複数種類あった)も比較対象として同時に上げているツイートが多かったのでそれを取り除く必要があった。画像の比較にはPerceptual Hashを使用。性質上オリジナルと似た画像が多いため、閾値を少し上げただけで結構な数の画像を弾いてしまった。そこで、ハッシュ値が完全に一致した場合のみ弾くことにした。
前処理
学習用のデータとするために、顔の部分を中心として512x512の矩形に切り抜く必要がある。顔検出にはlbpcascade_animeface.xmlを用いた。
ultraist.hatenablog.com
どう切り抜くかであるが、リファレンスとしてオリジナルの画像を512x512に切り抜いたものを顔検出。
画像の外枠の正方形と顔検出された矩形との対応関係が求まるので、後はそれを元に個々の収集した画像を顔検出して切り抜き。誤検知、関係ない画像を目視で取り除いて、計9578枚。
学習
StyleGANを使用。Google Colaboratoryで学習させるために、下記のを少し改変したものを用いた。
github.com
Google Colaboratoryはセッション切れ、タイムアウトがあるので、それを考慮してモデルの途中保存の頻度を大きくした。
プレ映像
GANの潜在変数をぐるぐるさせる系の映像作品は、実際の展示でも既に3回ぐらいは見ていて食傷気味である。画像を大量に学習させて少しずつ潜在変数を移動させれば、絵となる映像が容易に出力出来るので、流行るのもむべなるかな、といった感じではあるが、見せ方が中央に一つドンと見せるかグリッド状に見せるかのどちらかが多かったので、そこに多少のアレンジを加えた。
つまり、複数解像度の正方形を組み合わせて、タイル状に並べるようにした(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スクリプトを書いた。これだと一時間足らずで処理が終わった。
AEでノイズを抑えるためにCC Wide Timeを掛けたり、色調を整えたりして完成。
おわり
ここらへんの技術はアイディア次第で上手くオリジナルの映像作品へと昇華出来そうではあるが自分の中にまだ確固たるものがない。
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を連携させた例は以下のようなものがある。
- デジタル・フロンティア-Digital Frontier | DF TALK | AfterEffectsスクリプトの使用状況をExcelに出力してみよう ~Javascriptと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エポック回してるが、学習が進むにつれ右下の画像の色合いが左下の画像の色合いに近付いていってるのが分かる。
左が元画像、真ん中がLightroomで現像した画像、右が学習したフィルタを適用した画像である。学習したフィルタは一度3D LUTに出力してから適用してある。LUTはAEだとApply Color LUTかRed GiantのLut Buddy(Free)で読み込める。
差分画像、ちょっと分かりにくいが、ヘルメットや勝負服、ブリンカー等の色鮮やかなところの再現性が少し劣っている。
他の画像に学習させたフィルタを適用した例。なかなか見栄えがする。
のちのち
ヴィネッティングの効果も学習させたい場合、r, g, bだけでなく位置情報も入力する必要があるけど、当然3D LUTには出力できないので可搬性に劣るのでそこらへんは何とかする必要がありそう。 後、適当なパラメータを入力に加えて、それを弄ることでトランジションさせていったりとか。
どっちにしろ、現状の1ピクセル入力、1ピクセル出力だとやれることに限度があるので、もうちょっと色々考えたい。