発端
セーラームーンのあれはスクレイピング的なあれして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を掛けたり、色調を整えたりして完成。
おわり
ここらへんの技術はアイディア次第で上手くオリジナルの映像作品へと昇華出来そうではあるが自分の中にまだ確固たるものがない。