【全自動】水玉コラ生成マシーン
聖夜なので表題のものを作った。
processing で書いたアプリだけど、この記事の内容はほぼ OpenCV の話です。
仕組み
- 水着を自動認識して「隠す」とマーク
- 顔を自動認識して「見せる」とマーク
- マークに沿って円充填
水着領域の自動認識
最初のアプローチ
- OpenCV を使って肌色認識
- 選択領域を膨張 -> 収縮させる
- 肌色との差分を取れば水着領域が完成
肌色認識
先人が大量に居た。RGB 色空間ではなく HSV 色空間を使うというのがコツなようだ。
HSV 色空間なら影になっている部分も抽出できる。
今回は Hue: 7..15 を肌色として定義した。
PImage detectHada() {
// 作業用に hue で grayscale にする
opencv.loadImage(img);
opencv.useColor(HSB);
opencv.setGray(opencv.getH().clone());
// 肌色は 7..15 と定義。inRange で取り出す
opencv.inRange(7, 15);
// ノイズ除去 MORPH_OPEN
opencv.erode();
opencv.dilate();
return opencv.getSnapshot();
}
PImage detectHada() {
// 作業用に hue で grayscale にする
opencv.loadImage(img);
opencv.useColor(HSB);
opencv.setGray(opencv.getH().clone());
// 肌色は 7..15 と定義。inRange で取り出す
opencv.inRange(7, 15);
// ノイズ除去 MORPH_OPEN
opencv.erode();
opencv.dilate();
return opencv.getSnapshot();
}
膨張・収縮
欲しいのは肌色領域ではなく水着領域なので、肌と肌の間の空間を選択する必要がある。
膨張 -> 収縮し、最初との差分を取ることで取得できるのではないかと考えた。
// 輪郭を広げて戻す
for (int i = 0; i < 10; i++) { opencv.dilate(); }
for (int i = 0; i < 10; i++) { opencv.erode(); }
が、膨張する際に水着領域を上手に隠せないパターンが多い。
問題はループで 10 回回しているところにあり、本来は膨張・収縮時にパラメータを渡して解決すべきである。 デフォルトのパラメータのまま無理やりループで代替したところ、望むような膨張効果が得られていない。
opencv-processing でパラメータを渡す方法を調べるのに時間がかかりそうだったので次善の策に頼ることにした。
// 輪郭を広げて戻す
for (int i = 0; i < 10; i++) { opencv.dilate(); }
for (int i = 0; i < 10; i++) { opencv.erode(); }
彩度の高いもの=水着と仮定する
グラビアの水着はなぜか彩度の高いものが採用されていることが多いので「彩度の高いもの」という条件で水着が抽出できる。
いくつかパラメータを変えつつ試したが、
- Saturation: 180..255 (彩度の高いものを水着と仮定する)
- Brightness: 8..247 (あまりに暗いもの、明るすぎるものを除外する)
という絞り込みで、一定の条件の水着に対しては判定が効くようになった。
void detectMizugi() {
// 水着を彩度 180..255, 明度 8..247 と定義。inRange で取り出す
opencv.loadImage(img);
opencv.useColor(HSB);
opencv.setGray(opencv.getS().clone());
opencv.inRange(180, 255);
PImage s = opencv.getSnapshot();
opencv.loadImage(img);
opencv.useColor(HSB);
opencv.setGray(opencv.getB().clone());
opencv.inRange(8, 247);
PImage b = opencv.getSnapshot();
// TODO: 無駄に PImage に一度変換して blend しているが
// おそらく OpenCV のみで可能
s.blend(b, 0, 0, width, height, 0, 0, width, height, MULTIPLY);
opencv.loadImage(s);
opencv.erode();
opencv.dilate();
ArrayList<Contour> contours = opencv.findContours();
for (Contour contour : contours) {
// 面積が小さすぎるものはノイズと判断して弾く
if (contour.area() > 25) {
mizugiAreaList.add(contour);
}
}
}
void detectMizugi() {
// 水着を彩度 180..255, 明度 8..247 と定義。inRange で取り出す
opencv.loadImage(img);
opencv.useColor(HSB);
opencv.setGray(opencv.getS().clone());
opencv.inRange(180, 255);
PImage s = opencv.getSnapshot();
opencv.loadImage(img);
opencv.useColor(HSB);
opencv.setGray(opencv.getB().clone());
opencv.inRange(8, 247);
PImage b = opencv.getSnapshot();
// TODO: 無駄に PImage に一度変換して blend しているが
// おそらく OpenCV のみで可能
s.blend(b, 0, 0, width, height, 0, 0, width, height, MULTIPLY);
opencv.loadImage(s);
opencv.erode();
opencv.dilate();
ArrayList<Contour> contours = opencv.findContours();
for (Contour contour : contours) {
// 面積が小さすぎるものはノイズと判断して弾く
if (contour.area() > 25) {
mizugiAreaList.add(contour);
}
}
}
顔領域の自動認識
OpenCV に任せて 2 行で解決した。
opencv.loadCascade(OpenCV.CASCADE_FRONTALFACE);
Rectangle[] faces = opencv.detect();
デフォルトだと誤認識率が 30% ぐらいあるが、OpenCV に同梱されている分類器を複数重ね合わせて使う のように、認識率を上げる手段がまだあるようだ。
opencv.loadCascade(OpenCV.CASCADE_FRONTALFACE);
Rectangle[] faces = opencv.detect();
円充填
ここまでで「隠す領域」と「隠してはいけない領域」が抽出できたので、 いい感じに円で埋めていく作業をする。
等で勉強したのだが、美しい水玉コラを作るには
- 水着を全て隠す
- 顔は必ず見せる
- 水玉は重ねない
- 水玉の数は多くても 15 個まで。少なければ少ないほど良い
といった標準的なルールの他に
- 穴が直線上に並ばないようにする
- 隠された領域が水着っぽく見えてしまい、「らしさ」が薄れる
- 肩、くびれを見せる
- ボディラインを強調することができ、より完成度が高く見える
- 谷間・アゴを見せる
- 「らしさ」が一層高まる
といったテクニックがあるようだ。
とはいえ「くびれを学習させた教師データ」なるものは存在しないので、右上、左上から先に開けることでなるべく肩が含まれることを期待するにとどめた。
ここでゲームで培った当たり判定の技術 (OBB と点の当たり判定、凸包と点の当たり判定等) が生きたんだが、 処理を簡便化するために水着領域を凸包にしたことで谷間を見せることができなくなってしまった。
(黄色の部分が当たり判定になるので谷間は必ず隠れてしまう)
ここは今後改善したい。
- 隠された領域が水着っぽく見えてしまい、「らしさ」が薄れる
- ボディラインを強調することができ、より完成度が高く見える
- 「らしさ」が一層高まる
0 件のコメント :
コメントを投稿